Kaydol

Merhaba Sevgili Floodlar.com Kullanıcısı, Web sitemizde geçirdiğiniz zaman ve bu büyüleyici flood evrenine katılımınız için teşekkür ederiz. Floodların geniş dünyasıyla dolu deneyiminizi daha fazla keşfetmek için, web sitemizi sınırsız olarak kullanabilmeniz adına giriş yapmanız gerekmektedir.

Oturum aç

Merhaba Floodlar.com Kullanıcısı, İlk üç sayfayı tamamladınız, tebrikler! Ancak, floodların devamını görmek ve daha fazla interaktif deneyim yaşamak için giriş yapmanız gerekiyor. Hesabınız yoksa, hızlıca oluşturabilirsiniz. Sınırsız floodlar ve etkileşimler sizleri bekliyor. Giriş yapmayı unutmayın!

Şifremi hatırlamıyorum

Şifreniz mi unuttunuz? Endişelenmeyin! Lütfen kayıtlı e-posta adresinizi giriniz. Size bir bağlantı göndereceğiz ve bu link üzerinden yeni bir şifre oluşturabileceksiniz.

Fil Necati Masonlar Locası Subreddit Adı Nedir? Cevap: ( N31 )

Üzgünüz, flood girme izniniz yok, Flood girmek için giriş yapmalısınız.

Lütfen bu Floodun neden bildirilmesi gerektiğini düşündüğünüzü kısaca açıklayın.

Lütfen bu cevabın neden bildirilmesi gerektiğini kısaca açıklayın.

Lütfen bu kullanıcının neden rapor edilmesi gerektiğini düşündüğünüzü kısaca açıklayın.

Mobil Uygulamada Açın

Güncel Floodlar En sonuncu Nesne

Emscripten kullanarak WebAssembly’de bellek sızıntılarında hata ayıklama

Emscripten kullanarak WebAssembly’de bellek sızıntılarında hata ayıklama

Squoosh.app kaliteyi önemli ölçüde etkilemeden görüntü dosyası boyutunu ne kadar farklı görüntü kodeklerinin ve ayarlarının iyileştirebileceğini gösteren bir PWA’dır. Bununla birlikte, aynı zamanda C++ veya Rust ile yazılmış kitaplıkları alıp web’e nasıl taşıyabileceğinizi gösteren teknik bir demodur.

Mevcut ekosistemlerden kod taşıyabilmek inanılmaz derecede değerlidir, ancak bu statik diller ile JavaScript arasında bazı önemli farklılıklar vardır. Bunlardan biri, bellek yönetimine yönelik farklı yaklaşımlarıdır.

JavaScript kendi kendini temizleme konusunda oldukça bağışlayıcı olsa da, bu tür statik diller kesinlikle değildir. Açıkça yeni bir ayrılmış hafıza istemeniz ve daha sonra geri verdiğinizden emin olmanız ve bir daha asla kullanmamanız gerekir. Bu olmazsa, sızıntı olur… ve aslında oldukça düzenli bir şekilde olur. Bu bellek sızıntılarını nasıl ayıklayabileceğinize ve daha da iyisi, bir dahaki sefere bunlardan kaçınmak için kodunuzu nasıl tasarlayabileceğinize bir göz atalım.

şüpheli model #

Son zamanlarda, Squoosh üzerinde çalışmaya başlarken, C++ codec paketleyicilerinde ilginç bir model fark etmeme engel olamadım. bir göz atalım ImageQuant örnek olarak sarıcı (yalnızca nesne oluşturma ve ayırma parçalarını gösterecek şekilde küçültüldü):

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;

attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);

// …

free(image8bit);
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);

return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
}

void free_result() {
free(result);
}

JavaScript (iyi, TypeScript):

export async function process(data: ImageData, opts: QuantizeOptions) {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;

const result = module.quantize(/* … */);

module.free_result();

return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}

Bir sorun fark ettiniz mi? ipucu: bu ücretsiz kullanımancak JavaScript’te!

Emscripten’de, typed_memory_view bir JavaScript döndürür Uint8Array WebAssembly (Wasm) bellek arabelleği tarafından desteklenir ve byteOffset Ve byteLength verilen işaretçiye ve uzunluğa ayarlayın. Ana nokta, bunun bir TypedArray olmasıdır. görüş verilerin JavaScript’e ait bir kopyası yerine bir WebAssembly bellek arabelleğine.

aradığımızda free_result JavaScript’ten sırayla standart bir C işlevi çağırır free bu belleği gelecekteki herhangi bir tahsis için kullanılabilir olarak işaretlemek, yani bizim Uint8Array görüş noktaları, Wasm’a gelecekteki herhangi bir çağrı ile isteğe bağlı verilerle üzerine yazılabilir.

Veya, bazı uygulama free boşalan hafızayı hemen sıfır doldurmaya bile karar verebilir. bu free Emscripten’in kullandığı bunu yapmaz, ancak burada garanti edilemeyen bir uygulama detayına güveniyoruz.

Veya işaretçinin arkasındaki bellek korunsa bile, yeni ayırmanın WebAssembly belleğini büyütmesi gerekebilir. Ne zaman WebAssembly.Memory JavaScript API aracılığıyla veya karşılık gelen memory.grow talimat, mevcut olanı geçersiz kılar ArrayBuffer ve geçişli olarak, onun tarafından desteklenen tüm görüşler.

Bu davranışı göstermek için DevTools (veya Node.js) konsolunu kullanmama izin verin:

> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}

> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42

> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
// (the size of the buffer is 1 WebAssembly "page" == 64KB)

> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data

> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!

> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one

Son olarak, arada bir tekrar Wasm’ı açıkça aramasak bile free_result Ve new Uint8ClampedArray, bir noktada codec’lerimize çoklu okuma desteği ekleyebiliriz. Bu durumda, biz onu klonlamayı başarmadan hemen önce verilerin üzerine yazan tamamen farklı bir iş parçacığı olabilir.

Bellek hatalarını arıyorum #

Her ihtimale karşı, daha ileri gitmeye ve bu kodun pratikte herhangi bir sorun gösterip göstermediğini kontrol etmeye karar verdim. Bu yeni(ish) denemek için mükemmel bir fırsat gibi görünüyor Emscripten dezenfektan desteği geçen yıl eklenen ve Chrome Dev Summit’teki WebAssembly konuşmamızda sunulan:

Bu durumda, ilgilendiğimiz AdresDezenfektanişaretçi ve bellekle ilgili çeşitli sorunları algılayabilen. Bunu kullanmak için codec bileşenimizi yeniden derlememiz gerekiyor. -fsanitize=address:

emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \

node_modules/libimagequant/libimagequant.a

Bu, işaretçi güvenlik kontrollerini otomatik olarak etkinleştirir, ancak olası bellek sızıntılarını da bulmak istiyoruz. ImageQuant’ı bir program yerine bir kitaplık olarak kullandığımız için, Emscripten’in tüm belleğin boşaltıldığını otomatik olarak doğrulayabileceği bir “çıkış noktası” yoktur.

Bunun yerine, bu gibi durumlar için LeakSanitizer (AddressSanitizer’a dahildir) şu işlevleri sağlar: __lsan_do_leak_check Ve __lsan_do_recoverable_leak_checktüm belleğin serbest kalmasını beklediğimizde ve bu varsayımı doğrulamak istediğimizde manuel olarak çağrılabilir. __lsan_do_leak_check çalışan bir uygulamanın sonunda, herhangi bir sızıntı tespit edilmesi durumunda işlemi iptal etmek istediğinizde kullanılmak üzere tasarlanmıştır. __lsan_do_recoverable_leak_check bizimki gibi kütüphane kullanım durumları için daha uygundur, sızıntıları konsola yazdırmak istediğinizde, ancak uygulamayı ne olursa olsun çalışır durumda tutun.

Bu ikinci yardımcıyı Embind aracılığıyla açığa çıkaralım, böylece onu JavaScript’ten istediğimiz zaman çağırabiliriz:



// …

void free_result() {
free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result);

}

Görüntüyle işimiz bittiğinde onu JavaScript tarafından çağırın. Bunu C++ yerine JavaScript tarafından yapmak, biz bu kontrolleri çalıştırana kadar tüm kapsamlardan çıkıldığından ve tüm geçici C++ nesnelerinin serbest bırakıldığından emin olmaya yardımcı olur:

  // …

const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

module.free_result();


return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}

Bu bize konsolda aşağıdakine benzer bir rapor verir:

Uh-oh, bazı küçük sızıntılar var, ancak tüm işlev adları karıştırıldığı için yığın izleme pek yardımcı olmuyor. Onları korumak için temel bir hata ayıklama bilgisi ile yeniden derleyelim:

emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \

node_modules/libimagequant/libimagequant.a

Bu çok daha iyi görünüyor:

Yığın izlemenin bazı bölümleri, Emscripten dahili bileşenlerine işaret ettikleri için hala belirsiz görünüyor, ancak sızıntının bir kaynaktan geldiğini söyleyebiliriz. RawImage Embind tarafından “wire type” (bir JavaScript değerine) dönüştürülür. Nitekim koda baktığımızda geri döndüğümüzü görebiliriz. RawImage C++ örneklerini JavaScript’e dönüştürür, ancak bunları hiçbir zaman iki tarafta da serbest bırakmayız.

Bir hatırlatma olarak, şu anda JavaScript ve WebAssembly arasında herhangi bir çöp toplama entegrasyonu yoktur, ancak biri geliştiriliyor. Bunun yerine, nesneyle işiniz bittiğinde, tüm belleği manuel olarak boşaltmanız ve JavaScript tarafından yıkıcıları çağırmanız gerekir. Özellikle Embind için, resmi belgeler aramayı öner .delete() maruz kalan C++ sınıflarında yöntem:

JavaScript kodu, aldığı herhangi bir C++ nesne tanıtıcısını açıkça silmelidir, aksi takdirde Emscripten yığını süresiz olarak büyüyecektir.

var x = new Module.MyClass;
x.method();
x.delete();

Aslında, bunu sınıfımız için JavaScript’te yaptığımızda:

  // …

const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);

module.free_result();

module.doLeakCheck();

return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}

Sızıntı beklendiği gibi gider.

Dezenfektanlarla ilgili daha fazla sorun keşfetme #

Temizleyicilerle diğer Squoosh kodeklerini oluşturmak, hem benzer hem de bazı yeni sorunları ortaya çıkarır. Örneğin, MozJPEG bağlamalarında şu hatayı alıyorum:

Burada sızdırma değil, ayrılan sınırlar dışında bir hatıraya yazıyoruz 😱

MozJPEG’in kodunu incelerken, buradaki sorunun şu olduğunu görüyoruz: jpeg_mem_dest—JPEG için bir bellek hedefi tahsis etmek için kullandığımız işlev—mevcut değerleri yeniden kullanır outbuffer Ve outsize sıfır olmadıklarında:

if (*outbuffer == NULL || *outsize == 0) {
/* Allocate initial buffer */
dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
if (dest->newbuffer == NULL)
ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
*outsize = OUTPUT_BUF_SIZE;
}

Ancak, bu değişkenlerden herhangi birini başlatmadan onu çağırırız, bu da MozJPEG’in sonucu, çağrı sırasında bu değişkenlerde saklanan potansiyel olarak rastgele bir bellek adresine yazdığı anlamına gelir!

uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);

Çağırmadan önce her iki değişkeni de sıfırlamak bu sorunu çözer ve şimdi kod bunun yerine bir bellek sızıntısı kontrolüne ulaşır. Neyse ki, kontrol başarıyla geçti ve bu codec bileşeninde herhangi bir sızıntı olmadığını gösteriyor.

Paylaşılan durumla ilgili sorunlar #

…Yoksa biz mi?

Codec bağlamalarımızın, genel statik değişkenlerdeki durumların yanı sıra sonuçların bir kısmını sakladığını biliyoruz ve MozJPEG’in bazı özellikle karmaşık yapıları var.

uint8_t* last_result;
struct jpeg_compress_struct cinfo;

val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
// …
}

Ya bunlardan bazıları ilk çalıştırmada tembel bir şekilde başlatılırsa ve ardından sonraki çalıştırmalarda uygunsuz bir şekilde yeniden kullanılırsa? O zaman dezenfektanla yapılan tek bir arama onları sorunlu olarak bildirmez.

Kullanıcı arayüzünde farklı kalite seviyelerine rastgele tıklayarak görüntüyü birkaç kez deneyelim ve işleyelim. Nitekim, şimdi aşağıdaki raporu alıyoruz:

262.144 bayt—görünüşe göre tüm örnek görüntü şu kaynaktan sızdırılmış: jpeg_finish_compress!

Dokümanları ve resmi örnekleri inceledikten sonra, ortaya çıkıyor ki jpeg_finish_compress tarafından ayrılan hafızayı boşaltmaz. jpeg_mem_dest çağrı—sıkıştırma yapısı bellek hedefimizi zaten biliyor olsa bile, yalnızca sıkıştırma yapısını serbest bırakır… Ah.

Verileri manuel olarak serbest bırakarak bunu düzeltebiliriz. free_result işlev:

void free_result() {
/* This is an important step since it will release a good deal of memory. */
free(last_result);

}

Bu bellek hatalarını birer birer avlamaya devam edebilirdim, ancak bellek yönetimine yönelik mevcut yaklaşımın bazı kötü sistematik sorunlara yol açtığının şimdiye kadar yeterince açık olduğunu düşünüyorum.

Bazıları dezenfektan tarafından hemen yakalanabilir. Diğerleri yakalanmak için karmaşık numaralar gerektirir. Son olarak, günlüklerden de görebileceğimiz gibi, dezenfektan tarafından hiç yakalanmayan, gönderinin başındaki gibi sorunlar var. Bunun nedeni, asıl yanlış kullanımın, temizleyicinin görünür olmadığı JavaScript tarafında gerçekleşmesidir. Bu sorunlar kendilerini yalnızca üretimde veya gelecekte kodda görünüşte ilgisiz değişikliklerden sonra ortaya çıkaracaktır.

Güvenli bir sarmalayıcı oluşturma #

Birkaç adım geriye gidelim ve bunun yerine kodu daha güvenli bir şekilde yeniden yapılandırarak tüm bu sorunları çözelim. Yine örnek olarak ImageQuant sarıcı kullanacağım, ancak benzer yeniden düzenleme kuralları tüm kodekler ve diğer benzer kod tabanları için geçerlidir.

Öncelikle ücretsiz kullan-sonrası sorununu gönderinin başından itibaren düzeltelim. Bunun için, JavaScript tarafında ücretsiz olarak işaretlemeden önce WebAssembly destekli görünümdeki verileri klonlamamız gerekiyor:

  // …

const result = /* … */;







module.free_result();
result.delete();
module.doLeakCheck();

return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);

}

Şimdi, çağrılar arasında global değişkenlerde herhangi bir durumu paylaşmadığımızdan emin olalım. Bu, hem şimdiye kadar gördüğümüz bazı sorunları çözecek hem de gelecekte kodeklerimizi çok iş parçacıklı bir ortamda kullanmayı kolaylaştıracak.

Bunu yapmak için, işleve yapılan her çağrının yerel değişkenleri kullanarak kendi verilerini yönettiğinden emin olmak için C++ sarıcısını yeniden düzenleriz. Ardından, imzamızı değiştirebiliriz. free_result işaretçiyi geri kabul etme işlevi:

liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;

RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;

attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);


liq_set_max_colors(attr, num_colors);

liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);


// …
}

void free_result() {

free(result);
}

Ancak JavaScript ile etkileşim için Embind’i Emscripten’de zaten kullandığımız için, C++ bellek yönetimi ayrıntılarını tamamen gizleyerek API’yi daha da güvenli hale getirebiliriz!

Bunun için taşıyalım new Uint8ClampedArray(…) Embind ile JavaScript’ten C++ tarafına geçiş. Ardından, verileri JavaScript belleğine klonlamak için bile kullanabiliriz. önce fonksiyondan dönen:

class RawImage {
public:
val buffer;
int width;
int height;

RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};


RawImage quantize(/* … */) {

// …
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};






}

Tek bir değişiklikle hem sonuçtaki bayt dizisinin JavaScript’e ait olmasını hem de WebAssembly belleği tarafından desteklenmemesini sağladığımıza dikkat edin. Ve daha önce sızanlardan kurtulun RawImage sarıcı da.

Artık JavaScript’in verileri serbest bırakma konusunda endişelenmesine gerek yok ve sonucu herhangi bir çöp toplanmış nesne gibi kullanabilir:

  // …

const result = /* … */;

const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);

module.free_result();
result.delete();
// module.doLeakCheck();

return imgData;

}

Bu aynı zamanda artık bir geleneğe ihtiyacımız olmadığı anlamına da geliyor. free_result C++ tarafında bağlama:

void free_result(uint8_t* result) {
free(result);
}

EMSCRIPTEN_BINDINGS(my_module) {
class_RawImage>("RawImage")
.property("buffer", &RawImage::buffer)
.property("width", &RawImage::width)
.property("height", &RawImage::height);

function("quantize", &quantize);
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result, allow_raw_pointers());
}

Sonuç olarak, sarmalayıcı kodumuz aynı anda hem daha temiz hem de daha güvenli hale geldi.

Bundan sonra ImageQuant paketleyicisinin kodunda bazı küçük iyileştirmeler yaptım ve diğer codec’ler için benzer bellek yönetimi düzeltmelerini kopyaladım. Daha fazla ayrıntıyla ilgileniyorsanız, ortaya çıkan PR’yi burada görebilirsiniz: C++ kodekleri için bellek düzeltmeleri.

paketler #

Diğer kod tabanlarına uygulanabilecek bu yeniden düzenlemeden hangi dersleri öğrenebilir ve paylaşabiliriz?

  • Hangi dilden oluşturulmuş olursa olsun, WebAssembly tarafından desteklenen bellek görünümlerini tek bir başlatmanın ötesinde kullanmayın. Bundan daha uzun süre hayatta kalacaklarına güvenemezsiniz ve bu hataları geleneksel yöntemlerle yakalayamazsınız, bu nedenle verileri daha sonrası için saklamanız gerekirse, JavaScript tarafına kopyalayın ve orada saklayın.
  • Mümkünse, doğrudan ham işaretçiler üzerinde çalışmak yerine, güvenli bir bellek yönetimi dili veya en azından güvenli tip sarmalayıcılar kullanın. Bu sizi JavaScript ↔ WebAssembly sınırındaki hatalardan kurtarmaz, ancak en azından statik dil kodu tarafından kendi kendine yeten hataların yüzeyini azaltır.
  • Hangi dili kullanırsanız kullanın, geliştirme sırasında temizleyicilerle kod çalıştırın; bunlar yalnızca statik dil kodundaki sorunları değil, aramayı unutmak gibi JavaScript ↔ WebAssembly sınırındaki bazı sorunları da yakalamaya yardımcı olabilir. .delete() veya JavaScript tarafından geçersiz işaretçiler iletilmesi.
  • Mümkünse, yönetilmeyen verileri ve nesneleri WebAssembly’den JavaScript’e tamamen göstermekten kaçının. JavaScript çöp toplayan bir dildir ve manuel bellek yönetimi bu dilde yaygın değildir. Bu, WebAssembly’ınızın oluşturulduğu dilin bellek modelinin soyutlama sızıntısı olarak kabul edilebilir ve bir JavaScript kod tabanında yanlış yönetimi gözden kaçırmak kolaydır.
  • Bu bariz olabilir, ancak diğer tüm kod tabanlarında olduğu gibi değişken durumu global değişkenlerde depolamaktan kaçının. Çeşitli çağrılarda ve hatta ileti dizilerinde yeniden kullanımıyla ilgili sorunları ayıklamak istemezsiniz, bu nedenle onu olabildiğince bağımsız tutmak en iyisidir.

İlgili Mesajlar

Yorum eklemek için giriş yapmalısınız.