以太坊智能合约字节码深入解析

智能合约编写好之后需要通过编译器编译后才能在虚拟机上运行,智能合约的编译结果称为字节码,字节码是一串十六进制数字编码的字节数组。字节码的解析是以一个字节为单位,每个字节都表示一个EVM指令或一个操作数据。我们通过一个简单的智能合约来分析智能合约字节码对应的汇编指令的操作。
智能合约的例子如下:

pragma solidity ^0.4.19; 
contract C {
    uint256 a; 
    function C() public { 
      a = 1; 
    }
        
    function Foo(uint256 _in) public { 
      a = _in;
    }
}

对应的字节码如下:

606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680631176bd96146044575b600080fd5b3415604e57600080fd5b606260048080359060200190919050506064565b005b80600081905550505600a165627a7a72305820889b48be07282eb533ea34e9be8dc6c8e79bf4e758d05ebc9fb3c2544e9f55ae0029

EVM依次读入这个字节码的十六进制数字"60 60 60 40 52 ...",首先解析第一个字节"60",从汇编指令集中查询该值对应的指令,60对应的指令是"push",因为push指令后需要一个输入参数,因此后面的字节"60"是该push指令的参数,然后继续解析第三个个"60"。上面提到的汇编指令集完整的列表如下:

opcodes = {
    0x00: ['STOP', 0, 0, 0], 
    0x01: ['ADD', 2, 1, 3],
    0x02: ['MUL', 2, 1, 5],
    0x03: ['SUB', 2, 1, 3],
    0x04: ['DIV', 2, 1, 5],
    0x05: ['SDIV', 2, 1, 5],
    0x06: ['MOD', 2, 1, 5],
    0x07: ['SMOD', 2, 1, 5],
    0x08: ['ADDMOD', 3, 1, 8],
    0x09: ['MULMOD', 3, 1, 8],
    0x0a: ['EXP', 2, 1, 10],
    0x0b: ['SIGNEXTEND', 2, 1, 5],
    0x10: ['LT', 2, 1, 3],
    0x11: ['GT', 2, 1, 3],
    0x12: ['SLT', 2, 1, 3],
    0x13: ['SGT', 2, 1, 3],
    0x14: ['EQ', 2, 1, 3],
    0x15: ['ISZERO', 1, 1, 3],
    0x16: ['AND', 2, 1, 3],
    0x17: ['OR', 2, 1, 3],
    0x18: ['XOR', 2, 1, 3],
    0x19: ['NOT', 1, 1, 3],
    0x1a: ['BYTE', 2, 1, 3],
    0x20: ['SHA3', 2, 1, 30],
    0x30: ['ADDRESS', 0, 1, 2],
    0x31: ['BALANCE', 1, 1, 20],  # now 400
    0x32: ['ORIGIN', 0, 1, 2],
    0x33: ['CALLER', 0, 1, 2],
    0x34: ['CALLVALUE', 0, 1, 2],
    0x35: ['CALLDATALOAD', 1, 1, 3],
    0x36: ['CALLDATASIZE', 0, 1, 2],
    0x37: ['CALLDATACOPY', 3, 0, 3],
    0x38: ['CODESIZE', 0, 1, 2],
    0x39: ['CODECOPY', 3, 0, 3],
    0x3a: ['GASPRICE', 0, 1, 2],
    0x3b: ['EXTCODESIZE', 1, 1, 20], # now 700
    0x3c: ['EXTCODECOPY', 4, 0, 20], # now 700
    0x3d: ['RETURNDATASIZE', 0, 1, 2],
    0x3e: ['RETURNDATACOPY', 3, 0, 3],
    0x40: ['BLOCKHASH', 1, 1, 20],
    0x41: ['COINBASE', 0, 1, 2],
    0x42: ['TIMESTAMP', 0, 1, 2],
    0x43: ['NUMBER', 0, 1, 2],
    0x44: ['DIFFICULTY', 0, 1, 2],
    0x45: ['GASLIMIT', 0, 1, 2],
    0x50: ['POP', 1, 0, 2],
    0x51: ['MLOAD', 1, 1, 3],
    0x52: ['MSTORE', 2, 0, 3],
    0x53: ['MSTORE8', 2, 0, 3],
    0x54: ['SLOAD', 1, 1, 50],  # 200 now
    # actual cost 5000-20000 depending on circumstance
    0x55: ['SSTORE', 2, 0, 0],
    0x56: ['JUMP', 1, 0, 8],
    0x57: ['JUMPI', 2, 0, 10],
    0x58: ['PC', 0, 1, 2],
    0x59: ['MSIZE', 0, 1, 2],
    0x5a: ['GAS', 0, 1, 2],
    0x5b: ['JUMPDEST', 0, 0, 1],
    0x60: ['PUSH1', 0, 1, 3],
    ......
    0x7f: ['PUSH32', 0, 1, 3],
    0x80: ['DUP1', 1, 2, 3],
    ......
    0x8f: ['DUP32', 16, 17, 3],
    0x90: ['SWAP1', 2, 2, 3],
    ......
    0x9f: ['SWAP32', 17, 17, 3],
    0xa0: ['LOG0', 2, 0, 375],
    0xa1: ['LOG1', 3, 0, 750],
    0xa2: ['LOG2', 4, 0, 1125],
    0xa3: ['LOG3', 5, 0, 1500],
    0xa4: ['LOG4', 6, 0, 1875],
    # 0xe1: ['SLOADBYTES', 3, 0, 50], # to be discontinued
    # 0xe2: ['SSTOREBYTES', 3, 0, 0], # to be discontinued
    # 0xe3: ['SSIZE', 1, 1, 50], # to be discontinued
    0xf0: ['CREATE', 3, 1, 32000],
    0xf1: ['CALL', 7, 1, 40],  # 700 now
    0xf2: ['CALLCODE', 7, 1, 40],  # 700 now
    0xf3: ['RETURN', 2, 0, 0],
    0xf4: ['DELEGATECALL', 6, 1, 40],  # 700 now
    0xf5: ['CALLBLACKBOX', 7, 1, 40],
    0xfa: ['STATICCALL', 6, 1, 40],
    0xfd: ['REVERT', 2, 0, 0],
    0xff: ['SUICIDE', 1, 0, 0],  # 5000 now
}

以操作码CALL为例, 说明其中的含义如下:

  • 0xf1表示操作码对应的数值,
  • 7表示输入参数的个数,即从栈中弹出的数据个数
  • 1表示输出参数的个数, 压回栈中的数据个数
  • 40表示执行该操作需要花费gas数量

根据该指令集,对于上面的字节码可以翻译出对应的汇编指令代码:

// 这一部分是创建合约时执行的汇编指令, 等合约部署完成后,不会再被执行 
.code
  PUSH 60           //压入0x60 stack=[0x60]
  PUSH 40           //压入0x40 stack=[0x60,0x40].
  MSTORE            //在memory空间中偏移64*32的位置写入值0x60, 实际上意味着开辟2k的存储空间
  CALLVALUE     //压入创建合约的这笔交易携带的eth数量,没有就压入0, stack=[value].
  ISZERO            //判断压入的value是否为0, 如果是0就压入1, 否则压入0: stack=[0x1/0x0].
  PUSH [tag] 1  //压入tag1 stack=[0x1/0x0,tag1].
  JUMPI             //如果没有value,跳转到tag1, 否则,继续往下执行 stack=[]
  PUSH 0            //stack=[0]
  DUP1            //stack=[0,0]
  REVERT            //该指令是metropolis hardfork之后才支持的, 该指令返回memory地址=0,size=0中的数据 stack=[]
tag 1           //这里是没有携带value的部署合约交易的执行入口, 实际上就是构造函数的执行代码
  JUMPDEST          //没有含义, 表示跳转的目标位置
  PUSH 1               
  PUSH 0               
  DUP2          
  SWAP1             
  SSTORE            
  POP            //以上都是 构造函数中 执行a = 1;的汇编指令, 详细分析见下文.

    //压入本合约字节码中.data段的长度, .data段是智能合约真正的字节码 //stack=[len]
  PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000        
  DUP1            //stack=[len,len]

  //压入.data段在本字节码中的偏移地址 stack=[len,len,offset]
  PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 
  PUSH 0            //压入合约字节码复制到memory的起始地址,即0, stack=[len,len,offset,0]
  
  //根据len和offset 复制下面.data开始的字节码到0地址开始的memory内存中
  CODECOPY      
  PUSH 0            //stack=[len,0]
  RETURN            //将memory的空间扩展到len(如果之前不足的话) 并返回部署后的字节码. stack=[]
.data  // 这一部分开始合约内容的汇编指令
  0:
    .code
      PUSH 60           
      PUSH 40           
      MSTORE            //同上分析, 开辟2k的memory空间
      PUSH 4            //stack=[4]
      CALLDATASIZE          //压入交易携带的calldata的数据长度 stack=[4,size]
      LT               //检查size是否小于4, 小于:压入1, 不小于: 压入0  stack=[0]
      PUSH [tag] 1      //stack=[0, tag1]
      JUMPI              //size小于4, 跳转到tag1, 否则继续
      PUSH 0              //stack=[0]
      CALLDATALOAD  //加载calldata中的前32个字节 stack=[calldata[0:32]]
      PUSH 100000000000000000000000000000000000000000000000000000000    
      SWAP1   //stack=[1000000..., calldata[0:32]]
      DIV       //calldata[0:32]/1000000...  表示取calldata[0:32]的前4个字节
      PUSH FFFFFFFF     // stack=[calldata[0:4], FFFFFFFF]
      AND            //确保calldata[0:4]的前28(32-4)字节是0  stack=[calldata[0:4]]
      DUP1          //stack=[calldata[0:4],calldata[0:4]]
            
            //压入Foo(uint256)的hash数据
      PUSH 1176BD96         //stack=[calldata[0:4],calldata[0:4],1176BD96]
      EQ             //比较是否相等, 相等:压入1; 不相等:压入0
      PUSH [tag] 2  //stack=[calldata[0:4],1,tag2]
      JUMPI             如果相等, 跳转到tag2, 否则继续 stack=[calldata[0:4]]
            
            //[注意]: 如果合约定义了多个函数, 会重复上面的流程, 挨个比较hash值,直到找到hash

    tag 1           // 合约函数调用时, calldata 长度小于4
      JUMPDEST          
      PUSH 0        
      DUP1          
      REVERT        

    tag 2           //函数Foo(uint256 _in)的参数检查
      JUMPDEST      
      CALLVALUE   //stack=[calldata[0:4],value]
      ISZERO            //检查value是否是0 stack=[calldata[0:4],0]
      PUSH [tag] 3  //stack=[calldata[0:4],0, tag3]
      JUMPI              //如果参数非0, 跳转到tag3 stack=[calldata[0:4]]
      PUSH 0            //否则, 返回, 撤销操作
      DUP1            //stack=[calldata[0:4],0,0]
      REVERT            
    tag 3           //函数Foo(uint256 _in)的代码实现, 中间的指令貌似有一些冗余 
      JUMPDEST          
      PUSH [tag] 4           //stack=[calldata[0:4],tag4]
      PUSH 4                 //stack=[calldata[0:4],tag4,4]
      DUP1             //stack=[calldata[0:4],tag4,4,4]
      DUP1             //stack=[calldata[0:4],tag4,4,4,4]
      CALLDATALOAD       //stack=[calldata[0:4],tag4,4,4,calldata[4:36]]
      SWAP1              //stack=[calldata[0:4],tag4,4,calldata[4:36],4]
      PUSH 20           //stack=[calldata[0:4],tag4,4,calldata[32:64],4,32]
      ADD            //stack=[calldata[0:4],tag4,4,calldata[32:64],0]
      SWAP1         //stack=[calldata[0:4],tag4,4,0,calldata[32:64]]    
      SWAP2         //stack=[calldata[0:4],tag4,calldata[32:64],0,4]        
      SWAP1         //stack=[calldata[0:4],tag4,calldata[32:64],4,0]    
      POP           //stack=[calldata[0:4],tag4,calldata[32:64],4]
      POP           //stack=[calldata[0:4],tag4,calldata[32:64]]
      PUSH [tag] 5  //stack=[calldata[0:4],tag4,calldata[32:64],tag5]
      JUMP          //跳转到tag5 stack=[calldata[0:4],tag4,calldata[32:64]]
    tag 4           function Foo(uint256 _in) publ...
      JUMPDEST          function Foo(uint256 _in) publ...
      STOP          function Foo(uint256 _in) publ...
    tag 5           //函数Foo(uint256 _in) 最终执行代码
      JUMPDEST          
      DUP1          //stack=[calldata[0:4],tag4,calldata[32:64],calldata[32:64]]
      PUSH 0    //stack=[calldata[0:4],tag4,calldata[32:64],calldata[32:64],0]
      DUP2      //stack=[calldata[0:4],tag4,calldata[32:64],calldata[32:64],0,calldata[32:64]]
      SWAP1     //stack=[calldata[0:4],tag4,calldata[32:64],calldata[32:64],calldata[32:64],0]
      SSTORE    //写入参到memory地址0位置: stack=[calldata[0:4],tag4,calldata[32:64],calldata[32:64]]
      POP       //stack=[calldata[0:4],tag4,calldata[32:64]]
      POP       //stack=[calldata[0:4],tag4]    
      JUMP [out]        //执行成功, 退出
    .data

为了方便理解, 将指令的执行过程以注释的方式写在代码中。根据注释,可以看出指令对数据的处理,包括栈,存储,内存中的数据的变化。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容