# EVM学习- internal / external函数执行过程

By [rbtree](https://paragraph.com/@rbtree) · 2022-09-19

---

本文讲从EVM操作码的角度，对比internal函数和external函数在执行上的区别。

solidity version = 0.8.15，optimizer = true，optimizer\_runs = 200，evm\_version = "london"

在线反汇编 [https://ethervm.io/decompile](https://ethervm.io/decompile)

evm执行模拟使用foundry环境[https://book.getfoundry.sh/reference/forge/forge-debug](https://book.getfoundry.sh/reference/forge/forge-debug)

1 internal函数
------------

代码如下，我们主要关注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。

![](https://storage.googleapis.com/papyrus_images/15f6f727742f70e2f87411b973e6dfac944e16c3d2a00efaa4ad9593a49e3a37.png)

接下来就正式进入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开始执行。

![](https://storage.googleapis.com/papyrus_images/9c97c1ace268e7fc7f87c26611733b6a0dbc1d214a2332b10dca3e769a27b997.png)

接下来就是从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的过程。

![](https://storage.googleapis.com/papyrus_images/106cc618a1563611b6db9e5b9b388f9cfc4d1d26298e65468f9b769653ef79f7.png)

![](https://storage.googleapis.com/papyrus_images/06d36dbf9baa449a0bef0c6cd597924459b740b7f715bd121d4b447cc7129ae5.png)

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

![](https://storage.googleapis.com/papyrus_images/53aeb35f57f06a65711dab03cc3ab701d8b3251d179bae4cb72ce8a5a86b8e2e.png)

可以注意到，通过一些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。

![](https://storage.googleapis.com/papyrus_images/d93242087275d7c1fed3814a3ba50b427a7ab1aa3dad9f71a0d9a3f60a4d5f25.png)

在这之后，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是否大于取反后的数字。

![](https://storage.googleapis.com/papyrus_images/a30e85484b3a78c74e85718dc98b70928ec759c41fe97de6f547601fe6183860.png)

这里的当然没有翻转，所以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已经保存在栈中了。

![](https://storage.googleapis.com/papyrus_images/ea895c7bc9a40f445dcddf6fb35f43f67bf1fa020e18f237da8450a4117c6eda.png)

    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保存结果。清理栈内容。

![](https://storage.googleapis.com/papyrus_images/36d0fab1365e07ea82fc64400b3e42afde893601bfbd23c2e635511e2c14f264.png)

至此，整个setNumber(0x1234, 0x5678)执行完毕。

我们回顾一下其中setNumberInternal的逻辑部分，基本上都是在不停地jump，并没有call指令。

2 external函数
------------

代码如下，我们主要关注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被放入栈中。

![](https://storage.googleapis.com/papyrus_images/c609384af37c996ed62eb63c557af85d1fcd8eeded5027aff75394c9c9f8197b.png)

接下来，终于来到了体现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字节)。

![](https://storage.googleapis.com/papyrus_images/525b59cc0a38e25664d0771974e3821e57bf27821133ea518f511cb0eb50749c.png)

然后使用MSTORE命令，把它复制到了memory中。（这里memory可以看成一个比栈空间更大的存储区域，因为存储函数参数的话，栈可能会不够用，用到了memory）

memory的起始位从默认0x80开始，0x80之前有其他用途。

0x40存放的就是这个起始位置(自由空间指针)。

[https://docs.soliditylang.org/en/latest/internals/layout\_in\_memory.html](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html)

![](https://storage.googleapis.com/papyrus_images/9ab052d7083c5021dcf54c024a1be904e99c238c0dd56556dcf97040e8766e0a.png)

接下来我们还需要放入函数的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。

![](https://storage.googleapis.com/papyrus_images/3d5abb4236be1e4872d0f87e4a275031e34a028b356680f29007a71a476d681b.png)

至此，calldata数据组装完毕。

message call需要的不仅是calldata，还需要sender和value。

在这里，sender就是当前合约的地址，通过ADDRESS指令获得。

后面两条指令再次让函数签名入栈，似乎有点没必要，可能是为了后面的使用。

    0085    30  ADDRESS
    0086    90  SWAP1
    0087    63  PUSH4 0xc91bdb7f
    

![](https://storage.googleapis.com/papyrus_images/1a3e702763a42716e98ec8347a81c2eb8d93f7db6245220b8fbc3aebc7298c43.png)

下面的代码则是计算了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
    

![](https://storage.googleapis.com/papyrus_images/50f579289a34de61e1899707371609fcf698baee154748775bb12ab0bbb37b5b.png)

计算calldata的大小(0x44)

    0095    80  DUP1
    0096    83  DUP4
    0097    03  SUB
    

![](https://storage.googleapis.com/papyrus_images/57ce398888f13196748b648194afa3177fb8dcbc29a338b9f64b7c86d2429ccc.png)

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中存放的位置和大小，本例中没有返回值，忽略。

![](https://storage.googleapis.com/papyrus_images/f22da94a087fd6678ab599f270275d1545589ecdd6b98e4e32323b05d6f4c604.png)

接下来会执行call函数，开始message call。

执行之后，我们可以看到，stack和memory的内容都刷新了，实际上它们都开启了一个新的实例。

然后，需要从新的calldata（也就是刚才构造的calldata）中获取参数来执行setNumberExternal(uint256 newNumber1, uint256 newNumber2)的逻辑。

![](https://storage.googleapis.com/papyrus_images/5eecbac5e045cc16f3bd54e4524918441817ecfb55af5ad717c7a160a3fdbe42.png)

这里并不能直接定位到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结束。

![](https://storage.googleapis.com/papyrus_images/e35a0af57c5844b4b6f9d88ee9acecf04d18c2257f682f3b85e5d7cc4ceb12a3.png)

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

![](https://storage.googleapis.com/papyrus_images/bf45077acbadb86ebf5f9b3fa99f84e6dde78fc00a96b7f8a6b1f5e17d156725.png)

栈顶存储的应该是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函数调用只需要简单的指令跳转。

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/evm-internal-external)*
