結構&拷貝與引用

開始之前,我們約定數據塊也叫插槽,也就是storage。

storage是永久存儲在區塊鏈上的地方。如果你想操作storage中的數據,你可以將它復制到內存中。然后,所有內存代碼都在堆棧上執行。Stack 的最大深度為 1024 個元素,支持 256 位的字長。

結構

當定義局部變量時,它存儲在內存中,然后壓入堆棧以執行。

1024棧深

簡介

  • EVM不是寄存器機而是堆棧機,所以所有的計算都在稱為堆棧的數據區域上進行。
  • 在函數調用過程中
  • 調用合約內部的函數不會增加層數,每一個外部調用(call, callcode, delegatecall, staticcall)都有自己的stack,用來存儲參數、返回值和局部變量等。即如果一個調用A擁有自己的stack_A,如果再進行外部調用B,則會新起一個stack_B
  • 每一個局部變量都會占據一級,比如局部變量是bytes1,并不會將32個bytes1拼湊在一起占用一個slot而占據一級
  • 合約無法使用與查看call stack,合約只知道mg.sender和tx.origin,合約無法知道中間是否還有其他合約調用。但是我們在remix的debug中可以看到call調用其他合約的堆棧和內存,這是remix自己搞的功能,它可以訪問所有的內容,然而EVM本身是沒有這個功能的。想要看到中間調用了哪些外部合約,要進行contract trace。
  • 調用鏈越深,你需要的插槽就越多,如果你走得太深,你最終會用超1024個插槽,然后報錯。通常 1024 個就足夠了,除非出現無限遞歸或者循環過多
  • stack 也叫execution stack,它會執行一系列操作碼和運算,其數據來自memory
  • 假如合約A調用自身的方法b(),stack 不會增加級數。用戶B調用b(),會增加。合約A調用其他合約的函數,會增加。
  • 用來存放變量、引用、方法或是方法返回值,用來存儲函數內存中的數據。
  • 它不做任何運算,只保存臨時數據
  • memory
  • stack

設計原因

  • 具有固定大小使得 EVM 的整體模型更加簡單且易于實現
  • 如果它非常大,那么執行合約會更昂貴(即需要更多內存)。1024 是一個非常保守的值,以盡可能安全
  • EVM 的設計方式往往會使更大的堆棧變得無用。EVM 只能訪問堆棧中前16個slot。因此,即使您有一個 4096 slot的的堆棧,也只能夠訪問前16個,除非你不斷pop,才可以訪問到下面更深層次的內容

訪問限制

對stack的訪問僅限于頂端:

  • 可以將最頂端的前 16 個元素之一復制到堆棧的頂部:
  • 操作碼【PUSH n】,將第n個slot的內容放入堆棧中,n從 1 到 16
  • 操作碼【DUP n】,復制第n 個堆棧項,n從 1 到16
  • 可以將最頂端的元素與前 16 個元素之一交換
  • 操作碼【SWAP n】,交換第 1 個和第n 個堆棧項,其中 n從 1 到 16
  • 其他的操作就是正常的stack操作:取棧頂的元素,計算,壓回棧頂
  • 正常的pop和push操作

注意:只可以訪問前16個元素:狀態變量可以無限個,但是局部變量最多16個,局部變量存儲在堆棧中,下面是一個例子:

contract stackExample {

    function test() public{
        bytes1 a1 = "0";
        bytes1 a2 = "0";
        bytes1 a3 = "0";
        bytes1 a4 = "0";
        bytes1 a5 = "0";
        bytes1 a6 = "0";
        bytes1 a7 = "0";
        bytes1 a8 = "0";
        bytes1 a9 = "0";
        bytes1 a10 = "0";
        bytes1 a11 = "0";
        bytes1 a12 = "0";
        bytes1 a13 = "0";
        bytes1 a14 = "0";
        bytes1 a15 = "0";
        bytes1 a16 = "0";
        bytes1 a17 = "0";
    }
}

報錯內容:

報錯內容如下:

from solidity:
aaa.sol:6:9: CompilerError: Stack too deep, try removing local variables.
        uint256 a1 = 0;
        ^--------^
【去掉任何一個變量則不報錯,因為局部變量最多16個】

2023/24/27更新

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
contract stackExample {
    function test() public returns (uint256){
        uint256 a1 = 0;
        uint256 a2 = 0;
        uint256 a3 = 0;
        uint256 a4 = 0;
        uint256 a5 = 0;
        uint256 a6 = 0;
        uint256 a7 = 0;
        uint256 a8 = 0;
        uint256 a9 = 0;
        uint256 a10 = 0;
        uint256 a11 = 0;
        uint256 a12 = 0;
        uint256 a13 = 0;
        uint256 a14 = 0;
        uint256 a15 = 0;
        uint256 a16 = 0;
        uint256 a17 = 0;
        uint256 a18 = 0;
        return a18;
    }
}
//上述合約無法通過編譯
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract stackExample {
    function test() public returns (uint256){
        uint256 a1 = 0;
        uint256 a2 = 0;
        uint256 a3 = 0;
        uint256 a4 = 0;
        uint256 a5 = 0;
        uint256 a6 = 0;
        uint256 a7 = 0;
        uint256 a8 = 0;
        {uint256 a9 = 0;
        uint256 a10 = 0;
        uint256 a11 = 0;
        uint256 a12 = 0;
        uint256 a13 = 0;
        uint256 a14 = 0;
        uint256 a15 = 0;}
        uint256 a16 = 0;
        uint256 a17 = 0;
        uint256 a18 = 0;
        return a18;
    }
}
//上述合約可以通過編譯
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract stackExample {
    function test() public{
        uint256 a1 = 0;
        uint256 a2 = 0;
        uint256 a3 = 0;
        uint256 a4 = 0;
        uint256 a5 = 0;
        uint256 a6 = 0;
        uint256 a7 = 0;
        uint256 a8 = 0;
        uint256 a9 = 0;
        uint256 a10 = 0;
        uint256 a11 = 0;
        uint256 a12 = 0;
        uint256 a13 = 0;
        uint256 a14 = 0;
        uint256 a15 = 0;
        uint256 a16 = 0;
        uint256 a17 = 0;
        uint256 a18 = 0;
    }
}
//上述合約可以通過編譯

拷貝與引用

特點

  • 數據類型
  • 值類型
  • 引用類型
  • 成員變量storage
  • 局部變量
  • 局部變量的location
  • memory
  • calldata
  • storage:指向storage
  • 消息調用的有效載荷叫做calldata,和location的calldata不是同一個意思!
  • 消息調用message的calldata,即有效載荷的部分。message不能被修改,因此location的calldata是只讀的。calldata的數據塊不能被值拷貝,但是可以進行引用拷貝。也就是說,calldata這個area的數據只能引用message中的數據,或者兩個calldata之間相互引用。
  • 任何一個成員變量永遠都只會指向屬于自己的數據塊(插槽),是一對一的關系,不存在一對多、多對一、多對多的情況,如圖:

圖示與算法

代碼演示

pragma solidity ^0.8.0;

contract MemberVatiable{
	int256[] data1;
	int256[] data2;
	
	function getData1() public view returns(int256 memory){
		return data1;
	} 
	
	function getData2() public view returns(uint256 memory){
		return data2;
	}
	
	function insertData1(int256 d) public{
		return data1.push(d);
	}
	
	function insertData2(int256 d) public{
		return data2.push(d);
	}
	
	//1.成員變量都是存放在storage
	//2.storage存儲位置的變量之間賦值的時候是值拷貝
	function setData2ToData1() public{
		data1 = data2;//成員變量相互賦值
	}
	
	//成員變量本質上就是插槽,storage layout
	function refAndState_1() public{
		int256p[] storage dataref = data1;//data1和dataref指向同一個數據塊(插槽)
		//下面這條語句是引用賦值: data2和dataref指向同一個數據塊(插槽)
		dataref = data2;
	}
	
	//成員變量本質上就是插槽,storage layout
	function refAndState_2() public{
		int256p[] storage dataref = data2;//data2和dataref指向同一個數據塊(插槽)
		//下面這條語句是值拷貝
		data1 = dataref;
	}
	
	function calldata_ref_right(string calldata name) public{
		string calldata temp = name;//引用拷貝
		name = temp;
	}
    function calldata_copy_wrong(string calldata name) public {
        //calldata不能成為值拷貝目標
        //以下內容報錯
        //string memory temp = name;//值拷貝
        //name = temp;
        
        //報錯內容:Type string memory is not implicitly convertible to expected type string calldata.
    }
	
	function valueCopy() public{//值類型的數據之間賦值: 只能是值拷貝
		uint v1 = 1;
		uint v2 = 2;
		v1 = v2;
	}
	
	function refCopy() public{//引用類型的數據之間賦值:
	
		//定義一個引用變量,必須指明他的location,否則報錯,例如
        //string x;  ===>  這會報錯,應該這么寫
        string memory x;//或string calldata x;或string storage x;
        //假如location寫成calldata,那么這個變量x就是指向消息調用calldata的一部分calldata
        //注意:location的calldata指的是數據類型的,而消息調用的數據叫做calldata,兩者不同
	}
	
	function x_1() public{
		data1 = data2;//值拷貝
	}
	
	function x_2() public{
		int256[] memory temp = new int256[](6);
		data1 = temp;//值拷貝
	}
	
	function x_3() public{
		int256[] memory temp = new int256[](6);
		temp1 = data2;//值拷貝
	}
	
	function x_4() public{
		int256[] storage temp = data1;
		temp = data2;//引用拷貝
	}
	
}
  • 例子1
  1. 初始狀態下,調用insertData1()和insertData2()向data1和data2插入兩個數據,結果:getData1()返回[1,1],getData2()返回[2,2]
  2. 調用setData2ToData1(),結果:getData1()返回[2,2],getData2()返回[2,2]
  3. 調用insertData1()向data1插入數據1
  4. 結果:getData1()返回[2,2,1],getData2()返回[2,2]
  5. 調用insertData2()向data2插入數據2
  6. 結果:getData1()返回[2,2,1],getData2()返回[2,2,2]
  7. 結論:storage存儲位置的變量之間賦值的時候是值拷貝
  • 例子2
  1. 初始狀態下,調用insertData1()和insertData2()向data1和data2插入兩個數據,結果:getData1()返回[1,1],getData2()返回[2,2]
  2. 調用refAndState_2(),結果:getData1()返回[2,2],getData2()返回[2,2]