結構&拷貝與引用
開始之前,我們約定數據塊也叫插槽,也就是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
- 初始狀態下,調用insertData1()和insertData2()向data1和data2插入兩個數據,結果:getData1()返回[1,1],getData2()返回[2,2]
- 調用setData2ToData1(),結果:getData1()返回[2,2],getData2()返回[2,2]
- 調用insertData1()向data1插入數據1
- 結果:getData1()返回[2,2,1],getData2()返回[2,2]
- 調用insertData2()向data2插入數據2
- 結果:getData1()返回[2,2,1],getData2()返回[2,2,2]
- 結論:storage存儲位置的變量之間賦值的時候是值拷貝
- 例子2
- 初始狀態下,調用insertData1()和insertData2()向data1和data2插入兩個數據,結果:getData1()返回[1,1],getData2()返回[2,2]
- 調用refAndState_2(),結果:getData1()返回[2,2],getData2()返回[2,2]
信息安全與通信保密雜志社
雷石安全實驗室
中國信息安全
安全牛
信息安全與通信保密雜志社
關鍵基礎設施安全應急響應中心
黑白之道
安全圈
聚銘網絡
黑白之道
0x00實驗室
看雪學苑