Solidity ve inline assembly kavramı
Merhabalar, bugün “Blokzincir Mimarisi ve Merkezi Olmayan Uygulamalar” kitabımın 2. baskısında yer alan bir bölüm olan solidity dili inline assembly nasıl yazılır bölümünü dijital olarak paylaşmak istedim. Keyifle okuyunuz.
Solidity dili ile akıllı sözleşme oluştururken bazı koşullarda bir alt katmanda kod yazmaya ihtiyaç duyabiliriz. Bu seviyedeki kodlama makine diline en yakın dil olan assembly dilidir. Assembly dillerinde makine diline (1 ve 0) dönüştürülmeden önce hexadecimal byte dizilerinin her birine karşılık gelen komutların yürütülmesi için mikroişlemci komutları mevcuttur. Inline assembly ile ilgili detaylı açıklamaları aşağıdaki resmi adresten sağlayabilirsiniz.
=> https://solidity.readthedocs.io/en/v0.7.3/assembly.html
Neden bu şekilde bir mekanizmaya ihtiyacımız var?
· Solidity dilindeki fonksiyonlar ile her türlü işlemi gerçekleştiremiyoruz zira bu yetenekler built-in fonksiyonlar şeklinde bize sunulmamış.
· Solidity dilinde karşılıkları olsa bile daha az gas ücreti harcamak için assembly yazmamız gerekir zira direk belleği kullanmak daha az masraflı olmaktadır.
Bazı hesaplamaları ve bellek işlemlerini inline assembly ile yapmak akıllı sözleşmenizin daha az maliyetli olmasına olanak sağlar. Dr. Gavin Wood; Ethereum ve solidity dili yaratıcılarından biri, Ethereum yellow paper içerisinde solidity ve assembly komut dökümlerini aşağıdaki şekilde belirtmiştir. Şimdi bu operandlardan bazılarının üzerinden geçelim.
Buradaki komutlar assembly dili olan Yul dili komutlarıdır. Ethereum sanal makinesi bir yığın (stack) makinesi şeklinde çalışır; ilk atılan komut son çıkar şeklinde çalışır. Akıllı sözleşmenizi ilk yazdığınız zaman Ethereum sanal makinesi her akıllı sözleşme için bir veri uzayı tahsis eder ve global kalıcı verileri bu belli uzunluktaki veri uzayında saklarız. Şimdi Ethereum sanal makinesindeki belleğin durumuna göz atalım.
Yukarıda belleğin yerleşimi görülmektedir. Adres başlıkları (slot) hexadecimal olarak belirtilmiştir ve her 0x10 hexadecimal değer slotu 32 byte veri tutacak şekilde tasarlanmıştır. Yukarıdakı şekilde 0x40 işaretçi değeri görüldüğü üzere boştur ve özel bir anlamı vardır, tasarım yapılırken memory üzerinden işlem yapılmak ve boş alana işaret etmek gerekirse 0x40 adresindeki slota aktarım yapılabilmekte ve boş alan alınabilmektedir.
Belli başlı bazı komutlar ve adres karşılıkları
Yul dili ile inline assembly yazarken
· Kodlarımızı süslü parantez içerisindeki bölümlere yazmalıyız.
assembly {
…
}
· Değişken tanımlamalarını let ile yapmalıyız.
let x := 2
· Storage değişkenleri assembly içerisinde kullanabilmek için “slot” ifadesini kullanmalıyız.
· Memory değişkenler assembly içerisinde direk kullanılabilmektedir.
· Assembly ifadeler arası etkileşim bulunmamaktadır ve namespace alanları (isim uzayları) farklıdır.
Şimdi yazdığımız bir akıllı sözleşme örneğimizi inceleyelim;
=> 3. ve 4. satırlarda 2 tane kalıcı uint storage depolaması yapıyoruz.
=> 6. satırda işlemleri inline yapan ve uint tipinden bir değeri döndüren metod görüyoruz. Bu metod içerisinde assembly ifadesini adım adım inceleyelim;
=> result := mul(sload(c.slot),sload(a.slot)), mul metodu ile çarpma işlemi yapıyoruz ancak bu işlemi yapabilmek için kalıcı bellek alanından değeri getirmek için sload operandını kullanıyoruz. Kalıcı değişkene slot ifadesi ile ulaştığımız için elde ettiğimiz iki değeri çarpıp result değişkenine atıyoruz.
=> sstore(c.slot,result)
Elimizdeki sonucu bellekte yeri belli kalıcı alana tekrar yazıyoruz. Bu alan c için daha önce ayrılmıştı. Artık elimizde c ve a değişkenlerinin çarpımını c değerine atayan bir metodumuz mevcut.
=> 12. satırdaki kodumuz ise carpma ve şarta göre bölme işlemi yapıyor ve sonucu döndürüyor
assembly{
let _mul := mul(a,b) // 2 değeri çarp ve mul değişkenine ata
let _div := 0
if eq(b,5) { // eğer b değişkeni 5 değerine eşitse koşula git
_div := div(a,b) // a değerini b ile böl
}
result := add(_mul,_div) // _mul ve _div değerlerini topla
mstore(0x20,result) // toplama sonucu belleğin 0x20 adresine yaz
return (0x20,32) // ve 0x20 adresinden 32 byte veriyi döndür.
}
Çıktıları şöyle görebiliriz;
10 ve 5 değerlerini blokzincire gönderirsek 52 sonucu görüyor oluruz.
Şimdi farklı bir döngü kurup bir dizi üzerindeki değerleri toplayalım. Bu örnek solidity sitesinden alınmıştır. 3 elemanlı [1,2,3] dizisi için aşağıdaki işlemi inceleyelim ve sonucu ekran görüntüsünde 6 olarak görelim.
Dizilerle ilgili önemli olan, bellek üzerinde yer ayrılırken ilk referans alanı dizinin boyutu, sonraki 32 byte ise dizinin ilk elemanın olduğu bilgisidir.
let len := mload(_data) => dizinin boyutunu gösterilen adresten çekiyoruz
let data := add(_data, 0x20) => dizinin ilk elemanını olduğu referans işaretiçisini buluyoruz.
for
{ let end := add(data, mul(len, 0x20)) } // son elemanın adresini buluyoruz.
lt(data, end) // dizinin son elemanına ulaşılmadıysa devam edilir.
{ data := add(data, 0x20) } // dizinin bir sonraki alanına geçiyoruz.
{
sum := add(sum, mload(data)) // data adresin içeriğini her seferinde topluyoruz.
}
Inline assembly ile yapacağımız son örneğimizde dinamik boyutlu bytes dizisini 32 byte alana sahip bytes32 belirtecine dönüştüreceğiz. Böyle bir dönüşümü neden yaptığımızı sorgulayabilirsiniz; bytes boyutu belli olmayan bir diziyi ifade eder ve uzunluğu 1 byte olabileceği gibi 32 byte veriden de fazla olabilir, 32 byte veriden küçük bir uzunluğu solidity dilinde tanımlı bir tipte tutmak istersek o da bytes32 veri tipi olacaktır.
assembly
{
1 let length := mload(b) // dizi uzunluğu işaretçi ilk adresindedir
2 for { let i := 0 } lt(i, length) { i := add(i, 1) }
{
3 let res := mload(add(add(b,32),i)) // dizi ilk elemanı 32 byte adrestedir sonraki 33
byte
4 mstore(add(0x20,i),res) // veriyi belleğe alıyoruz
}
5 result:= mload(0x20) // sonucu 32 byte olarak döndürüyoruz
}
1 numaralı satırda belirsiz uzunluktaki bytes dizisinin uzunluğunu buluyoruz.
2 numaralı satırda bir döngü kurup dizinin elemanlarına erişmek istiyoruz.
3 numaralı satırda ise dizinin elemanlarına bellekte indislerle ulaşıyoruz.
4 numaralı satıra ulaştığımız zaman dizi verilerini belleğe yüklüyoruz(0x20 boş bellek alanına).
5 numaralı satırda ise bu alandaki verileri döndürüyoruz.
Sonucumuz aşağıdaki görülmektedir.