以太坊上的几种签名: eth_sign, personal_sign, eth_signTypedData
以太坊的签名算法是ECDSA-secp256k1,以下介绍的每一种签名都是基于该算法,只是用来签名的数据不同。1 交易签名 eth_sign以太坊上,签名之前的交易结构如下。let transaction = { to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: ethers.utils.parseEther('1'), data: '0xE0A293E08F72454CEd99E1769c3ebd21fD2C20a1', gasLimit: '22000', maxFeePerGas: ethers.utils.parseUnits('20', 'gwei'), maxPriorityFeePerGas: ethers.utils.parseUnits('5', 'gwei'), nonce: 1, type: 2, chainId: chainId, // 31337 } 各项目的含义不再介绍,有兴趣可以查阅https://ethereum.org/en/developers/docs/transactions/...
Solidity学习-可升级合约(Transparent/UUPS/Beacon)
以太坊合约原生并不支持升级,目前的升级一般是采用代理模式实现的。代理模式在代理模式中,有2个合约:Proxy和Implementation,用户总是和Proxy进行交互。Proxy在收到用户的调用请求后,并不执行自身的代码,而是通过delegatecall去执行Implementation合约的代码。delegatecall的特殊之处就是,它并不切换上下文,因此Implementation的代码所处理的存储空间是Proxy合约的存储空间,而非自己的。 Proxy合约里存储了Implementation合约的地址,这个地址是可修改的,当我们需要进行合约升级的时候,只需要重新部署一个新的Implementation合约,同时把Proxy合约中存储的地址改成新合约的地址就行了。升级前后,合约的存储空间没有任何变化(依然是Proxy的存储空间),地址也没有变化(依然是Proxy的地址),因此升级过程对用户完全透明。 合约升级之后,要保证storage变量的兼容性。新版本可以在旧版本的变量之后增加新的变量,但是不可以删除或者修改旧版本的变量。因为自始自终,变量都只存储在Proxy合约里,升...
用golang开发ethereum
之前看到一个用golang开发以太坊的教程 https://goethereumbook.org/zh/ 这个教程非常详细,然而它太陈旧了,目前很多go-ethereum函数接口已经有所修改。最明显的是,EIP1559之后,交易的格式的已经大不一样。因此,我基于上述教程,依据最新的go-ethereum(v1.10.26)对代码demo进行了修改。 用golang开发以太坊的一个好处是,可以很方便的查看和调试geth的源码,可以帮助我们更深入地理解以太坊的底层实现。 代码库为 https://github.com/CryptoRbtree/goeth-client 下面对主要功能做简单介绍。1 账户首先需要调用ethclient.DialContext连接一个rpc,这个rpc可以是外部服务商提供的公链rpc,也可以是本地区块链的。var ( ctx = context.Background() url = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID") client, err = ethclie...
以太坊上的几种签名: eth_sign, personal_sign, eth_signTypedData
以太坊的签名算法是ECDSA-secp256k1,以下介绍的每一种签名都是基于该算法,只是用来签名的数据不同。1 交易签名 eth_sign以太坊上,签名之前的交易结构如下。let transaction = { to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', value: ethers.utils.parseEther('1'), data: '0xE0A293E08F72454CEd99E1769c3ebd21fD2C20a1', gasLimit: '22000', maxFeePerGas: ethers.utils.parseUnits('20', 'gwei'), maxPriorityFeePerGas: ethers.utils.parseUnits('5', 'gwei'), nonce: 1, type: 2, chainId: chainId, // 31337 } 各项目的含义不再介绍,有兴趣可以查阅https://ethereum.org/en/developers/docs/transactions/...
Solidity学习-可升级合约(Transparent/UUPS/Beacon)
以太坊合约原生并不支持升级,目前的升级一般是采用代理模式实现的。代理模式在代理模式中,有2个合约:Proxy和Implementation,用户总是和Proxy进行交互。Proxy在收到用户的调用请求后,并不执行自身的代码,而是通过delegatecall去执行Implementation合约的代码。delegatecall的特殊之处就是,它并不切换上下文,因此Implementation的代码所处理的存储空间是Proxy合约的存储空间,而非自己的。 Proxy合约里存储了Implementation合约的地址,这个地址是可修改的,当我们需要进行合约升级的时候,只需要重新部署一个新的Implementation合约,同时把Proxy合约中存储的地址改成新合约的地址就行了。升级前后,合约的存储空间没有任何变化(依然是Proxy的存储空间),地址也没有变化(依然是Proxy的地址),因此升级过程对用户完全透明。 合约升级之后,要保证storage变量的兼容性。新版本可以在旧版本的变量之后增加新的变量,但是不可以删除或者修改旧版本的变量。因为自始自终,变量都只存储在Proxy合约里,升...
用golang开发ethereum
之前看到一个用golang开发以太坊的教程 https://goethereumbook.org/zh/ 这个教程非常详细,然而它太陈旧了,目前很多go-ethereum函数接口已经有所修改。最明显的是,EIP1559之后,交易的格式的已经大不一样。因此,我基于上述教程,依据最新的go-ethereum(v1.10.26)对代码demo进行了修改。 用golang开发以太坊的一个好处是,可以很方便的查看和调试geth的源码,可以帮助我们更深入地理解以太坊的底层实现。 代码库为 https://github.com/CryptoRbtree/goeth-client 下面对主要功能做简单介绍。1 账户首先需要调用ethclient.DialContext连接一个rpc,这个rpc可以是外部服务商提供的公链rpc,也可以是本地区块链的。var ( ctx = context.Background() url = "https://eth-mainnet.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_ID") client, err = ethclie...
Share Dialog
Share Dialog
Subscribe to rbtree
Subscribe to rbtree
<100 subscribers
<100 subscribers
本文讲从EVM操作码的角度,对比internal函数和external函数在执行上的区别。
solidity version = 0.8.15,optimizer = true,optimizer_runs = 200,evm_version = "london"
在线反汇编 https://ethervm.io/decompile
evm执行模拟使用foundry环境https://book.getfoundry.sh/reference/forge/forge-debug
代码如下,我们主要关注internal函数setNumberInternal的调用方式和执行过程。
pragma solidity 0.8.15;
contract Test {
uint256 number1;
uint256 number2;
function setNumber(uint256 newNumber1, uint256 newNumber2) external {
setNumberInternal(newNumber1, newNumber2);
number1 += 0x4321;
}
function setNumberInternal(uint256 newNumber1, uint256 newNumber2) internal {
number1 = newNumber1;
number2 = newNumber2;
}
}
操作码看起来比较费劲,这里贴出反汇编的结果以帮助我们更好地理解。
main()中展示出来的内容大体上对应setNumber,
func_0038(var arg0, var arg1)大体对应setNumberInternal。
认真读代码可以看出,func_0038(var arg0, var arg1)做了优化,其实是把number1 += 0x4321和setNumberInternal整合到了一块。
实际上没有一个完整的代码段完美对应setNumberInternal,我们也不需要,因为这个函数并非external函数,它不会被外部调用。
function main() {
......
var2, var3 = func_005E(var3, var4);
func_0038(var2, var3);
stop();
}
function func_0038(var arg0, var arg1) {
storage[0x00] = arg0;
storage[0x01] = arg1;
var var0 = 0x4321;
var var1 = 0x00;
var var2 = var1;
var var3 = 0x55;
var var4 = var0;
var var5 = arg0;
var3 = func_007F(var4, var5);
storage[var1] = var3;
}
function func_005E(var arg0, var arg1) returns (var r0, var arg0) {
var var0 = 0x00;
var var1 = var0;
if (arg0 - arg1 i< 0x40) { revert(memory[0x00:0x00]); }
var temp0 = arg1;
r0 = msg.data[temp0:temp0 + 0x20];
arg0 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
return r0, arg0;
}
下面从操作码层面分析setNumber(0x1234, 0x5678)的执行过程。
和 var2, var3 = func_005E(var3, var4)对应的操作码如下:
0030 60 PUSH1 0x38
0032 36 CALLDATASIZE
0033 60 PUSH1 0x04
0035 60 PUSH1 0x5e
0037 56 *JUMP
PUSH1 0x38: 0x38是返回地址,这个函数执行完毕之后,会通过JUMP返回到0x38处继续执行。
CALLDATASIZE:calldata大小入栈。calldata实际上是:函数签名hash的前4字节+函数参数。这里的例子里我们有2个uint256参数,它们都是0x20字节,加上签名hash前4字节,应该总共是0x44.
PUSH1 0x04:这里的0x04的含义其实就是签名hash的4字节。这个数字入栈是为了后面从calldata中获取参数的方便。第n个参数在calldata中的偏移地址是0x04 + (n - 1) * 0x20。
PUSH1 0x5e:配合下一句跳转。
*JUMP:跳转到0x5e。

接下来就正式进入func_005E(var arg0, var arg1),我们来看这个函数的执行逻辑。
注意arg0和arg1并不是我们在solidity里面的函数参数,而是我们刚才入栈的calldatasize和函数签名大小(也就是4)。这里首先做了个判断,如果calldatasize-4<0x40,那么说明出错了。这一步是为了防止从calldata中取参数越界。
005E 5B JUMPDEST
005F 60 PUSH1 0x00
0061 80 DUP1
0062 60 PUSH1 0x40
0064 83 DUP4
0065 85 DUP6
0066 03 SUB
0067 12 SLT
0068 15 ISZERO
0069 60 PUSH1 0x70
006B 57 *JUMPI
这里的操作码就不逐行解释了,大意就是calldatasize-4(SUB)之后,判断是否小于0x40(SLT)。如果小于0x40,SLT会返回1,ISZERO会返回0,跳转条件不成立,会继续往下走(revert逻辑,这里没有贴出来),这属于异常情况。正常情况下,跳转条件会成立,会跳到0x70开始执行。

接下来就是从calldata中取得参数。
0070 5B JUMPDEST
0071 50 POP
0072 50 POP
0073 80 DUP1
0074 35 CALLDATALOAD
0075 92 SWAP3
0076 60 PUSH1 0x20
0078 90 SWAP1
0079 91 SWAP2
007A 01 ADD
007B 35 CALLDATALOAD
007C 91 SWAP2
007D 50 POP
007E 56 *JUMP
从calldata中取参数的是CALLDATALOAD指令,取参数的偏移地址是执行指令是栈顶的数字。下面是取第一个参数0x1234的过程。


在取完两个参数之后,我们的栈会变成下面这样:

可以注意到,通过一些SWAP指令,2个参数不在00和01位置,而在01和02位置,栈顶是0x38。还记得它吗?这就是我们在执行这个函数之前压栈的返回地址,现在函数执行完毕,它终于派上用场了。接下来,我们就会回到0x38指令。
至此var2, var3 = func_005E(var3, var4)执行完毕,接下来是func_0038(var2, var3)。
0038 5B JUMPDEST
0039 60 PUSH1 0x3e
003B 56 *JUMP
跳转到003E,这里对应solidity里面setNumberInternal内部代码以及number1 += 0x4321。
003E 5B JUMPDEST
003F 60 PUSH1 0x00
0041 82 DUP3
0042 81 DUP2
0043 55 SSTORE
0044 60 PUSH1 0x01
0046 82 DUP3
0047 90 SWAP1
0048 55 SSTORE
值得注意的是,在这段代码中,我们并没有看到CALLDATALOAD指令,这是一个internal函数,对internal函数的调用并不是message call,并不会生成一个新的calldata。这里需要的calldata中的内容,我们在上一步都已经把它们取到栈内了。
这段代码中,我们可以看到,0x1234和0x5678会被SSTORE指令(0043、0048)存入storage。

在这之后,0x4321入栈,这是因为我们有number1 += 0x4321这行代码。
0049 61 PUSH2 0x4321
004C 90 SWAP1
004D 80 DUP1
004E 60 PUSH1 0x55
0050 83 DUP4
0051 86 DUP7
0052 60 PUSH1 0x7f
0054 56 *JUMP
PUSH1 0x55:又是一个返回地址入栈,我们执行完number1 += 0x4321的逻辑之后需要回来。
PUSH1 0x7f,*JUMP:跳转到0x7f。
0x7f这一段对应var3 = func_007F(var4, var5);
007F 5B JUMPDEST
0080 60 PUSH1 0x00
0082 82 DUP3
0083 19 NOT
0084 82 DUP3
0085 11 GT
0086 15 ISZERO
0087 60 PUSH1 0x9f
0089 57 *JUMPI
这里的代码可能有点让人费解,为什么会有GT(大小比较)?因为这里要做一个加法,而加法是有可能超出uint256的范围导致翻转的。如何判断a+b的结果有没有翻转?判断a是否大于~b,如果大于则没有翻转。在上面的例子里,是把0x4321进行了按位取反,判断0x1234是否大于取反后的数字。

这里的当然没有翻转,所以GT结果为0,ISZERO结果为1,跳转0x9f(0x8A开始是加法翻转的处理代码,这里并未贴出)。
009F 5B JUMPDEST
00A0 50 POP
00A1 01 ADD
00A2 90 SWAP1
00A3 56 *JUMP
执行加法,0x1234+0x4321,跳转0x55(刚才保存的返回地址)。可以看到加法的结果0x5555已经保存在栈中了。

0055 5B JUMPDEST
0056 90 SWAP1
0057 91 SWAP2
0058 55 SSTORE
0059 50 POP
005A 50 POP
005B 50 POP
005C 50 POP
005D 56 *JUMP
最后这一步很简单,SSTORE保存结果。清理栈内容。

至此,整个setNumber(0x1234, 0x5678)执行完毕。
我们回顾一下其中setNumberInternal的逻辑部分,基本上都是在不停地jump,并没有call指令。
代码如下,我们主要关注external函数setNumberExternal的调用方式和执行过程。
pragma solidity 0.8.15;
contract Test {
uint256 number1;
uint256 number2;
function setNumber(uint256 newNumber1, uint256 newNumber2) external {
this.setNumberExternal(newNumber1, newNumber2);
number1 += 0x4321;
}
function setNumberExternal(uint256 newNumber1, uint256 newNumber2) external {
number1 = newNumber1;
number2 = newNumber2;
}
}
反汇编代码如下,
if (var0 == 0x6b19e8c7)分支对应函数setNumber(uint256,uint256),
func_0049对应setNumberExternal(uint256 newNumber1, uint256 newNumber2)和number1 += 0x4321。
else if (var0 == 0xc91bdb7f)分支对应setExternalNumber(uint256,uint256),在我们message call调用setNumberExternal的时候会用到。
function main() {
......
if (var0 == 0x6b19e8c7) {
// Dispatch table entry for setNumber(uint256,uint256)
var var1 = 0x004e;
var var2 = 0x0049;
var var3 = msg.data.length;
var var4 = 0x04;
var2, var3 = func_00DD(var3, var4);
func_0049(var2, var3);
stop();
else if (var0 == 0xc91bdb7f) {
// Dispatch table entry for setNumberExternal(uint256,uint256)
var1 = 0x004e;
var2 = 0x005e;
var3 = msg.data.length;
var4 = 0x04;
var2, var3 = func_00DD(var3, var4);
func_005E(var2, var3);
stop();
}
......
}
function func_0049(var arg0, var arg1) {
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = 0xc91bdb7f << 0xe0;
memory[temp0 + 0x04:temp0 + 0x04 + 0x20] = arg0;
memory[temp0 + 0x24:temp0 + 0x24 + 0x20] = arg1;
var var0 = address(this);
var var1 = 0xc91bdb7f;
var var2 = temp0 + 0x44;
var var3 = 0x00;
var var4 = memory[0x40:0x60];
var var5 = var2 - var4;
var var6 = var4;
var var7 = 0x00;
var var8 = var0;
var var9 = !address(var8).code.length;
if (var9) { revert(memory[0x00:0x00]); }
var temp1;
temp1, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
var3 = !temp1;
if (!var3) {
var0 = 0x4321;
var1 = 0x00;
var2 = var1;
var3 = 0x00d4;
var4 = var0;
var5 = storage[var2];
var3 = func_00FF(var4, var5);
storage[var1] = var3;
return;
} else {
var temp2 = returndata.length;
memory[0x00:0x00 + temp2] = returndata[0x00:0x00 + temp2];
revert(memory[0x00:0x00 + returndata.length]);
}
}
function func_005E(var arg0, var arg1) {
storage[0x00] = arg0;
storage[0x01] = arg1;
}
function func_00DD(var arg0, var arg1) returns (var r0, var arg0) {
var var0 = 0x00;
var var1 = var0;
if (arg0 - arg1 i< 0x40) { revert(memory[0x00:0x00]); }
var temp0 = arg1;
r0 = msg.data[temp0:temp0 + 0x20];
arg0 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
return r0, arg0;
}
下面从操作码层面分析setNumber(0x1234, 0x5678)的执行过程。
我们从var2, var3 = func_00DD(var3, var4)开始,
下面的代码和上一节Internal里面是一致的,取参数之前,判断calldata足够大。
00DD 5B JUMPDEST
00DE 60 PUSH1 0x00
00E0 80 DUP1
00E1 60 PUSH1 0x40
00E3 83 DUP4
00E4 85 DUP6
00E5 03 SUB
00E6 12 SLT
00E7 15 ISZERO
00E8 61 PUSH2 0x00f0
00EB 57 *JUMPI
然后是取参数。
00F0 5B JUMPDEST
00F1 50 POP
00F2 50 POP
00F3 80 DUP1
00F4 35 CALLDATALOAD
00F5 92 SWAP3
00F6 60 PUSH1 0x20
00F8 90 SWAP1
00F9 91 SWAP2
00FA 01 ADD
00FB 35 CALLDATALOAD
00FC 91 SWAP2
00FD 50 POP
00FE 56 *JUMP
取到参数后,0x1234和0x5678被放入栈中。

接下来,终于来到了体现external函数和internal函数不同的地方。
external函数的调用时一个message call,需要构造calldata。calldata时函数签名hash前4字节+函数参数。所以首先需要的是setNumberExternal(uint256,uint256)的hash前4字节,它是0xc91bdb7f。
0069 5B JUMPDEST
006A 60 PUSH1 0x40
006C 51 MLOAD
006D 63 PUSH4 0xc91bdb7f
0072 60 PUSH1 0xe0
0074 1B SHL
0075 81 DUP2
0076 52 MSTORE
注意evm数据操作的基本单位是32字节,而我们只需要4字节,所以多了一步移位操作(SHL),左移224位(28字节)。

然后使用MSTORE命令,把它复制到了memory中。(这里memory可以看成一个比栈空间更大的存储区域,因为存储函数参数的话,栈可能会不够用,用到了memory)
memory的起始位从默认0x80开始,0x80之前有其他用途。
0x40存放的就是这个起始位置(自由空间指针)。
https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html

接下来我们还需要放入函数的2个参数。
这段代码我们还看到了一些ADD指令,其实这里是在计算Memory里面的偏移地址,刚才已经放入了0xc91bdb7f,所以再往里放参数的时候需要+0x4,第二次需要+0x24。
0077 60 PUSH1 0x04
0079 81 DUP2
007A 01 ADD
007B 83 DUP4
007C 90 SWAP1
007D 52 MSTORE
007E 60 PUSH1 0x24
0080 81 DUP2
0081 01 ADD
0082 82 DUP3
0083 90 SWAP1
0084 52 MSTORE
这段代码我们还看到了一些ADD指令,其实这里是在计算Memory里面的偏移地址,刚才已经放入了0xc91bdb7f,所以再往里放参数的时候需要+0x4,第二次需要+0x24。

至此,calldata数据组装完毕。
message call需要的不仅是calldata,还需要sender和value。
在这里,sender就是当前合约的地址,通过ADDRESS指令获得。
后面两条指令再次让函数签名入栈,似乎有点没必要,可能是为了后面的使用。
0085 30 ADDRESS
0086 90 SWAP1
0087 63 PUSH4 0xc91bdb7f

下面的代码则是计算了memory中calldata的始末位置,在这个例子里是0x80 ~ 0x80+0x44(即0xc4)
008C 90 SWAP1
008D 60 PUSH1 0x44
008F 01 ADD
0090 60 PUSH1 0x00
0092 60 PUSH1 0x40
0094 51 MLOAD

计算calldata的大小(0x44)
0095 80 DUP1
0096 83 DUP4
0097 03 SUB

EXTCODESIZE获取即将调用合约的代码大小(这个例子里就是自己),判断是否为0.
如果合约代码大小为0,说明地址对应的是并非一个合约,无法完成message call,会revert(0xA5代码,未展示)。
0098 81 DUP2
0099 60 PUSH1 0x00
009B 87 DUP8
009C 80 DUP1
009D 3B EXTCODESIZE
009E 15 ISZERO
009F 80 DUP1
00A0 15 ISZERO
00A1 61 PUSH2 0x00a9
00A4 57 *JUMPI
合约代码校验通过后,跳转到0xa9.
通过GAS指令获得msg.gas。这个数字代表message cal中可以使用的gas。
至此,message call所需要的数据准备完毕。
我们可以在栈内00-06分别对应:gas、sender address、value、argsOffset、argsSize、retOffset、retSize。
栈内argsOffset、argsSize指定了memory中参数真正的存储位置。
retOffset、retSize,是message call完成之后,返回值在memory中存放的位置和大小,本例中没有返回值,忽略。

接下来会执行call函数,开始message call。
执行之后,我们可以看到,stack和memory的内容都刷新了,实际上它们都开启了一个新的实例。
然后,需要从新的calldata(也就是刚才构造的calldata)中获取参数来执行setNumberExternal(uint256 newNumber1, uint256 newNumber2)的逻辑。

这里并不能直接定位到setNumberExternal所在的代码,而是像外部账户发起合约交易一样,需要根据函数签名寻找函数。这里其实是回到了main函数之中。
在经过一些if判断之后,会走到else if (var0 == 0xc91bdb7f) 这里。
然后进入func_00DD(var3, var4),从calldata中获取参数,这个刚才已经分析过了,不再赘述。
接下来进入func_005E(var2, var3)。
005E 5B JUMPDEST
005F 60 PUSH1 0x00
0061 91 SWAP2
0062 90 SWAP1
0063 91 SWAP2
0064 55 SSTORE
0065 60 PUSH1 0x01
0067 55 SSTORE
0068 56 *JUMP
这一段很容易理解,就是用SSTORE指令把0x1234和0x5678赋值给storage上的两个变量。
接下来是STOP,消息调用的返回。
004E 5B JUMPDEST
004F 00 *STOP
至此,我们的message call结束。

message call结束之后,其对应的stack和memory都会销毁,我们再次回到了之前的stack和memory。

栈顶存储的应该是message call的执行状态,1表示成功。
确认message call成功之后,别忘了我们还有number1 += 0x4321这个逻辑。
这些逻辑第1节里已经讲过,不再赘述。
00BD 5B JUMPDEST
00BE 50 POP
00BF 50 POP
00C0 50 POP
00C1 50 POP
00C2 61 PUSH2 0x4321
00C5 60 PUSH1 0x00
00C7 80 DUP1
00C8 82 DUP3
00C9 82 DUP3
00CA 54 SLOAD
00CB 61 PUSH2 0x00d4
00CE 91 SWAP2
00CF 90 SWAP1
00D0 61 PUSH2 0x00ff
00D3 56 *JUMP
......
00FF 5B JUMPDEST
0100 60 PUSH1 0x00
0102 82 DUP3
0103 19 NOT
0104 82 DUP3
0105 11 GT
0106 15 ISZERO
0107 61 PUSH2 0x0120
010A 57 *JUMPI
......
0120 5B JUMPDEST
0121 50 POP
0122 01 ADD
0123 90 SWAP1
0124 56 *JUMP
......
00D4 5B JUMPDEST
00D5 90 SWAP1
00D6 91 SWAP2
00D7 55 SSTORE
00D8 50 POP
00D9 50 POP
00DA 50 POP
00DB 50 POP
00DC 56 *JUMP
......
004E 5B JUMPDEST
004F 00 *STOP
可以看出,external函数调用是message call,它比internal函数调用要麻烦很多,需要构造calldata,需要新开stack和memory的实例,而internal函数调用只需要简单的指令跳转。
本文讲从EVM操作码的角度,对比internal函数和external函数在执行上的区别。
solidity version = 0.8.15,optimizer = true,optimizer_runs = 200,evm_version = "london"
在线反汇编 https://ethervm.io/decompile
evm执行模拟使用foundry环境https://book.getfoundry.sh/reference/forge/forge-debug
代码如下,我们主要关注internal函数setNumberInternal的调用方式和执行过程。
pragma solidity 0.8.15;
contract Test {
uint256 number1;
uint256 number2;
function setNumber(uint256 newNumber1, uint256 newNumber2) external {
setNumberInternal(newNumber1, newNumber2);
number1 += 0x4321;
}
function setNumberInternal(uint256 newNumber1, uint256 newNumber2) internal {
number1 = newNumber1;
number2 = newNumber2;
}
}
操作码看起来比较费劲,这里贴出反汇编的结果以帮助我们更好地理解。
main()中展示出来的内容大体上对应setNumber,
func_0038(var arg0, var arg1)大体对应setNumberInternal。
认真读代码可以看出,func_0038(var arg0, var arg1)做了优化,其实是把number1 += 0x4321和setNumberInternal整合到了一块。
实际上没有一个完整的代码段完美对应setNumberInternal,我们也不需要,因为这个函数并非external函数,它不会被外部调用。
function main() {
......
var2, var3 = func_005E(var3, var4);
func_0038(var2, var3);
stop();
}
function func_0038(var arg0, var arg1) {
storage[0x00] = arg0;
storage[0x01] = arg1;
var var0 = 0x4321;
var var1 = 0x00;
var var2 = var1;
var var3 = 0x55;
var var4 = var0;
var var5 = arg0;
var3 = func_007F(var4, var5);
storage[var1] = var3;
}
function func_005E(var arg0, var arg1) returns (var r0, var arg0) {
var var0 = 0x00;
var var1 = var0;
if (arg0 - arg1 i< 0x40) { revert(memory[0x00:0x00]); }
var temp0 = arg1;
r0 = msg.data[temp0:temp0 + 0x20];
arg0 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
return r0, arg0;
}
下面从操作码层面分析setNumber(0x1234, 0x5678)的执行过程。
和 var2, var3 = func_005E(var3, var4)对应的操作码如下:
0030 60 PUSH1 0x38
0032 36 CALLDATASIZE
0033 60 PUSH1 0x04
0035 60 PUSH1 0x5e
0037 56 *JUMP
PUSH1 0x38: 0x38是返回地址,这个函数执行完毕之后,会通过JUMP返回到0x38处继续执行。
CALLDATASIZE:calldata大小入栈。calldata实际上是:函数签名hash的前4字节+函数参数。这里的例子里我们有2个uint256参数,它们都是0x20字节,加上签名hash前4字节,应该总共是0x44.
PUSH1 0x04:这里的0x04的含义其实就是签名hash的4字节。这个数字入栈是为了后面从calldata中获取参数的方便。第n个参数在calldata中的偏移地址是0x04 + (n - 1) * 0x20。
PUSH1 0x5e:配合下一句跳转。
*JUMP:跳转到0x5e。

接下来就正式进入func_005E(var arg0, var arg1),我们来看这个函数的执行逻辑。
注意arg0和arg1并不是我们在solidity里面的函数参数,而是我们刚才入栈的calldatasize和函数签名大小(也就是4)。这里首先做了个判断,如果calldatasize-4<0x40,那么说明出错了。这一步是为了防止从calldata中取参数越界。
005E 5B JUMPDEST
005F 60 PUSH1 0x00
0061 80 DUP1
0062 60 PUSH1 0x40
0064 83 DUP4
0065 85 DUP6
0066 03 SUB
0067 12 SLT
0068 15 ISZERO
0069 60 PUSH1 0x70
006B 57 *JUMPI
这里的操作码就不逐行解释了,大意就是calldatasize-4(SUB)之后,判断是否小于0x40(SLT)。如果小于0x40,SLT会返回1,ISZERO会返回0,跳转条件不成立,会继续往下走(revert逻辑,这里没有贴出来),这属于异常情况。正常情况下,跳转条件会成立,会跳到0x70开始执行。

接下来就是从calldata中取得参数。
0070 5B JUMPDEST
0071 50 POP
0072 50 POP
0073 80 DUP1
0074 35 CALLDATALOAD
0075 92 SWAP3
0076 60 PUSH1 0x20
0078 90 SWAP1
0079 91 SWAP2
007A 01 ADD
007B 35 CALLDATALOAD
007C 91 SWAP2
007D 50 POP
007E 56 *JUMP
从calldata中取参数的是CALLDATALOAD指令,取参数的偏移地址是执行指令是栈顶的数字。下面是取第一个参数0x1234的过程。


在取完两个参数之后,我们的栈会变成下面这样:

可以注意到,通过一些SWAP指令,2个参数不在00和01位置,而在01和02位置,栈顶是0x38。还记得它吗?这就是我们在执行这个函数之前压栈的返回地址,现在函数执行完毕,它终于派上用场了。接下来,我们就会回到0x38指令。
至此var2, var3 = func_005E(var3, var4)执行完毕,接下来是func_0038(var2, var3)。
0038 5B JUMPDEST
0039 60 PUSH1 0x3e
003B 56 *JUMP
跳转到003E,这里对应solidity里面setNumberInternal内部代码以及number1 += 0x4321。
003E 5B JUMPDEST
003F 60 PUSH1 0x00
0041 82 DUP3
0042 81 DUP2
0043 55 SSTORE
0044 60 PUSH1 0x01
0046 82 DUP3
0047 90 SWAP1
0048 55 SSTORE
值得注意的是,在这段代码中,我们并没有看到CALLDATALOAD指令,这是一个internal函数,对internal函数的调用并不是message call,并不会生成一个新的calldata。这里需要的calldata中的内容,我们在上一步都已经把它们取到栈内了。
这段代码中,我们可以看到,0x1234和0x5678会被SSTORE指令(0043、0048)存入storage。

在这之后,0x4321入栈,这是因为我们有number1 += 0x4321这行代码。
0049 61 PUSH2 0x4321
004C 90 SWAP1
004D 80 DUP1
004E 60 PUSH1 0x55
0050 83 DUP4
0051 86 DUP7
0052 60 PUSH1 0x7f
0054 56 *JUMP
PUSH1 0x55:又是一个返回地址入栈,我们执行完number1 += 0x4321的逻辑之后需要回来。
PUSH1 0x7f,*JUMP:跳转到0x7f。
0x7f这一段对应var3 = func_007F(var4, var5);
007F 5B JUMPDEST
0080 60 PUSH1 0x00
0082 82 DUP3
0083 19 NOT
0084 82 DUP3
0085 11 GT
0086 15 ISZERO
0087 60 PUSH1 0x9f
0089 57 *JUMPI
这里的代码可能有点让人费解,为什么会有GT(大小比较)?因为这里要做一个加法,而加法是有可能超出uint256的范围导致翻转的。如何判断a+b的结果有没有翻转?判断a是否大于~b,如果大于则没有翻转。在上面的例子里,是把0x4321进行了按位取反,判断0x1234是否大于取反后的数字。

这里的当然没有翻转,所以GT结果为0,ISZERO结果为1,跳转0x9f(0x8A开始是加法翻转的处理代码,这里并未贴出)。
009F 5B JUMPDEST
00A0 50 POP
00A1 01 ADD
00A2 90 SWAP1
00A3 56 *JUMP
执行加法,0x1234+0x4321,跳转0x55(刚才保存的返回地址)。可以看到加法的结果0x5555已经保存在栈中了。

0055 5B JUMPDEST
0056 90 SWAP1
0057 91 SWAP2
0058 55 SSTORE
0059 50 POP
005A 50 POP
005B 50 POP
005C 50 POP
005D 56 *JUMP
最后这一步很简单,SSTORE保存结果。清理栈内容。

至此,整个setNumber(0x1234, 0x5678)执行完毕。
我们回顾一下其中setNumberInternal的逻辑部分,基本上都是在不停地jump,并没有call指令。
代码如下,我们主要关注external函数setNumberExternal的调用方式和执行过程。
pragma solidity 0.8.15;
contract Test {
uint256 number1;
uint256 number2;
function setNumber(uint256 newNumber1, uint256 newNumber2) external {
this.setNumberExternal(newNumber1, newNumber2);
number1 += 0x4321;
}
function setNumberExternal(uint256 newNumber1, uint256 newNumber2) external {
number1 = newNumber1;
number2 = newNumber2;
}
}
反汇编代码如下,
if (var0 == 0x6b19e8c7)分支对应函数setNumber(uint256,uint256),
func_0049对应setNumberExternal(uint256 newNumber1, uint256 newNumber2)和number1 += 0x4321。
else if (var0 == 0xc91bdb7f)分支对应setExternalNumber(uint256,uint256),在我们message call调用setNumberExternal的时候会用到。
function main() {
......
if (var0 == 0x6b19e8c7) {
// Dispatch table entry for setNumber(uint256,uint256)
var var1 = 0x004e;
var var2 = 0x0049;
var var3 = msg.data.length;
var var4 = 0x04;
var2, var3 = func_00DD(var3, var4);
func_0049(var2, var3);
stop();
else if (var0 == 0xc91bdb7f) {
// Dispatch table entry for setNumberExternal(uint256,uint256)
var1 = 0x004e;
var2 = 0x005e;
var3 = msg.data.length;
var4 = 0x04;
var2, var3 = func_00DD(var3, var4);
func_005E(var2, var3);
stop();
}
......
}
function func_0049(var arg0, var arg1) {
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = 0xc91bdb7f << 0xe0;
memory[temp0 + 0x04:temp0 + 0x04 + 0x20] = arg0;
memory[temp0 + 0x24:temp0 + 0x24 + 0x20] = arg1;
var var0 = address(this);
var var1 = 0xc91bdb7f;
var var2 = temp0 + 0x44;
var var3 = 0x00;
var var4 = memory[0x40:0x60];
var var5 = var2 - var4;
var var6 = var4;
var var7 = 0x00;
var var8 = var0;
var var9 = !address(var8).code.length;
if (var9) { revert(memory[0x00:0x00]); }
var temp1;
temp1, memory[var4:var4 + var3] = address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6 + var5]);
var3 = !temp1;
if (!var3) {
var0 = 0x4321;
var1 = 0x00;
var2 = var1;
var3 = 0x00d4;
var4 = var0;
var5 = storage[var2];
var3 = func_00FF(var4, var5);
storage[var1] = var3;
return;
} else {
var temp2 = returndata.length;
memory[0x00:0x00 + temp2] = returndata[0x00:0x00 + temp2];
revert(memory[0x00:0x00 + returndata.length]);
}
}
function func_005E(var arg0, var arg1) {
storage[0x00] = arg0;
storage[0x01] = arg1;
}
function func_00DD(var arg0, var arg1) returns (var r0, var arg0) {
var var0 = 0x00;
var var1 = var0;
if (arg0 - arg1 i< 0x40) { revert(memory[0x00:0x00]); }
var temp0 = arg1;
r0 = msg.data[temp0:temp0 + 0x20];
arg0 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
return r0, arg0;
}
下面从操作码层面分析setNumber(0x1234, 0x5678)的执行过程。
我们从var2, var3 = func_00DD(var3, var4)开始,
下面的代码和上一节Internal里面是一致的,取参数之前,判断calldata足够大。
00DD 5B JUMPDEST
00DE 60 PUSH1 0x00
00E0 80 DUP1
00E1 60 PUSH1 0x40
00E3 83 DUP4
00E4 85 DUP6
00E5 03 SUB
00E6 12 SLT
00E7 15 ISZERO
00E8 61 PUSH2 0x00f0
00EB 57 *JUMPI
然后是取参数。
00F0 5B JUMPDEST
00F1 50 POP
00F2 50 POP
00F3 80 DUP1
00F4 35 CALLDATALOAD
00F5 92 SWAP3
00F6 60 PUSH1 0x20
00F8 90 SWAP1
00F9 91 SWAP2
00FA 01 ADD
00FB 35 CALLDATALOAD
00FC 91 SWAP2
00FD 50 POP
00FE 56 *JUMP
取到参数后,0x1234和0x5678被放入栈中。

接下来,终于来到了体现external函数和internal函数不同的地方。
external函数的调用时一个message call,需要构造calldata。calldata时函数签名hash前4字节+函数参数。所以首先需要的是setNumberExternal(uint256,uint256)的hash前4字节,它是0xc91bdb7f。
0069 5B JUMPDEST
006A 60 PUSH1 0x40
006C 51 MLOAD
006D 63 PUSH4 0xc91bdb7f
0072 60 PUSH1 0xe0
0074 1B SHL
0075 81 DUP2
0076 52 MSTORE
注意evm数据操作的基本单位是32字节,而我们只需要4字节,所以多了一步移位操作(SHL),左移224位(28字节)。

然后使用MSTORE命令,把它复制到了memory中。(这里memory可以看成一个比栈空间更大的存储区域,因为存储函数参数的话,栈可能会不够用,用到了memory)
memory的起始位从默认0x80开始,0x80之前有其他用途。
0x40存放的就是这个起始位置(自由空间指针)。
https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html

接下来我们还需要放入函数的2个参数。
这段代码我们还看到了一些ADD指令,其实这里是在计算Memory里面的偏移地址,刚才已经放入了0xc91bdb7f,所以再往里放参数的时候需要+0x4,第二次需要+0x24。
0077 60 PUSH1 0x04
0079 81 DUP2
007A 01 ADD
007B 83 DUP4
007C 90 SWAP1
007D 52 MSTORE
007E 60 PUSH1 0x24
0080 81 DUP2
0081 01 ADD
0082 82 DUP3
0083 90 SWAP1
0084 52 MSTORE
这段代码我们还看到了一些ADD指令,其实这里是在计算Memory里面的偏移地址,刚才已经放入了0xc91bdb7f,所以再往里放参数的时候需要+0x4,第二次需要+0x24。

至此,calldata数据组装完毕。
message call需要的不仅是calldata,还需要sender和value。
在这里,sender就是当前合约的地址,通过ADDRESS指令获得。
后面两条指令再次让函数签名入栈,似乎有点没必要,可能是为了后面的使用。
0085 30 ADDRESS
0086 90 SWAP1
0087 63 PUSH4 0xc91bdb7f

下面的代码则是计算了memory中calldata的始末位置,在这个例子里是0x80 ~ 0x80+0x44(即0xc4)
008C 90 SWAP1
008D 60 PUSH1 0x44
008F 01 ADD
0090 60 PUSH1 0x00
0092 60 PUSH1 0x40
0094 51 MLOAD

计算calldata的大小(0x44)
0095 80 DUP1
0096 83 DUP4
0097 03 SUB

EXTCODESIZE获取即将调用合约的代码大小(这个例子里就是自己),判断是否为0.
如果合约代码大小为0,说明地址对应的是并非一个合约,无法完成message call,会revert(0xA5代码,未展示)。
0098 81 DUP2
0099 60 PUSH1 0x00
009B 87 DUP8
009C 80 DUP1
009D 3B EXTCODESIZE
009E 15 ISZERO
009F 80 DUP1
00A0 15 ISZERO
00A1 61 PUSH2 0x00a9
00A4 57 *JUMPI
合约代码校验通过后,跳转到0xa9.
通过GAS指令获得msg.gas。这个数字代表message cal中可以使用的gas。
至此,message call所需要的数据准备完毕。
我们可以在栈内00-06分别对应:gas、sender address、value、argsOffset、argsSize、retOffset、retSize。
栈内argsOffset、argsSize指定了memory中参数真正的存储位置。
retOffset、retSize,是message call完成之后,返回值在memory中存放的位置和大小,本例中没有返回值,忽略。

接下来会执行call函数,开始message call。
执行之后,我们可以看到,stack和memory的内容都刷新了,实际上它们都开启了一个新的实例。
然后,需要从新的calldata(也就是刚才构造的calldata)中获取参数来执行setNumberExternal(uint256 newNumber1, uint256 newNumber2)的逻辑。

这里并不能直接定位到setNumberExternal所在的代码,而是像外部账户发起合约交易一样,需要根据函数签名寻找函数。这里其实是回到了main函数之中。
在经过一些if判断之后,会走到else if (var0 == 0xc91bdb7f) 这里。
然后进入func_00DD(var3, var4),从calldata中获取参数,这个刚才已经分析过了,不再赘述。
接下来进入func_005E(var2, var3)。
005E 5B JUMPDEST
005F 60 PUSH1 0x00
0061 91 SWAP2
0062 90 SWAP1
0063 91 SWAP2
0064 55 SSTORE
0065 60 PUSH1 0x01
0067 55 SSTORE
0068 56 *JUMP
这一段很容易理解,就是用SSTORE指令把0x1234和0x5678赋值给storage上的两个变量。
接下来是STOP,消息调用的返回。
004E 5B JUMPDEST
004F 00 *STOP
至此,我们的message call结束。

message call结束之后,其对应的stack和memory都会销毁,我们再次回到了之前的stack和memory。

栈顶存储的应该是message call的执行状态,1表示成功。
确认message call成功之后,别忘了我们还有number1 += 0x4321这个逻辑。
这些逻辑第1节里已经讲过,不再赘述。
00BD 5B JUMPDEST
00BE 50 POP
00BF 50 POP
00C0 50 POP
00C1 50 POP
00C2 61 PUSH2 0x4321
00C5 60 PUSH1 0x00
00C7 80 DUP1
00C8 82 DUP3
00C9 82 DUP3
00CA 54 SLOAD
00CB 61 PUSH2 0x00d4
00CE 91 SWAP2
00CF 90 SWAP1
00D0 61 PUSH2 0x00ff
00D3 56 *JUMP
......
00FF 5B JUMPDEST
0100 60 PUSH1 0x00
0102 82 DUP3
0103 19 NOT
0104 82 DUP3
0105 11 GT
0106 15 ISZERO
0107 61 PUSH2 0x0120
010A 57 *JUMPI
......
0120 5B JUMPDEST
0121 50 POP
0122 01 ADD
0123 90 SWAP1
0124 56 *JUMP
......
00D4 5B JUMPDEST
00D5 90 SWAP1
00D6 91 SWAP2
00D7 55 SSTORE
00D8 50 POP
00D9 50 POP
00DA 50 POP
00DB 50 POP
00DC 56 *JUMP
......
004E 5B JUMPDEST
004F 00 *STOP
可以看出,external函数调用是message call,它比internal函数调用要麻烦很多,需要构造calldata,需要新开stack和memory的实例,而internal函数调用只需要简单的指令跳转。
No activity yet