1. Anasayfa
  2. Solidity

Solidity Eğitimi : Bellek Hakkında Her Şey

Solidity Eğitimi : Bellek Hakkında Her Şey
Bellek, Solidity Eğitimi
0

Solidity Eğitimi : Bellek Hakkında Her Şey

EVM belleğini anlama

EVM belleğinin düzenini, ayrılmış alanlarını, boş bellek işaretçisini, bellekten okuma ve belleğe yazma için bellek referanslarının nasıl kullanılacağını ve bellekle çalışırken geleneksel en iyi uygulamaları öğreneceğiz.

Bu makaleyi anlamlı örneklerle desteklemek için Ethereum Name Service (ENS) sözleşmelerinin kod parçacıklarını kullanacağız. Bu, bu popüler projenin arkasındaki akıllı sözleşmelerin sisteminin nasıl çalıştığını daha iyi anlamamıza yardımcı olacak.

Solidity Eğitimi: Bellek Hakkında Her Şey

  • Giriş
  • EVM Belleği — Genel Bakış
  • Bellek Düzeni
  • Belleğin Temelleri
  • Bellekten Okuma ( MLOAD)
  • Belleğe Yazma ( MSTORE+ MSTORE8)
  • Bellek Boyutunu Bilme ( MSIZE)
  • Boş hafıza işaretçisi
  • memory fonksiyon parametreleri olarak referanslar
  • memory fonksiyon gövdesi içindeki referanslar
  • Bellek Genişletme maliyeti
  • Sözleşme çağrıları arasındaki bellek
  • Sonuç Bağlamı

Veri Konumları Hakkında Her Şey” giriş makalesinde EVM’yi endüstriyel bir fabrika olarak tanımlıyorum. Bir fabrikanın bazı bölümlerinde operatörler tarafından kontrol edilen makineler ve robotlar bulacaksınız.

Bu makineler, işlenemeyen büyük çelik/alüminyum parçalarını daha küçük parçalara ayırır.

Aynı örneği Ethereum için de kullanabiliriz. EVM, 32 byte word üzerinde yığın makinesi olarak çalışır. EVM 32 bayttan büyük verilerle karşılaştığında (string, byte, struct veya diziler gibi karmaşık türler), bu öğeler çok büyük olduğundan bunları yığında işleyemez.

Bu nedenle, EVM’nin bu verileri alması ve başka bir yerde işlemesi gerekir. Bunun için ayrılmış bir yeri var: hafıza. Bu tür değişkenleri belleğe koyarak, EVM bunları birbiri ardına daha küçük parçalar halinde yığına teslim edebilir.

EVM belleği ayrıca abi-encoding, abi-decoding veya keccak256 aracılığıyla hashing işlevleri gibi yerleşik Solidity için karmaşık işlemler için kullanılır. Bu özel durumlar için, belleğin EVM için bir karalama defteri veya beyaz tahta görevi gördüğünü hayal edin.

Bir öğretmen veya bilim adamı, sorunları çözmek için üzerine bir şeyler yazmak için beyaz tahta kullanabilir. Aynısı EVM için de geçerlidir. EVM, bu işlemleri veya hesaplamaları gerçekleştirmek ve nihai değeri döndürmek için belleği bir not defteri olarak kullanır.

  • abi.decode(…) veya keccak256 için, girişlerin kaynağı bellektir.
  • abi.encode(…) için çıktının depolanacağı bellektir.

EVM Belleği — Genel Bakış

EVM belleğinin 4 ana özelliği vardır:

  • cheaper : gaz açısından
  • mutable : üzerine yazılabilir ve değiştirilebilir
  • relative to transactions : işlev çağrılarından veya yapıcıdan ( sözleşme oluşturma) geliyor
  • short term : kalıcı değil ve harici işlev çağrıları arasında silindi.

EVM belleği, bayt adreslenebilir bir alandır . İçindeki tüm baytlar başlangıçta boştur (sıfır olarak tanımlanır). Değişken bir veri alanıdır, yani buradan okuyabilir ve ona yazabilirsiniz. Calldata gibi, bellek de bayt dizinleri tarafından adreslenir, ancak “Hafıza Etkileşimi” bölümünde bellekte bir seferde yalnızca 32 baytlık kelimeleri okuyabileceğinizi göreceğiz.

EVM belleği de uçucudur. Bellekte saklanan değerler, harici aramalar arasında kalıcı değildir.

Bir sözleşme başka bir sözleşme çağırdığında, yeni temizlenmiş ve yeni bir bellek örneği elde edilir .

Solidity Eğitimi

Hafıza her seferinde silinmez ve silinmez. EVM belleğinin her yeni örneği, geçerli sözleşme yürütmesi olan bir yürütme bağlamına özgüdür.

Bu nedenle, EVM belleğinin hem bir mesaj çağrısına hem de çağrılan sözleşmenin yürütme ortamına özgü olduğunu unutmamalısınız. Bu kavramı daha sonra ayrı bir bölümde daha ayrıntılı olarak açıklayacağız.

Bellek Düzeni

Bellek doğrusaldır ve bayt düzeyinde adreslenebilir.

Belleği çok büyük bir bayt dizisi olarak düşünün.

EVM belleği ile etkileşime girdiğinizde, 32 bayt uzunluğundaki “hafıza bloklarını” okur veya bunlara yazarsınız.

Ayrılmış Alanlar

Bellekteki ilk 4 x 32 baytlık sözcükler, farklı amaçlar için ayrılmış alanlardır:

  • İlk 2x word ( ofset 0x00 ve 0x20): hash fonksiyonları için kazıma alanı
  • ofset 0x40 ve 0x50: 3. kelime: boş bellek işaretçisi
  • offset 0x60: kalıcı olarak sıfır olması gerekiyordu ve boş dinamik bellek dizileri için başlangıç değeri olarak kullanıldı
Solidity Eğitimi : Bellek Hakkında Her Şey
Solidity Eğitimi : Bellek Hakkında Her Şey

Boş bellek işaretçisi (0x40 ofsetinde bulunur) EVM belleğinin en önemli parçasıdır. Özellikle montaj/Yul’da dikkatli kullanılmalıdır. Onu ayrı bir bölümde ele alacağız.

Boş bellek işaretçisi (0x40 ofsetinde bulunur) EVM belleğinin en önemli parçasıdır. Özellikle assembly/Yul’da dikkatli kullanılmalıdır. Onu ayrı bir bölümde ele alacağız.

Maksimum bellek sınırı

EVM belleğinin bayt indeksi (ofset olarak adlandırılır) aracılığıyla adreslenebilen doğrusal bir dizi olduğunu gördük. En fazla kaç bayt içerebilir?

Bu dizi ne kadar büyük? EVM belleği ne kadar büyük?

Bunun cevabı geth kaynak kodunda yatmaktadır. Kullanılan dönüşüm türüne bakın.

func opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
	// pop value of the stack
	mStart, val := scope.Stack.pop(), scope.Stack.pop()
	scope.Memory.Set32(mStart.Uint64(), &val)
	return nil, nil
}

Geth istemcisinin bu ekran görüntüsünden mStart.Uint64(), bellek ofsetini bir uint64 değere dönüştürdüğünü görebiliriz. Yani belleğe koyabileceğiniz maksimum veri miktarı, bir uint64 sayının maksimum değeridir.

Belirtilen ofset bundan fazlaysa, geri dönecektir.

Belleğin Temelleri

memory Sözleşme düzeyinde dış işlevleri değil, yalnızca iç işlevi belirtebilirsiniz.

Aşağıdaki veriler ve değerler varsayılan olarak her zaman bellektedir:

  • Karmaşık türlerin işlev bağımsız değişkenleri.
  • Karmaşık türlerin yerel değişkenleri (fonksiyon gövdelerinin içinde).
  • Türlerinden bağımsız olarak işlevlerden döndürülen değerler (bu, dönüş işlem kodu aracılığıyla yapılır).
  • Bir işlev tarafından döndürülen herhangi bir karmaşık değer türü, memory anahtar sözcüğünü belirtmelidir.

Karmaşık türlerin değişkenleri/değerleri ile yapı, diziler, baytlar ve dizeler gibi değişkenlere atıfta bulunuruz.

memory anahtar sözcüğüyle tanımlanan bu değişkenler, işlev çağrısı sona erdiğinde kaybolacaktır. Daha önce “kalmaz” derken bunu kastetmiştik.

Bunun nedeni, memory‘nin Solidity‘ye çalışma zamanında değişken için bir alan yığını oluşturmasını söylemesidir ve bu, işlevin yürütülmesi sırasında bu işlevde gelecekte kullanılmak üzere boyutunu ve yapısını garanti eder.

Bellekle etkileşim — Genel bakış

Solidity belgeleri, EVM belleğinde şunları belirtir:

…okumalar 256 bitlik bir genişlikle sınırlıdır, yazmalar ise 8 bit veya 256 bit genişliğinde olabilir.

Solidity Eğitimi

Ethereum Sarı Kağıda bakarsak, bellekten ( MLOAD ) okumak için bir işlem kodunun ve belleğe yazmak için iki işlem kodunun tanımlandığını görebiliriz: MSTORE ve MSTORE8.

Solidity Eğitimi : Bellek Hakkında Her Şey
Solidity Eğitimi : Bellek Hakkında Her Şey

Hafızadan Okuma

İşlem kodunu kullanarak bellekten okuyabilirsiniz MLOAD.

Sarı Kağıt formülü

MLOAD İşlem kodu spesifikasyonu için sarı kağıt şunları söylüyor.

  • Us[0]= yığındaki en üst öğe.
  • Us'[0]= yığının üstüne konulan sonuç öğesi.
  • Um= belirli bir ofsetten başlayan bellekteki içerik.

Formül Um[Us[0]...Us[0] + 31]]aşağıdaki gibi dümdüz çevrilebilir:

  1. yığındaki son üst öğeyi alın Us[0].
  2. bu değeri bellekte okumak için başlangıç ​​işaretçisi olarak kullanın Um(= offset)
  3. bu bellek işaretçisinden Us[0]( ) sonraki 31 baytı okuyun Us[0] + 31.

Bellekten okuma, bir seferde yalnızca 32 bayt sözcük yapılabilir. mload yani opcode ile aynı anda bellekten yalnızca 32 bayt yapabilirsiniz .

Bu işlem kodları, Solidity satır içi montajında veya bağımsız Yul kodunda kullanılabilir.

Örnek: ENS sözleşmelerinden SHA1 kitaplığı

Ethereum Name Service, ENS
Ethereum Name Service, ENS

ENS sözleşmelerindeki bir örneğe bakalım:

Aşağıdaki kod parçasında mload işlem kodu iki kez kullanılmıştır.

  • önce boş hafıza işaretçisini almak için. Scratch değişkeni daha sonra, sha1 veri karmasının hesaplanıp yazılacağı bellekte bir işaretçi olarak kullanılır.
  • veri değişkeninin uzunluğunu almak için saniye (bayt sayısı).
pragma solidity >=0.8.4;

library SHA1 {
    event Debug(bytes32 x);

    function sha1(bytes memory data) internal pure returns (bytes20 ret) {
        assembly {
            // Güvenli bir konum edinin
            let scratch := mload(0x40)

            // Veri uzunluğunu alın ve verileri ilk baytta işaretleyin
            let len := mload(data)
            data := add(data, 32)

            // Dolgudan sonraki uzunluğu bulun
            let totallen := add(and(add(len, 1), 0xFFFFFFFFFFFFFFC0), 64)
            switch lt(sub(totallen, len), 9)
            case 1 {
                totallen := add(totallen, 64)
            }

            let h := 0x6745230100EFCDAB890098BADCFE001032547600C3D2E1F0

            function readword(ptr, off, count) -> result {
                result := 0
                if lt(off, count) {
                    result := mload(add(ptr, off))
                    count := sub(count, off)
                    if lt(count, 32) {
                        let mask := not(sub(exp(256, sub(32, count)), 1))
                        result := and(result, mask)
                    }
                }
            }

Belleğe Yazma

Aşağıdaki iki işlem kodundan birini kullanarak belleğe yazabilirsiniz:

  • MSTORE → belleğe bir kelime (32 bayt) yazın.
  • MSTORE8 → belleğe tek bir bayt yaz

geth istemcisindeki EVM örneğinin girdi olarak yığından argümanları nasıl aldığını MSTORE detayı ile bilahare aktaracağım.

Solidity Programlama Dili ile…

Solidity‘de, memory anahtar kelimeyle bir değişken başlattığınızda ve bir değer atadığınızda (literal byte/string veya bir fonksiyonun dönüş değeri), sistem EVM ile mstore talimat gerçekleştirir.

function _claim(bytes memory name, bytes memory proof)
        internal
        returns (
            bytes32 rootNode,
            bytes32 labelHash,
            address addr
        )
    {
        // İlk etiketi alın
        uint256 labelLen = name.readUint8(0);
        labelHash = name.keccak(1, labelLen);

        // Ebeveyn adı, genel son ek listesinde olmalıdır.
        bytes memory parentName = name.substring(
            labelLen + 1,
            name.length - labelLen - 1
        );
        require(
            suffixes.isPublicSuffix(parentName),
            "Parent name must be a public suffix"
        );

        // Ebeveyn adının etkinleştirildiğinden emin olun
        rootNode = enableNode(parentName, 0);

        (addr, ) = DNSClaimChecker.getOwnerAddress(oracle, name, proof);

        emit Claim(
            keccak256(abi.encodePacked(rootNode, labelHash)),
            addr,
            name
        );
    }

    function enableNode(bytes memory domain, uint256 offset)
        internal
        returns (bytes32 node)
    {
        uint256 len = domain.readUint8(offset);
        if (len == 0) {
            return bytes32(0);
        }

Assembly ile…

İşlem mstore kodu, satır içi montajda kullanılabilir. İki argümanı kabul eder:

  • yazılacak bellekteki ofset.
  • belleğe yazılacak veriler

Aynı ENS sözleşmesinde assembly ile nasıl mstore kullanıldığını görün SHA1.sol.

            for {
                let i := 0
            } lt(i, totallen) {
                i := add(i, 64)
            } {
                mstore(scratch, readword(data, i, len))
                mstore(add(scratch, 32), readword(data, add(i, 32), len))

                // Son baytı yüklediysek, sonlandırıcı baytı saklayın
                switch lt(sub(len, i), 64)
                case 1 {
                    mstore8(add(scratch, sub(len, i)), 0x80)
                }

                // Bu son blok ise, uzunluğu kaydedin
                switch eq(i, sub(totallen, 64))
                case 1 {
                    mstore(
                        add(scratch, 32),
                        or(mload(add(scratch, 32)), mul(len, 8))
                    )
                }

Bellek Boyutunu Bilmek

İlk tahminde, EVM işlem kodu MSIZE, adından da anlaşılacağı gibi, bellekte ne kadar veri depolandığını döndürecek gibi görünüyor. Veya başka bir deyişle, bellekte şu anda kaç bayt yazıldığı.

İşlem MSIZE kodu biraz daha karmaşıktır. Solidity derleyicisinin C++ kaynak kodu, onu anlamak için daha fazla bilgi sağlar.

	case Push:
	case PushTag:
	case PushSub:
	case PushSubSize:
	case PushProgramSize:
	case PushData:
	case PushLibraryAddress:
	case PushImmutable:
		return false;
	case evmasm::Operation:
	{
		if (isSwapInstruction(_item) || isDupInstruction(_item))
			return false;
		if (_item.instruction() == Instruction::GAS || _item.instruction() == Instruction::PC)
			return true; // GAS ve PC, belirli bir işlem kodu sırasını varsayar
		if (_item.instruction() == Instruction::MSIZE)
			return true; // msize zaten bellek erişimiyle değiştirilmiş, bundan kaçının
		InstructionInfo info = instructionInfo(_item.instruction());
		if (_item.instruction() == Instruction::SSTORE)
			return false;
		if (_item.instruction() == Instruction::MSTORE)
			return false;
		if (!_msizeImportant && (
			_item.instruction() == Instruction::MLOAD ||
			_item.instruction() == Instruction::KECCAK256
		))
			return false;
		// @todo: Şu an için aşağıdaki bellek talimatlarını işlemiyoruz:
                // calldatacopy, codecopy, extcodecopy, mstore8,
                // msize (msize'nin bellek okuma erişimine de bağlı olduğunu unutmayın)

		// ikinci gereklilik uygulandıktan sonra kaldırılacaktır
		return info.sideEffects || info.args > 2;
	}
	}
}

İşlem MSIZE kodu, geçerli yürütme ortamında bellekte erişilen en yüksek bayt ofsetini döndürür. Boyut her zaman kelimenin katı olacaktır (32 bayt).

Ancak Solidity‘de “bellekte kaç bayt depolanır” ile “bellekte erişilen en büyük indeks/ofset” arasındaki fark nedir?

Solidity‘nin kendisini kullanarak pratik bir örnekle açıklayacağız!

pragma solidity ^0.8.4;

contract TestingMsize {

    function test() 
        public 
        pure 
        returns (
            uint256 freeMemBefore, 
            uint256 freeMemAfter, 
            uint256 memorySize
        ) 
    {
        // yeni bellek ayırmadan önce
        assembly {
            freeMemBefore := mload(0x40)
        }

        bytes memory data = hex"masamasamasamasamasamasamasamasamasamasamasamasamasamasamasamasa";

        // yeni bellek ayırdıktan sonra
        assembly {
            // freeMemAfter = freeMemBefore + 32 bytes for length of data + data value (32 bytes long)
            // = 128 (0x80) + 32 (0x20) + 32 (0x20) = 0xc0
            freeMemAfter := mload(0x40)

            // şimdi yeni boş bellek işaretçisinden daha fazla bellekte bir şeye erişmeye çalışıyoruz
            let whatIsInThere := mload(freeMemAfter)

            // şimdi msize 224 döndürecek.
            memorySize := msize()
        }
    }

}

Burada ne oluyor?

  1. Adım : freeMemBefore önce boş bellek işaretçisini döndürün:0x80 (= 128)
  2. Adım data daha sonra belleğe yazıyoruz (64 bayt). Boş bellek işaretçisi güncellenir. ( freeMemAfter) olmak 0xc0 (= 192).

Not: Yukarıdaki örnekte, boş bellek işaretçisi yalnızca biz assembly bloğunun dışında olduğumuz için otomatik olarak güncellenir. mstore derlemede belleğe, gibi belleğe yazan benzer işlem kodları aracılığıyla veya aracılığıyla yazarsanız calldatacopy, boş bellek işaretçisi otomatik olarak güncellenmez. Bunu manuel olarak kendiniz yapmaktan siz sorumlusunuz.

Solidity belgelerinde belirtilen kuralları hatırlayın: “Satır içi assembly oldukça yüksek seviyeli bir görünüme sahip olabilir, ancak son derece düşük seviyelidir”.

Bu noktada teknik olarak toplamda bellekte ayrılmış 192 bayt bulunmaktadır.

32 bayt
x 4 (bellekteki ilk 4 ayrılmış boşluk)
---------------------
= 128
+ 64 bayt ('data' değişkeni)
---- -----------------
= 192 (toplam)

Şimdi 28. satıra dikkat edin. Hafızada ofsette okumaya çalışıyoruz. 0x0c (192)

3. Adım msize (31. satır) yaptığımızda 224 sayısını elde ederiz (0xe0). Az önce ne oldu? Toplamda bellekte depolanan/tahsis edilen yalnızca 192 bayt vardır. Bu 224 nereden geliyor?

224 = 192 + 32. Yani döndürülen değer msize, bellekte ( ) + 32 saklanan toplam bayt sayısıdır. Az önce bir bellek genişlemesini tetikledik ve tanık olduk. Bellek her zaman bir seferde 32 bayt kelimeyi genişletir.

msize track, mevcut yürütmede şimdiye kadar erişilen en yüksek ofsettir. Daha büyük bir ofset için ilk yazma veya okuma, bir bellek genişlemesini tetikleyecektir.

Boş hafıza işaretçisi

OpenZeppelin, “Akıllı Sözleşmenin Yapısını Bozma” adlı popüler makale dizisinde, her akıllı sözleşmenin ilk 5 baytının arkasındaki işlem kodlarının anlamını ortaya koyuyor.

  • Deconstructing a Solidity Contract —Part I: Introduction
  • Deconstructing a Solidity Contract — Part II: Creation vs. Runtime
  • Deconstructing a Solidity Contract — Part III: The Function Selector
  • Deconstructing a Solidity Contract — Part IV: Function Wrappers
  • Deconstructing a Solidity Contract — Part V: Function Bodies
  • Deconstructing a Solidity Contract — Part VI: The Metadata Hash
0x6080604052...
Boş bellek işaretçisi EVM bayt kodu yapısı.
Boş bellek işaretçisi
EVM bayt kodu yapısı.

Özetle, bu işlem kodları dizisi sayıyı 0x80 (ondalık 128) bellekte 0x40 (ondalık 64) konumda saklar. Ne için?

“Bellek Düzeni” bölümünde açıklandığı gibi, bellekteki ilk 4 kelime belirli amaçlar için ayrılmıştır. Konumda bellekte bulunan 3. kelimeye boş bellek işaretçisi 0x40 denir .

Open Zeppelin, boş bellek işaretçisini “bellekte kullanılmayan ilk kelimeye bir referans” olarak tanımlar. Bellekte nerede (hangi ofset) veri yazmak için boş alan olduğunu bilmeyi sağlar. Bu, bellekte zaten mevcut olan verilerin geçersiz kılınmasını önlemek içindir.

Boş bellek işaretçisi, EVM’nin bilinmesi gereken en önemli ve anahtar şeylerinden biridir.

Solidity’de boş bellek işaretçisi

Solidity‘de, boş bellek işaretçisi alınır + bytes memory myVariable gibi kod parçacıkları yapılırken otomatik olarak güncellenir.

function test() public {

	String memory test = “Solidity Egitimi”;
 }

Bunlar, Solidity derleyicisi tarafından oluşturulan işlem kodlarıdır. Bizi ilgilendiren, 056 numaralı talimattan 065 numaralı talimata kadar, boş bellek işaretçisinin nasıl getirildiği ve güncellendiğidir.

bir dize belleği yazmak için temel bir işlem kodu dizisi.
bir dize belleği yazmak için temel bir işlem kodu dizisi.

Solidity‘de belleğe bir dize veya bazı veriler yazıldığında, EVM her zaman aşağıdaki ilk iki adımı gerçekleştirir:

Adım 1: Boş bellek işaretçisini alın.

EVM önce boş bellek işaretçisini 0x40 bellek konumundan yükler. mload tarafından döndürülen değer 0x80‘dir. Boş bellek işaretçimiz, bellekte yazılacak boş alanın olduğu ilk yerin 0x80 ofsetinde olduğunu söyler. Sonunda yığınımızın üstünde olan şey budur.

Adım 2: Yeni boş bellek işaretçisi ile bellek + güncelleme ayırın.

EVM şimdi bu konumu dizi testi için bellekte saklayacaktır. Boş bellek işaretçisi tarafından döndürülen değeri yığında yerel olarak tutar.

Ancak Solidity derleyicisi akıllı ve güvenlidir! Ayırma işleminden sonra ve belleğe herhangi bir değer yazmadan önce, her zaman boş bellek işaretçisini günceller. Bu, bellekteki bir sonraki boş alana işaret etmektir.

ABI spesifikasyonuna göre, bir dizi iki bölümden oluşur: uzunluk + dizinin kendisi. Sonraki adım, boş bellek işaretçisini güncellemektir. EVM’nin burada söylediği şey “Hafızaya 2 x 32 byte word yazacağım. Böylece yeni boş bellek işaretçisi, mevcut olandan 64 bayt daha uzakta olacak”.

Aşağıdaki opcode’ların yaptığı şey basittir.

  • boş bellek işaretçisinin mevcut değerini çoğaltın : 0x80
  • buna 0x40 ekleyin ( 64 bayt için ondalık olarak 64)
  • 0x40 ‘ı(boş bellek işaretçisinin konumu) yığının üzerine itin
  • MSTORE aracılığıyla boş bellek işaretçisini yeni değerle güncelleyin.

Assembly’de boş bellek işaretçisi

Satır içi, Assembly, boş bellek işaretçisi dikkatle ele alınmalıdır!

Sadece manuel olarak getirilmek zorunda değil, aynı zamanda manuel olarak güncellenmesi gerekiyor!

Bu nedenle, derlemede belleği işlerken dikkatli olmalısınız. Her zaman ilk olarak boş belleği derlemede getirdiğinizden ve bellekte zaten bir içeriği olan bir şeyin üzerine yazmak istemiyorsanız, boş bellek işaretçisinin gösterdiği yere yazdığınızdan emin olmalısınız.

Belleğe yazdıktan sonra, boş bellek işaretçisini yeni bir boş bellek ofseti ile güncellediğinizden emin olmalısınız.

Sonuç olarak, boş hafıza işaretçisi söz konusu olduğunda, daima OpenZeppelin önerisini hatırlayın:

Assembly düzeyinde bellekle çalışırken çok dikkatli olmalısınız. Aksi takdirde, ayrılmış bir alanın üzerine yazabilirsiniz.

UYARI: İlk olarak boş bellek işaretçisinin işaret ettiği bellek konumunda gerçekte ne saklandığını kontrol etmeden önce boş bellek işaretçisine yazmak iyi bir uygulama olmayabilir.

Solidity Eğitimi

Baytları işlemek için kullanılan bu popüler Solidity kitaplığına bakalım. Her fonksiyonun ilk montaj koduna dikkatlice bakarsanız, ilk işin boş hafıza işaretçisini yüklemek olduğunu göreceksiniz.

İşlevin sonunda tempBytes döndürülür. Düşük seviyede, bu “tempBytes tarafından gösterilen bellek ofsetinde bellekte mevcut olanı döndür” ile çevrilebilir.

Solidity tempBytes
Solidity tempBytes

İşlev parametreleri olarak bellek referansları

Bu ifadeyi, bir fonksiyona dinamik veya karmaşık tipte bir argüman iletmemiz gerektiğinde, Solidity‘de her zaman kullanırız.

Örneğin, ENS sözleşmesinde, DNSRegistar.sol‘den gelen claim(...) işlevi iki bağımsız değişken alır: name ve proof, her ikisi de memory referanslarıdır.

Ancak bir işlev parametresi olarak bir bellek referansı EVM için ne anlama gelir? Temel bir Solidity örneği kullanalım.

function test(string memory input) public {
    // ...
}

Bir fonksiyona parametre olarak bir hafıza referansı iletildiğinde, fonksiyonun EVM bayt kodu sırayla 4 ana adımı gerçekleştirir:

  • Dize uzaklığını çağrı verilerinden yığına yükleyin: dizenin çağrı verilerinin içinde nerede başladığını bilmek için.
  • Dize uzunluğunu yığına yükleyin: çağrı verilerinden ne kadar veri kopyalanacağını bilmek için kullanılacaktır.
  • dizeyi çağrı verisinden belleğe taşımak için bir miktar bellek alanı ayırın: bu, “boş bellek işaretçisi”nde açıklananla aynıdır.
  • opcode calldatacopy kullanarak dizeyi calldata‘dan belleğe aktarın.

İşlev gövdesi içindeki bellek referansları

function test() public {
    uint256[] memory data;
}

Sorulması gereken soru , değişkenin ne data içerdiğidir?

Boş bir uint256 numarası dizisi” yanıtını vermek cazip gelebilir. Ancak sözdizimine aldanmayın veya yanılmayın. Bu Solidity, Javascript veya TypeScript değil!

Typescript’te, bir uint256[] türe sahip bir değişkeni başlatmadan bildirmek, değişkenin ilk etapta boş bir dizi tutmasına neden olur.

Ancak, anahtar kelime memory burada her şeyi değiştirir!

Beynimizi tazeleyelim, “Veri Konumları Hakkında Her Şey” başlıklı giriş makalemizde, bu değişkenleri anahtar kelime ile açıklıyoruz storage veya memory referans tipi değişkenler calldata olarak adlandırıyoruz .

Yani bir Solidity fonksiyonu içinde anahtar kelime ile bir değişken gördüğünüzde memory, hafızadaki bir lokasyona referansla uğraşıyorsunuz demektir.

Bu nedenle, yukarıdaki değişken data bir dizi tutmaz, bunun yerine bellekteki bir konuma bir işaretçi tutar. Solidity belgeleri bunu çok iyi açıklıyor:

Belleğe atıfta bulunan yerel değişkenler, değerin kendisini değil, bellekteki değişkenin adresini değerlendirir.

Solidity Eğitimi

Ve Solidity açıklamada daha da ileri gidiyor!

Bu tür değişkenlere de atanabilir, ancak bir atamanın verileri değil yalnızca işaretçiyi değiştireceğini unutmayın.

Solidity Eğitimi

Daha iyi anlamak için başka bir örneğe bakalım.

function test() public pure returns (bytes memory) {
    bytes memory data;
    bytes memory greetings = hex"masamasa";
    data = greetings;
    data[0] = 0x00;
    data[1] = 0x00;
    return greetings;
}

Burada greetings değişkeninin güvenli olduğu ve bu fonksiyonun 0xmasamasa döndüreceği düşünülebilir. Ama burada yanlış varsayım, bu fonksiyonu çalıştırırsanız sonuç beklediğiniz gibi olmayacaktır :)

Gerçekte olan şey, ve değişkenleri tarafından adlandırılan belleğe iki işaretçi data oluşturmamızdır greetings.

Bunu yaptığımızda data = greetings, değeri cafecafedeğişkene atadığımızı düşünüyoruz data. Ama burada hiçbir şey tayin etmiyoruz ! EVM’ye şu talimatı veriyoruz:

Gerçekteolan şey, data ve greetings değişkenleriyle adlandırılan belleğe iki işaretçi oluşturmamızdır.

data = greetings yaptığımızda, masamasa değerini data değişkenine atadığımızı düşünüyoruz. Ama burada hiçbir şey tayin etmiyoruz! EVM’ye şu talimatı veriyoruz:

data değişkeni, bellekte greetings değişkeninin işaret ettiği aynı yeri göstermenizi emrediyorum!”

Solidity Eğitimi

Bellekte yeni öğeler ayırma

Değişkenler için bellekte bir miktar yer ayırabileceğimizi ve değişkene bir değer atayarak doğrudan içine yazabileceğimizi önceki bölümde görmüştük.

Ayrıca bellekte bir miktar yer ayırabiliriz ancak new anahtar sözcüğünü de kullanarak hemen belleğe yazamayız.

Bu, esas olarak, işlevler içindeki diziler gibi karmaşık türleri başlatırken olur.

Yeni anahtar kelime ile diziler oluşturulduğunda, dizi uzunluğu parantez içinde belirtilmelidir. Bir işlev gövdesi içindeki bellekte yalnızca sabit boyutlu dizilere izin verilir.

uint[] memory data = new uint[](3);

Yapılar için new anahtar sözcüğü gerekli değildir.

Bir depolama referans değişkeninden kopyalama

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Playground {
    bytes storageData = hex"C0C0A0C0DE";
    
    function test() public {
        bytes memory data = storageData;
    }
}

Bu senaryoda, bir depolama referansını bir bellek referansına kopyalıyoruz. Burada iki şey olur:

  • yeni bellek tahsis edilir ve değişken veriler bellekte yeni bir konuma işaret eder.
  • Onaltılık değer 0xM0M0A0M0DE, depodan yüklenir ve verilerin işaret ettiği bellek konumunda belleğe kopyalanır.

Bellek Genişletme Maliyeti

Solidity belgeleri şunları belirtir:

Daha önce dokunulmamış bir bellek kelimesine (yani bir kelime içindeki herhangi bir ofset) erişirken (okurken veya yazarken) hafıza bir kelime (256-bit) ile genişletilir.

Genişleme zamanında gaz bedeli ödenmelidir. Bellek büyüdükçe daha maliyetli olur (kuadratik olarak ölçeklenir).

Aslında, hafızaya daha önce kullanılmamış (içinde bir miktar veri bulunan) veya erişilmemiş (mload aracılığıyla) yeni bir kelime yazdığımızda hafızanın “genişlediği” söylenir.

Bellek genişletme neden önemlidir? Daha büyük bellek büyüdüğü için, onunla her etkileşimde bulunduğunuzda daha fazla gaz tüketir.

mstore (veya mstore8) aracılığıyla belleğe yazdığınızda, bu iki işlem kodu için bir miktar gaz kullanılır. Ancak belleğe yazmanın gaz maliyeti yalnızca belleğe ne kadar veri yazdığınıza bağlı değildir. Aynı zamanda, EVM gölge geliştiricileri topluluğunda “bellek genişletme maliyeti” olarak bilinen gerçek bellek boyutuna da bağlıdır.

Belleğe yazma maliyetine ek olarak, belleğin ne kadar genişleyeceğiyle ilgili her zaman ek bir maliyet vardır.

Bellek genişletme maliyeti şu şekilde artar:

  • ilk 724 bayt için doğrusal olarak.
  • sonrasında Kuadratik olarak …

Bellekte mload işlem kodu aracılığıyla daha yüksek ofsetlere erişildiğinde, basit bellek okuma işlemleriyle bellek genişletme maliyeti de artar.

Sözleşme çağrıları arasındaki bellek

EVM Memory ve akıllı sözleşmeler konusunda bilinmesi gereken önemli bir kavram var. Solidity belgeleri bunu çok iyi ifade ediyor:

… bir sözleşme, her mesaj çağrısı için yeni temizlenmiş bir örnek (bellek) alır.

Solidity Eğitimi

Bu, EVM Belleğinin ana özelliklerinden birini anlamamıza yardımcı olur: harici aramalar arasında net bir bellek örneği elde edilir.

Aslında, EVM belleğinin bir örneği, her sözleşmeye ve mevcut yürütme bağlamına özgüdür. Bu, her yeni sözleşme etkileşiminde yeni temizlenmiş ve boş bir hafızanın elde edildiği anlamına gelir.

Her yeni harici çağrıda net bir hafıza örneğinin nasıl elde edildiğini pratikte inceleyelim. Bu iki sözleşmeyi örnek olarak kullanacağız:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Source {
    Target target;

    constructor(Target _target) {
        target = _target;
    }

    function callTarget() public {
        target.doSomething();
    }
}

contract Target {
    function doSomething() public {
        // ne istersen yap
    }
}

Bu iki temel sözleşmeyi kullanarak, bir Target sözleşmeyle etkileşim kurmak için Source sözleşmesini kullanabiliriz. Bunları Remix‘te dağıtalım ve hatalarını ayıklayalım.

  • Remix IDE’yi açın, yeni bir dosya oluşturun ve yukarıdaki Solidity kodunu kopyalayın.
  • Dosyayı optimize edici etkinleştirilmeden veya herhangi bir sayıda çalıştırma olmadan derleyin.
  • önce Target sözleşmesini dağıtın.
  • İkinci olarak Source sözleşmesini dağıtın, daha önce bir yapıcı argümanı olarak dağıtılan Target sözleşmesinin adresini verin.
  • Source sözleşmesinde callTarget() işlevini çalıştırın
  • Konsolda, işlemin her işlem kodunun hatalarını ayıklamak için “Hata Ayıkla”yı tıklayın.

Hata ayıklarken ve her bir işlem kodunu incelerken, EVM belleğinin çeşitli ofsetlerde verilerle dolduğunu görmelisiniz. Bunlardan biri özellikle 0x80 ofsetinde 0x826232037360000000000000000… değerini gösterir. Bu, Hedef sözleşmedeki doSomething() işlevinin işlev seçicisidir.

Yürütme bağlamının üzerindeki ekran görüntüsünde görebiliriz. Hata ayıklayıcı, dış çağrı target.doSomething() olan kod satırı nb 12’yi vurgulamıştır.

Şimdi bir sonraki adıma dikkat edin! Hata ayıklamak için bir sonraki işlem koduna atlamak için mavi ok düğmesine tıklarsanız, sihir gibi, bellek temizlenir ve boşalır!

Ne oldu?

CALL işlem kodu, EVM’nin yürütme ortamını değiştirmesini sağladı. Şimdi EVM’yi yeni bir yürütme bağlamında çalıştırıyoruz: Hedef sözleşmesinin bağlamı. Yukarıdan da görebileceğiniz gibi, doSomething() işlevi şimdi vurgulanmıştır ve bu yeni yürütme bağlamı geçişine ilişkin ek bir ipucu sağlar.

Kısa bir açıklama olarak, EVM, doSomething() fonksiyon seçicisini (0x82623203736‘dır) yığında iterek çağrı verisi baytını üretecek ve çağrı verilerini hazırlamak için sola kaydıracak, böylece çağrı verilerinde işlev seçici olarak bu 4 bayt olsun.

Gönderilecek arama verileri yükü daha sonra boş bellek işaretçisi tarafından alınan konumda bellekte saklanır.

Son olarak, CALL işlem kodu, başlangıçta sözleşme deposundan alınan (talimat numarası 057’de) harici sözleşme adresini arayacak ve çağrı verilerini bellekten (önceden yazıldığı yer) getirerek birlikte gönderecektir.

Sonuç Bağlamı : Solidity Eğitimi, Bellek Hakkında Her Şey

EVM’deki bellek, öğrenilmesi gereken önemli bir alandır. EVM’nin standart call , staticcall ve delegatecall gibi mesaj çağrıları gerçekleştirmesini sağlar. Mesaj aramalarıyla birlikte gönderilen arama verileri ve yük, depolanır ve bellekten alınır.

Bu nedenle, EVM belleği, akıllı sözleşmelerde esnek dahili işlevler ve alt rutinler oluşturmayı sağlayarak daha iyi bir birleştirilebilirlik sağlar. Ayrıca, bellek olarak tanımlanan parametreler, sözleşmelerin hem EOA’lardan hem de harici sözleşme çağrılarından (çağrı verilerinden belleğe yük yükleme) çeşitli kaynaklardan çağrı ve argüman almasına olanak tanır, aynı zamanda doğrudan dahili işlevlerden girdiler oluşturmayı sağlar.

Son olarak, düşük seviyeli montajda kullanıldığında bellek dikkatle ele alınmalıdır. Bu, zaten bazı veriler içeren bazı ayrılmış bellek alanlarını geçersiz kılmayacağınızdan emin olmak içindir. Bu nedenle Solidity bellek yönetimine saygı duymak sizin sorumluluğunuzdadır.

Solidity dili ayrıca satır içi derlemeyi daha güvenli kullanmak ve Solidity bellek modeline uymak için “memory safe” anahtar sözcüğünü sağlar.

Blockchain ​​Developer Olmak
Blockchain ​​Developer Olmak

Bu makale “Solidity Programlama Dilinde Bellek Hakkında Her Şey” hakkında oluşturulan içeriklerden derlenmiştir.

Akıllı sözleşme geliştirme yolculuğunuz hakkında daha iyi rehberlik almak için Solidity nedir? Ethereum Akıllı Sözleşmelerinin Dili Rehberi içeriğimize göz atın. Dilerseniz Yeni Başlayanlar için Solidity – Akıllı Sözleşme Geliştirme Hızlandırılmış Kursuna katılın.

Çalışmaya nereden başlayacağım diyenler için Blockchain ​​Developer Olmak İçin Yol Haritası içeriğine de muhakkak bakın.

Bu makaleyi okuduğunuz için teşekkürler! Bana destek olmak isterseniz;

Beni TwitterLinkedin ve YouTube‘da takip edin.

Kısa bir yorum bırakmayı UNUTMAYIN!

Hasan YILDIZ, Girişimci. Doktora Öğrencisi. Yazmayan YAZILIMCI. Veri Şeysi. Eğitmen...

Yazarın Profili
İlginizi Çekebilir

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir