fuzzing

Fuzzing Günlüğü #3: Guardlar, Gizli Tipler ve Ölçmek


TurboFan kodu optimize ederken “bu değerler hep küçük tamsayı olacak” gibi varsayımlar yapıyor ve her varsayımın önüne bir guard koyuyordu. Bugünkü yazıda o varsayımları bilerek bozup motorun ne yaptığına baktım. Bu sırada fark ettim ki sadece fonksiyonların değil, dizilerin ve nesnelerin de gizli tipleri var.

Bütün denemeleri daha önce derlediğim d8 üzerinde, % ile başlayan "servis modu" komutlarıyla yaptım (bkz. 2. yazı).

1-) Bir guard’ı suçüstü yakalamak

Basit bir add fonksiyonunu önce hep tamsayılarla çağırdım ve optimize olmasını tetikledim. Sonra aynı fonksiyona bir string verdim:

add(5, 6);        // optimize edilmiş kod çalışıyor
add("x", "y");    // varsayımı bozuyoruz

Motorun cevabı şu şekilde oldu:

[bailout (reason: not a Smi): deoptimizing add ...]

add, optimize durumundan (bir kod içi rozet: 41) tek hamlede yorumlayıcıya geri düştü (65). Buna deoptimization deniyor: guard “bu bir tamsayı değil!” deyip optimize kodu çöpe attı ve fonksiyonu en yavaş ama en güvenli hâline, yorumlayıcıya geri gönderdi.

Ayrıca add(1.5, 2) de aynı şekilde patladı. 1.5 bir sayı ama guard’ın umurunda değil — o sadece “küçük tamsayı mı, yoksa bellekte bir nesne mi?” diye bakıyor. Yani TurboFan “sayı”ya göre değil, özellikle küçük tamsayıya göre özelleşmişti. (Dar varsayım = hızlı ama kırılgan kod)

Son olarak gördüm ki motor pes etmiyor. Aynı fonksiyonu bu kez hep string’lerle çağırınca onu yeniden optimize etti — bu sefer string birleştirmeye göre. Tek bir hatadan sonra fonksiyonu ölü ilan etmiyor, yeni duruma uyarlanıyor.

2-) Dizilerin gizli tipi

Burada işin ilginçleştiği yer: [1, 2, 3] V8’in gözünde “genel bir dizi” değil. Onun bir tipi var: “içinde sadece küçük tamsayı olan, deliksiz dizi” (PACKED_SMI). Diziyle oynadıkça bu tip değişiyor:

let a = [1, 2, 3];   // PACKED_SMI  (sadece tamsayı)
a.push(4.5);         // PACKED_DOUBLE (artık ondalık da var)
a.push({});          // PACKED_ELEMENTS (artık her şey olabilir)

Açıklama

Bu bir merdiven ve sadece aşağı iniliyor: dizi “gevşedikçe” daha genel bir tipe geçiyor, ama asla geri dönmüyor. Mesela bir diziye float ekleyip hemen geri çıkardım, dizi yine de “double olabilir” tipinde kaldı, tamsayı tipine dönmedi. V8 “bir kez ondalık gördüm, yine görebilirim” diye varsayımı geri almıyor. Küçük bir not: gereksiz yere karışık (farklı tipte elemanlı) ya da delikli diziler kurmak, motoru daha genel ve daha yavaş bir yola sokuyor.

3-) Nesnelerin gizli tipi

Aynı mekanizma nesnelerde de var. Her nesnenin bir hidden class (V8’in deyimiyle Map) var: hangi alanlara, hangi sırayla sahip olduğunun tarifi. Aynı tarifteki iki nesne aynı Map’i paylaşıyor:

let p1 = {}; p1.x = 1; p1.y = 2;
let p2 = {}; p2.x = 3; p2.y = 4;
// p1 ve p2 aynı Map'i paylaşır → true

Ama özellikleri farklı sırayla eklersen (önce y sonra x) ayrı bir Map doğuyor — aynı alanlara sahip olsalar bile. “Nesne alanlarını hep aynı sırada başlat” tavsiyesinin sebebi bu.

Bu kısımda beklentilerim iki kez boşa çıktı. Önce “literal ile elle kurduğum nesne herhalde aynı Map’i paylaşır” diye düşünmüştüm — öyle olmadı. Sonra “o zaman fark, alanların const olmasıdır” diye tahmin ettim; o da tutmadı. Motorun kendi çıktısına baktığımda gördüm ki iki nesne bellekte farklı boyutta olduğu için V8 onları farklı tip sayıyormuş. Aynı görünüyorlardı ama aynı değillerdi. V8 motorunun çıktılarının önemini bir kez daha gördük.