Inline Assembly

Inline assembly ile Solidity ifadelerini Ethereum Sanal Makine’sinin dillerinden birine yakın dile çevirebilirsiniz. Bu size özellikle dili kütüphaneler yazarak geliştiriyorsanız daha detaylı bir kontrol sağlar.

Inline assembly için kullanılan Solidity diline Yul deniyor ve dosyalarını kendi bölümünde bulabilirsiniz. BU bölüm sadece inline assembly kodunun etrafındaki Solidity kodları ile nasıl bağlandığını anlatacak.

Uyarı

Inline assembly Ethereum Sanal Makinesi’ne düşük seviyede erişişmin bir yoludur. Bu, Solidity’nin birçok güvenlik özelliklerini ve kontrollerini yok sayar. Yani inline assembly’i sadece gereken yerlerde ve nasıl kullanacağınızdan eminseniz kullanmalısınız.

Bir inline assembly bloğu assembly { ... } ile işaretlidir. Süslü parantez içerisindeki kod Yul dili içerisinde yer alır.

Bir inline assembly kodu yerel Solidity değişkenlerine aşağıda açıklandığı gibi erişebilir.

Farklı inline assembly blokları aynı yer adlarını paylaşmazlar. Yani farklı bir inline assembly bloğunda tanımlanmış olan bir Yul fonksiyonunu çağırmak ya da bir Yul değişkenine erişmek mümkün değldir.

Örnek

Aşağıdaki örnek başka bir kontrat üzerindeki koda erişimi ve bir bytes değişkenine atımını sağlayan kütüphane kodunu verir. Bu “düz Solidity” ile de <address>.code kullanarak mümkündür ama buradaki amaç tekrar kullanılabilir assembly kütüphanelerinin bir derleyici(compiler) değişimi olmadan Solidity dilini geliştirebildiğini göstermektir.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

library GetCode {
    function at(address addr) public view returns (bytes memory code) {
        assembly {
            // kodun boyutunu döndürür, burası için assembly kullanılmalı
            let size := extcodesize(addr)
            // çıkış bit array'ini allocate() eder
            // burası assembly kullanmadan,
            // code = new bytes(size) kullanarak da yapılabilir.
            code := mload(0x40)
            // padding'i içeren yeni "memory end"
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // uzunluğu hafızada saklayın
            mstore(code, size)
            // kodun şu anki halini döndürür, burası için assembly kullanılmalı
            extcodecopy(addr, add(code, 0x20), 0, size)
        }
    }
}

Inline assembly optimizer verimli kodlar üretemediği zamanlarda da yararlıdır, örneğin:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;


library VectorSum {
    // Bu fonksiyon şu anda verimli değil
    // çünkü optimizer array sınır erişim kontrolünü yaparken hata veriyor
    function sumSolidity(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i)
            sum += data[i];
    }

    // Array'e sadece sınırları içerisinde erişebileceğimizi biliyoruz, yani bu kontrolü atlayabiliriz.
    // 0x20'nin array'e eklenmesi gerekiyor çünkü array'in ilk slotu array uzunluğunu içerir.
    function sumAsm(uint[] memory data) public pure returns (uint sum) {
        for (uint i = 0; i < data.length; ++i) {
            assembly {
                sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
            }
        }
    }
    // Yukarıdaki gibi ama tüm kodu inline assembly kullanarak tamamlayın.
    function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
        assembly {
            // uzunluğu yükleyin (önce 32 byte)
            let len := mload(data)

            // Uzunluk alanını atlayın.
            //
            // Geçici bir değişken tutun, böylece yer değiştikçe onu da arttırabilirsiniz.
            //
            // NOT: Bu assembly bloktan sonra arttırılan veri kullanılamayacak bir değişkene dönüşecek

            let dataElementLocation := add(data, 0x20)

            // Sınıra ulaşana kadar tekrarlayın.
            for
                { let end := add(dataElementLocation, mul(len, 0x20)) }
                lt(dataElementLocation, end)
                { dataElementLocation := add(dataElementLocation, 0x20) }
            {
                sum := add(sum, mload(dataElementLocation))
            }
        }
    }
}

Dış(External) değişkenlere, fonksiyonlara ve kütüphanelere erişim

Solidity değişkenlerine ve diğer tanımlayıcılara isimlerini kullanarak erişebilirsiniz.

Bir değer tipinin yerel değişkenleri inline assembly içinde kullanılabilir durumdadır. Bu yerel değişkenler okunabilir de atanabilir de.

Belleği kasteden yerel değişkenler değerin kendisini değil, değerin bellekteki adresini işaret eder. Bu değişkenler aynı zamanda değiştirilebilir de ancak bu sadece bir pointer değişimi olur, veri değişimi olmaz. Bu sebeple Solidity’nin hafıza yönetimini yapmak sizin yükümlülüğünüzdedir. Bkz Solidity’de Konvansiyonlar

Benzer şekilde, statik boyutlandırılmış calldata array’leri ya da struct’ları gösteren yerel değişkenler de değerin adresini işaret eder, değerini değil. Bu değişken yeni bir offset’e de atanabilir fakat değişkenin calldatasize() çalıştırılması dışında bir yeri işaret edebileceğinin hiçbir garantisi yoktur.

Dış(External) fonksiyon pointer’ları için adres ve fonksiyon seçiyiye x.address ve x.selector ile erişilebilir. Seçici dört adet right-aligned bitten oluşur. İki değer de atanbilir. Örneğin:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.10 <0.9.0;

contract C {
    // @fun değerini dönmek için yeni bir seçici de adres atayın
    function combineToFunctionPointer(address newAddress, uint newSelector) public pure returns (function() external fun) {
        assembly {
            fun.selector := newSelector
            fun.address  := newAddress
        }
    }
}

Dinamik calldata array’leri üzerinde, x.offset ve x.length kullanarak -bit halinde- calldata offset’ine ve uzunluğuna erişebilirsiniz. Her iki ifade aynı zamanda atanabilir de ama statik bir durum için dönecekleri sonucun calldatasize() sınırları içerisinde olacağının bir garantisi yoktur.

Yerel depolama değişkenleri ya da durum değişkenleri için tek bir Yul tanımlayıcısı yeterli değildir. Çünkü bu değişkenler her zaman tam bir depolama alanı kaplamazlar. Bu sebeple onların ‘adresleri’ bir slottan ve o slot içerisindeki bir byte-offset’ten oluşur. x değişkeni tarafından işaret edilen slotu çağırmak için x.slot , byte-offset’i çağırmak için ise x.offset kullanılır. Sadece x kullanmak ise hata verecektir.

Bir yerel depolama değişkeninin pointer’ının .slot kısmına atama yapılabilir. Bu değişkenler(struct, array, mapping) için .offset kısmı ise her zaman sıfırdır. Fakat bir durum değişkeninin .slot ve .offset kısmına atama yapmak ise mümkün değildir.

Yerel Solidity değişkenleri görevler için hazırdır. Örneğin:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract C {
    uint b;
    function f(uint x) public view returns (uint r) {
        assembly {
            // Bu senaryoda depolama slotunun offset'ini değelendirmiyoruz.
            // Çünkü sıfır olduğunu biliyoruz.
            r := mul(x, sload(b.slot))
        }
    }
}

Uyarı

Eğer uint64, address veya bytes16 gibi 256 bitten daha az yer kaplayan bir değişkene erişmeye çalışıyorsanız bu tipin parçası olmayan bitler hakkında bir varsayımda bulunmayın. Özellikle de o bitleri sıfır kabul etmeyin. Her ihtimale karşı, uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } parçasının önemli olduğu yerlerde düzgün bir şekilde bu verileri temizleyin. Signed tipleri temizlemek için signextend kullanabilirsiniz. opcode: assembly { signextend(<num_bytes_of_x_minus_one>, x) }

Solidity 0.6.0’dan beri bir inline assembly değişkeninin ismi inline assembly bloğundaki kullanımını karşılamayabilir. (değişken, kontrat ve fonkisyon kullanımları dahil)

Soldity 0.7.0’dan beri inline assembly bloğunun içinde kullanılan değişken ve fonksiyonlar . içermeyebilir. Fakat . kullanmak inline assembly bloğu dışındaki Solidity değişkenlerine ulaşmak için etkilidir.

Kaçınılacak Şeyler

Inline assembly high-level gözükebilir fakat aslında aşırı derecede low-level’dır. Fonksiyon çağrıları, döngüler, if’ler ve switch’ler basit tekrar yazım kuralları ile çevrilir ve bundan sonra assembler’ın tek yaptığı iş blok sonuna erişildiğinde functional-style opcode’ları tekar ayarlamak, değişken erişimi için stack boyutunu saymak ve assembly içerisindeki değişkenleri için stack slotlarını kaldırmaktır.

Solidity kuralları

Typed Değişkenlerin Değerleri

EVM assembly’nin aksine, Solidity 256 bitten daha küçük tiplere sahiptir (ör: uint24). Verimlilik için çoğu aritmetik işlem bazı tiplerin 256 bitten küçük olabileceğini yok sayar ve higher-order bitler gerekliyse (hafızaya yazılmadan hemen önce ya da herhangi bir karşılaştırma yapılmadan önce) temizlenir. Burası şu yüzden önemlidir: Eğer inline assembly içerisinde böyle bir değişkene erişmek istiyorsanız önce higher-order bitleri kendiniz temizlemeniz gerekebilir.

Hafıza Yönetimi

Solidity Belleği şu şekilde yönetir. Hafızada 0x40 konumunda bir “boş bellek pointer”ı bulunur. Eğer belleğe bir şey atamak isterseniz bu pointer’ın işaret ettiği yerden başlayıp güncelleyin. Bu hafızanın daha önce kullanılmadığına dair herhangi bir kanıt bulunmadığı için tamamen sıfır olduğunu da varsayamazsınız. Belleği boşaltacak ya da rahatlatacak herhangi bir hazır kurulu mekanizma yoktur. Aşağıda belleği yukarıda anlatıldığı şekilde kullanabileceğiniz bir assembly kod parçası bulunuyor:

function allocate(length) -> pos {
  pos := mload(0x40)
  mstore(0x40, add(pos, length))
}

Hafızanın ilk 64 biti kısa dönem hafızası için “geçici alan” olarak kullanılabilir. Boş bellek pointer’ından sonraki 32 bit (yani 0x60 tan başlayan alan) ise kalıcı olarak sıfır olmalıdır ve bu alan boş dinamik bellek array’lerinin temel değeri olarak kullanılır. Bunlar ise demektir ki kullanılabilir hafıza 0x80 den başlar ve bu değer ise boş bellek pointer’ının ilk değeridir. Solidity’deki hafıza array’lerinin tamamı 32 bitin katları olacak şekilde yer kaplar.(Bu kural bytes1[] için de geçerlidir fakat bytes ve string için geçerli değildir.) Çok boyutlu hafıza array’leri ise başka hafıza array’lerine pointer’lardır. Dinamik array’in uzunluğu array’in ilk slotunda saklanır ve diğer slotlara array’in elemanları gelir.

Uyarı

Statik boyutlandırılmış hafıza array’leri herhangi bir uzunluk alanına sahip değildir fakat bu sonradan dinamik ve statik boyutlandırılmış array’ler arasında daha kolay çevrimi sağlamak için eklenmiş olabilir. Yani bu kurala dayanarak ilerlememelisiniz.

Hafıza Güvenliği

Inline assembly kullanmadan; derleyici(compiler), iyi tanımlanmış bir durumda kalmak için her zaman belleğe güvenir. Bu özellikle Yul IR üzerinden yeni kod oluşturma hattı Yul IR ile ilgilidir. Bu kod parçası yerel değişkenleri stack üzerinden belleğe atarak stack-too-deep hatasından kaçınmayı sağlar ve eğer bazı kesin varsayımlara uyuyorsa ekstra bellek optimizasyonları uygulayabilir.

Biz her ne kadar Solidity’nin kendi bellek modeline saygı gösterilmesini önersek de Inline assembly belleği uyumsuz bir biçimde kullanmanızı sağlar. Bu nedenle stack değişkenlerini belleğe taşımak ve diğer bellek optimizasyonları, bir bellek işlemi içeren ya da Solidity değişkenlerini belleğe atayan tüm inline assembly bloklarında varsayılan olarak devredışı haldedir.

Fakat bir assembly bloğuna aşağıdaki şekilde özel olarak ek açıklamalar ekleyerek Solidity’nin bellek modeline uyduğunu belirtebilirsiniz:

assembly ("memory-safe") {
    ...
}

Bellek açısından güvenli bir assembly bloğu sadece aşağıdaki bellek bölümlerine erişebilir: - Sizin tarafınızdan yukarıda anlatıldığı gibi allocate benzeri bir mekanizma kullanarak atanmış bir bellek. - Solidity tarafından atanmış bellek, yani sizin referans verdiğiniz bellek array’inin sınırları içerisinde kalan alan. - Yukarıda bahsedilen 0 ile 64 bellek offset’leri arasında kalan geçici alan. - Assembly bloğunun başındaki boş bellek pointer’ının değerinden sonra konumlanmış geçici bellek, yani boş bellek pointer’ının güncellememiş hali için ayrılan bellek alanı.

Bunlara ek olarak, eğer bir assembly bloğu bellekteki bir Solidity değişkenine atanırsa bu erişimin yukarıda belirtilen bellek sınırları içerisinde olduğundan emin olmalısınız.

Belirtilen işlemler genellikle optimizer ile ilgili olduğu için assembly bloğu hata verse de verilen kısıtlamalar takip edilmeli. Bir örnek olarak aşağıda verilen assembly kod parçası bellek açısından güvenli değil. Sebebi ise returndatasize() fonksiyonunun değeri belirtilen 64 bitlik geçici bellek alanını aşabilir.

assembly {
  returndatacopy(0, 0, returndatasize())
  revert(0, returndatasize())
}

Fakat aşağıdaki kod ise bellek açısından güvenli dir. Çünkü boş bellek pointer’ının gösterdiği yerden sonrası güvenli bir şekilde geçici alan olarak kullanılabilir.

assembly ("memory-safe") {
  let p := mload(0x40)
  returndatacopy(p, 0, returndatasize())
  revert(p, returndatasize())
}

Unutmayın ki eğer bir atama yoksa boş bellek pointer’ını güncellemenize gerek yoktur ama belleği kullanmaya boş bellek pointer’ının verdiği offset’ten başlayabilirsiniz.

Eğer bellek işlemleri sıfır uzunluğunu kullanıyorsa -geçici alana düşmediği sürece- herhangi bir offset’i de kullanabilirsiniz.

assembly ("memory-safe") {
  revert(0, 0)
}

Unutmayın ki inline assembly içerisindeki bellek işlemleri bellek için güvenli olmadığı gibi bellekte referans tipinde olan Solidity değişkenlerine olan atamalar da bellek için güvenli olmayabilir. Aşağıdaki örnek bellek için güvenli değildir:

bytes memory x;
assembly {
  x := 0x40
}
x[0x20] = 0x42;

Belleğe erişim istemeyen işlemlerden oluşan ve bellek üzerindeki Solidity değişkenlerine atama yapmayan inline assembly otomatik olarak bellek için güvenli sayılır ve ekstra olarak belirtilmesine gerek duyulmaz.

Uyarı

Assembly’nin bellek modelini sağladığından emin olmak sizin sorumluluğunuzdadır. Eğer siz bir assembly bloğunu bellek için güvenli olarak tanımlayıp herhangi bir bellek hatası yaparsanız bu kesinlikle, doğru olmayan ya da tanımlanmamış bir davranışa sebep olur. Ve bu hata test yaparak kolay bir şekilde bulunamaz.

Eğer Solidity’nin farklı versiyonları ile uyumlu olacak şekilde bir kütüphane oluşturuyorsanız bir assembly bloğunun bellek için güvenli olduğunu özel bir komut ile belirtebilirsiniz:

/// @solidity memory-safe-assembly
assembly {
    ...
}

Unutmayın ki yorum satırları ile belirtmeyi gelecek bir sürümde kaldıracağız yani eğer geçmiş derleyici(compiler) sürümleri ile uyum konusunda yeterli bilgiye sahip değilseniz dialect string kullanmayı tercih edin.