【スマートコントラクト】イーサリアムコントラクト実行解析

目次

キーワード:EVM実行エンジン、アセンブリ命令、オペコード、バイトコード

読者がこの記事が良いと思う場合は、著者の記事の最初のアドレスにアクセスして詳細を確認できます。

1. 基本的な考え方

1.1 EVM

EVM はスタックベースのビッグエンディアン仮想マシンです。この仮想マシンは VMware ではなく、JVM に似た仮想マシンであるため、JVM を理解するのと同じように EVM を理解することができます。

JVM と同様に、EVM も一連のカスタム命令セットをサポートするために実際のコンピュータ上に設計および作成されたコンピュータです。また、スタックと 2 つのストレージ ドメイン (メモリとストレージ) も含まれています。

はい、一連の命令セットをカスタマイズする場合は、通常、対応するアセンブリ言語を実装する必要があります。アセンブリの上には、Solidity、vyper など、開発者が使用する高級言語があります。

ただし、JVM とは異なり、EVM はさまざまな物理マシンに直接インストールでき、イーサリアム クライアントに組み込まれるように設計されており、イーサリアム システム上で実行されます。EVM の役割は、イーサリアム スマート コントラクトを実行することです。
コントラクトは外部アカウントのトランザクションを通じて作成され、コントラクトのバイトコードはトランザクションに添付されますdata同様に、トランザクションでは、コントラクトの呼び出しや破棄など、コントラクトとのさまざまな種類の対話を実行することもできますdata

1.2 コントラクトバイトコード

コントラクト バイトコードは一連の演算子 (命令とも呼ばれます) で構成されており、 PUSHnを除く任意の演算子をバイト リテラルにエンコードできます
EVM 命令セットは、 などの複数の PUSH 命令をサポートします。PUSH1PUSH2の数値は、スタックにプッシュされるデータ バイトのサイズを示し、PUSH1 はスタックに 1 バイトずつプッシュされるデータです。PUSHn はデータを運ぶため、
この命令が占有するスペースは可変です ( PUSH1 0x002 バイト、PUSH2 0X00103 バイトなどを占有する)。

1.3 契約施工者

コントラクトが正常に作成されると、そのコンストラクターはコントラクトから削除されます。つまり、コンストラクターはデプロイされたコントラクトに表示されなくなります。

1.4 契約のやり取り

コントラクトは、外部の世界がそれと対話できるようにするために、一部の ABI (アプリケーション バイナリ インターフェイス) を公開します。

1.5 通話データ

これは、コントラクトが呼び出されたときにトランザクションのフィールドに添付される情報でありdata
、通常は 4 バイトのメソッド識別子が含まれます。メソッド識別子の構築方法は次のとおりです。keccak256("somefunc(uint)uint")[:4]これは、関数の keccak256 ハッシュ後の最初の 4 バイトですサイン。

1.6 プログラムカウンター

カウンタはスタック、メモリ、ストレージと連携してコントラクト バイトコードの実行を完了します。

カウンタの本質は、実行ポインタとして理解できる展開されたオペコード シーケンスのオフセットであり、カウンタが指す位置は次の命令が実行される位置です。以下はオペコードの短いシーケンスです (理解できない場合は、最初にテキストを読んでから戻ってください)。

// 这段操作码序列的总offset为2+2+1=5,其中PUSH1指令占用1byte,指令后的数据占用1字节,JUMPI占用1byte
PUSH1 2 // offset=0,当指针指向offset=0时,表示接下来执行这一行指令。下一个指令的offset=这个指令的offset+这个指令占用的字节数
PUSH1 5 // offset=2
JUMPI    // offset=4,JUMPI实现条件跳转,首先依次取出栈顶2个元素5 2,判断第二个元素(2)是否为0,若不是就跳转到offset=第一个元素(5)的位置,那么就是0x05的位置
         // 若第二个元素是0,则指针自增,继续向下执行
JUMPDEST // offset=5,这个指令是标识此处作为一个跳转着陆点,跳转指令JUMP和JUMPI都必须以此作为着陆点,否则不能跳转,执行报错。

1.7 実行環境(コンテキスト)

EVM がコントラクト バイトコードの実行を開始すると、コントラクト用の一時的で独立したコンテキストが作成されます。具体的には、それぞれ異なる目的を持つ複数の個別のメモリ領域が作成されます。

  • コード領域: コントラクト バイトコードを格納する静的な読み取り専用領域。それを読んで指示することができCODESIZECODECOPY他の契約のコードを読んで指示することができEXTCODESIZE
    ますEXTCODECOPY
  • スタック: 32 バイトの要素と 1024 の容量 (長さ) を持つ配列スペースで、EVM 命令に必要なパラメータと返された結果を格納するために使用されます。命令は、スタックの最上位から始まるスタック要素にのみアクセスできます。通常、命令はPUSH1, DUP1, SWAP1, POP
    スタック上で動作します。
  • メモリ: シングルバイト要素の配列空間であり、コントラクトの実行中に一時的なデータを保存するために使用されます。メモリ空間にはバイト オフセットによってアクセスされます。通常、命令はMLOAD, MSTORE, MSTORE8
    メモリ上で動作します (命令の前に M プレフィックスが付いていることがわかります)。
  • ストレージ: 前の 2 つの構造とは異なり、永続データを保存するためのマップ構造であり、キーと値は両方とも uint256 型です。通常、このコマンドはSLOAD, SSTORE
    ストレージを操作します (コマンドの前に S 接頭辞が付いています)。
  • calldata: トランザクション発生時に付加されるデータであり、静的な読み取り専用領域です。たとえば、コントラクトが作成されるとき、calldata の内容はコンストラクター コードです。通常、コマンドでCALLDATALOAD, CALLDATASIZE, CALLDATACOPY
    読み取ることができます。
  • return data: コントラクトの戻り値が格納される領域です。命令によって変更したりRETURN, REVERTRETURNDATASIZE, RETURNDATACOPY命令によって読み取ることができます。

1.8 オペコード (オペコード/EVM 命令/ニーモニック)

これは EVM 用に調整された一連の命令セットであり、算術演算、論理演算、ビット演算、条件付きジャンプなどの機能をサポートしており、チューリング完全言語です。その上に構築される堅牢性などの言語はもちろんチューリング完全言語です。

OpCode はオペコード/EVM アセンブリ命令/ニーモニック (ニーモニック) と呼ばれることがあり、その役割は人々がコード ロジックを読みやすくすることです。コントラクトの最終的なコンパイルと展開の結果は、一連の OpCode と操作データで構成されます。
EVM は、コンパイルされたバイトコード シーケンスから OpCode を 1 つずつ抽出して実行することによってコントラクト ロジックを実行します。特定の OpCode の実行に失敗した場合 (パラメータやガスが不十分な場合など)、EVM はすべての変更を元に戻します。

公式文書では、OpCode という名前がより頻繁に使用されるため、読者はクエリの際にキーワードとして OpCode を使用できます。

すべての OpCode がガスを消費するわけではなく、一部の OpCode はガスを返します。ガスを返す操作は 2 つ知られており、1 つはコントラクトの破棄 (24000 ガスを返す)、もう 1 つはストレージをクリアする (15000 ガスを返す) です。
ただし、契約が締結された時点では、ガスの返却は別の返金カウンターにまだ存在しており、ガス残高が直接増加するわけではなく、後でガスが不足した場合でも元に戻されることに注意してください。つまり、返却されたばかりのガスは契約実行時には使用できず、
実行完了後にのみ口座に返却できることになります。最後に、返されるガスの量は、トランザクション実行のために消費されたガスの半分を超えてはなりません。つまり、消費されたガスの少なくとも半分がマイナーに支払われる必要があります。

現在 140 を超える OpCode があり、命令を除いて、それぞれを 1 バイトにエンコードできます (データと結合してバイトコードを形成します)。これは、命令が任意PUSH
の長さのデータを運ぶことができるためです (スタックから取得するのではなく、書き込み時に実行します)。コード) 固定されています)、通常、表示されるのはPUSH1またはPUSH2
コマンドの後の数字は、伝送されるデータのバイト長を示します。現在、 ~ PUSH コマンド
があります。各 OpCode に必要なパラメータはスタックから取得され (スタック入力)、計算結果はスタックにプッシュされます (スタック出力)。PUSH1PUSH32

各 OpCode は 1 バイト サイズにエンコードする必要があるため、最大 256 個の命令を設計できます (1 バイトの 10 進数の範囲は 0 ~ 255、16 進数の範囲は 0x00 ~ 0xFF、つまり上) 〜 256 の異なる値を表現できます)。

1.9 ガス消費量

トランザクション実行のためのリソースを提供するインセンティブとして、一定量のイーサがマイナーに支払われます。この金額は、送信金額とトランザクションを完了するために必要な作業量という 2 つの要素によって決まります。

ガス料金は 2 つのタイプに分けられます: 固定料金と動的料金です。固定料金は、特定の操作に対してイーサリアム プラットフォームによって設定されます。たとえば、単純な送金トランザクションは固定コストで 21000 ガスを消費します。動的料金は次に従って計算されます。次の式:

gas_price * gas_limit = total max gas costs

これら 2 つの変数の値は、トランザクションの開始者によって設定されます。Gas_price は、ガス 1 単位のイーサ価格 (gas_price=10wei など) であり、wei
イーサ通貨の単位であるため、ここでは詳しく説明しません。Gas_limit は、トランザクションを開始するユーザーがトランザクションの実行に対して支払うガスの最大量です。
これらのガスは常に消費されるわけではなく、トランザクションの完了後に未消費のガスがトランザクション開始者に返されます。設定されたガスの総量がトランザクションの完了をサポートするのに十分でない場合、トランザクションは失敗するだけでなく、消費されたガスは返金されません。

コントラクトバイトコードを伴うトランザクションのガス料金構成: トランザクション自体には 21000gas + OpCode のガス料金がかかります。

このうち、OpCode のガス料金は 2 つのタイプに分けられ、1 つは固定料金、もう 1 つは動的料金で、通常はコマンドに必要なパラメーターのサイズまたは数量によって決定され、evm.codes でクエリできます
。詳細。

1.10 契約締結プロセス

EVM がコントラクトを実行する一般的なプロセスを理解する必要があります。

  • EVM で実行される各命令は OpCode (オペレーション コード) と呼ばれます。
  • コントラクトが実行されると、コンパイルされたバイトコードは、EVM によって読み取り可能なオペコードと操作データに変換されてから実行されます。

  • まず、プログラム カウンタ (PC、レジスタに似ています) がコントラクト バイトコードから 1 を読み取り、オペコードに対応する演算関数やガス コストなどの情報を JumpTable (長さ 256 の配列) から取得します。ガスを差し引いて (オペコードが動的ガス コストの場合は計算する必要があります)、十分であればオペコードを実行し、十分でない場合はガスを完全に差し引いて、実行された命令をロールバックします。(命令によってはスタック、メモリ、StateDB上で動作する場合があります)
  • そして、このオペコードが正常に実行されると、プログラム カウンタがインクリメントされ、次のループに入ります (次の命令が実行されます)。

具体的な実行処理はforループの中にありますが、ここではgeth実行コントラクトの関数コードEVMInterpreter.Run()をそのまま貼り付けていますので、コメントと合わせてお読みください。

コードが長いので、展開して表示します
 
 
// 这是EVM执行合约的核心方法
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
    
    
  // 省略部分代码
  
  // 初始化执行合约所需的各种变量,其中就有stack、memory、pc等对象
  var (
      op          OpCode // 当前执行的操作码,会在下面的for循环执行时不断变化
      mem = NewMemory()  // bound memory,内部初始化一个包含[]byte的结构体
      stack = newstack() // local stack,内部初始化一个[]uint256数组
      callContext = &ScopeContext{
    
     // 属于当前合约的执行上下文
        Memory:   mem,
        Stack:    stack,
        Contract: contract,
      }
      // For optimisation reason we're using uint64 as the program counter.
      // It's theoretically possible to go above 2^64. The YP defines the PC
      // to be uint256. Practically much less so feasible.
      pc = uint64(0) // program counter 程序计数器
      cost uint64
      // copies used by tracer
      pcCopy  uint64 // needed for the deferred EVMLogger
      gasCopy uint64 // for EVMLogger to log gas remaining before execution
      logged  bool   // deferred EVMLogger should ignore already logged steps
      res     []byte // result of the opcode execution function,当前调用返回值
  )
 

  for {
    
    
      if in.cfg.Debug {
    
    
          // Capture pre-execution values for tracing.
          logged, pcCopy, gasCopy = false, pc, contract.Gas
      }
      // Get the operation from the jump table and validate the stack to ensure there are
      // enough stack items available to perform the operation.
      // 根据程序计数器读取下一个要执行的操作码,实际是个byte类型
      op = contract.GetOp(pc)
      operation := in.cfg.JumpTable[op]  // 从数组中找到对应的操作对象
      cost = operation.constantGas // For tracing (操作对应的固定gas成本)
      // Validate stack(提前检查栈内元素个数是否足够本次操作所需)
      if sLen := stack.len(); sLen < operation.minStack {
    
    
          return nil, &ErrStackUnderflow{
    
    stackLen: sLen, required: operation.minStack}
      } else if sLen > operation.maxStack {
    
    
          return nil, &ErrStackOverflow{
    
    stackLen: sLen, limit: operation.maxStack}
      }
      // 扣减固定gas部分
      if !contract.UseGas(cost) {
    
    
          return nil, ErrOutOfGas
      }
      // 这个操作是否动态gas成本(根据参数长度或个数来决定扣减的gas数额)
      if operation.dynamicGas != nil {
    
    
          // All ops with a dynamic memory usage also has a dynamic gas cost.
          var memorySize uint64
          // calculate the new memory size and expand the memory to fit
          // the operation
          // Memory check needs to be done prior to evaluating the dynamic gas portion,
          // to detect calculation overflows
          if operation.memorySize != nil {
    
    
              memSize, overflow := operation.memorySize(stack)
              if overflow {
    
    
                  return nil, ErrGasUintOverflow
              }
              // memory is expanded in words of 32 bytes. Gas
              // is also calculated in words.
              if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
    
    
                  return nil, ErrGasUintOverflow
              }
          }
          // Consume the gas and return an error if not enough gas is available.
          // cost is explicitly set so that the capture state defer method can get the proper cost
          // 计算动态gas成本
          var dynamicCost uint64
          dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize)
          cost += dynamicCost // for tracing
          // 扣减动态gas部分
          if err != nil || !contract.UseGas(dynamicCost) {
    
    
              return nil, ErrOutOfGas
          }
          // Do tracing before memory expansion
          if in.cfg.Debug {
    
    
              in.cfg.Tracer.CaptureState(pc, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err)
              logged = true
          }
          if memorySize > 0 {
    
    
              mem.Resize(memorySize)
          }
      } else if in.cfg.Debug {
    
    
          in.cfg.Tracer.CaptureState(pc, op, gasCopy, cost, callContext, in.returnData, in.evm.depth, err)
          logged = true
      }
      // execute the operation (执行操作,pc=程序计数器,in是EVM引擎包含有账本DB对象,callContext包含栈和memory对象)
      res, err = operation.execute(&pc, in, callContext)
      if err != nil {
    
    
          break
      }
      pc++ // 计数器自增,准备执行下一条指令(注意pc在执行操作时可能已经改变。意思是如果又从字节码中提取了数据,则计数器要继续往右移,移动长度等于数据长度)
  }
}

2. プロセスの詳細な説明

私たちが作成した Solidity コードは、リミックスまたは sloc や slocjs などのローカル コンパイラーを通じて対応するアセンブリ コードにコンパイルし、マシンによって実行される純粋な 16 進文字コードに変換できます。

  1. Remix のアセンブリ コードとバイトコードは、[Solidity Compiler --> CompileDetails] パスで確認できます。
  2. solc コンパイラをローカルにダウンロードし、コンパイラを使用してコードをコンパイルし、アセンブリ コードとバイトコードを取得することもできます。

フル機能の cpp 実装をサポートする solc コンパイラーをダウンロードし (推奨)、公式インストール ガイド
を確認して、いくつかの機能をサポートする solcjs をダウンロードします: npm install -g solc

これは、次の簡単なコードで示されています。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {
  address _owner;
  uint abc = 0;
  constructor() {
    _owner = msg.sender;
  }
  function set_val(uint _value) public {
    abc = _value;
  }
}

これは、カスタム ロジックの実装に使用される人間が読み取り可能な Solidity コードです。マシンの実行を容易にするために、低レベルのアセンブリ コード (オペコードとも呼ばれます) にコンパイルされ、マシンによって 16 進コードに変換される必要があります。
アセンブリ コードは、CPU 実行層に最も近いコード形式と考えることができ、アセンブリ コードを通じて、関数がどのアセンブリ命令を使用しているかなど、アセンブリ層の Solidity コードの実際のパフォーマンスをより明確に確認できます。これは
、特にデバッグ段階でのトラブルシューティングに非常に役立ちます。以下は、Solidity をコンパクトなオペコード シーケンス形式に変換します。

// 请先下载solc编译器到本地
// solc -o learn_bytecode --opcodes 0x00_learn_bytecode.sol  
// 生成文件learn_bytecode/Example.opcode
PUSH1 0x80 PUSH1 0x40 MSTORE ...省略

オペコード シーケンスは完全に EVM 命令とデータで構成され、すべての命令とデータが線形に配置されます。

説明する例として、サンプル コントラクトのオペコード シーケンスの前の部分を取り上げます。PUSH1 0x80 PUSH1 0x40 MSTORE

  • まず、オペコードはバイトコードではなく、依然として読み取ることができますが、バイトコードは完全に 0128asdasda9s87d98asd などの読み取り不可能な 16 進文字の文字列であり、各オペコードはバイトに変換できます。
  • PUSH1 0x80 PUSH1 0x400x80 の 1 バイトがスタックにプッシュされ、その後に 0x40 がプッシュされることを示します (スタック要素は最大 32 バイトまたは 256 ビットであることに注意してください)。
  • MSTOREこの命令は、EVM の一時メモリに値を保存する操作です。2 つのパラメータを受け取ります。最初のパラメータは値を保存するために使用されるメモリ アドレスで、2 番目のパラメータは保存される値です。この命令は注意してください。規則に従ったパラメータです
    (外部入力ではなく) スタックから取得されるため、ここでのロジックは MSTORE 0x40 0x80 (値 0x80 をアドレス 0x40 に格納します)
  • その他の命令の意味については、以下の命令セット表をご確認ください。

オペコード シーケンスは、コードを読み取るのに役立ちません。したがって、アセンブリ コードを 1 行ずつ生成する必要があります。

// solc -o learn_bytecode --asm 0x00_learn_bytecode.sol   生成learn_bytecode/Example.evm
  /* "0x00_learn_bytecode.sol":57:241  contract Example {... */
  mstore(0x40, 0x80)
  /* "0x00_learn_bytecode.sol":111:112  0 */
  0x00
  /* "0x00_learn_bytecode.sol":100:112  uint abc = 0 */
  0x01
  sstore
  /* "0x00_learn_bytecode.sol":118:168  constructor() {... */
  callvalue
  ...省略
  dataOffset(sub_0)
  0x00
  codecopy
  0x00
  return
stop

sub_0 : assembly {
  ...
  auxdata : 0xa264697066735822122035b90a279bfd69292250dbe6e9f45c70ac30c03c0f50b99a887b24d9b292edce64736f6c63430008110033
}

このコードはsub_0 assembly境界、その上のコードがデプロイメント コード、sub_0 領域のコードがランタイム コード
です。上記のコードのコメントによれば、アセンブリ コードは Solidity コードに対して比較的明確に読み取ることができます。
最後のフィールドauxdata
は、Solidity バージョンなどのコントラクト メタデータを記述するために使用される CBOR エンコードされたバイナリ値です。詳細については、「メタデータ」
を参照してください。

2.1 コードのデプロイについて

名前が示すように、デプロイ時に実行されます。主に次の 3 つのタスクを実行します。

  1. Payable チェック。コントラクト コンストラクターが Payable を宣言していない場合、デプロイメント中に ether を注入するとデプロイメントが失敗します。
  2. コンストラクターを実行し、宣言された状態変数を初期化します。
  3. ランタイム コードのバイトコードを計算し、それを EVM に返し、StateDB に保存します (キーはコントラクト アドレス)。

2.2 ランタイムコード

デプロイ後に外部呼び出しを受信したときに実行されます。solcランタイムコードはデプロイメントコードの実行後に計算されますが、その計算方法は固定ロジックのため、コンパイラツールで直接生成することも可能です。

2.3 最終的なバイトコード

最後のバイトコードは、dataコントラクト デプロイメントのトランザクション フィールドで伝送されるデータであり、16 進文字の長い文字列であり、次の方法で生成されます。

//solc -o learn_bytecode --bin 0x00_learn_bytecode.sol
6080604052600060015534801561001557600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060e3806100646000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634edd148314602d575b600080fd5b60436004803603810190603f91906085565b6045565b005b8060018190555050565b600080fd5b6000819050919050565b6065816054565b8114606f57600080fd5b50565b600081359050607f81605e565b92915050565b6000602082840312156098576097604f565b5b600060a4848285016072565b9150509291505056fea264697066735822122035b90a279bfd69292250dbe6e9f45c70ac30c03c0f50b99a887b24d9b292edce64736f6c63430008110033

最終的なバイトコードも、固定形式 (デプロイメント コード + ランタイム コード + 補助データ) に従って生成されます。

このうち、ランタイムコード + auxdata は以下のように生成できます。

//solc -o learn_bytecode --bin -runtime 0x00_learn_bytecode.sol
6080604052348015600f57600080fd5b506004361060285760003560e01c80634edd148314602d575b600080fd5b60436004803603810190603f91906085565b6045565b005b8060018190555050565b600080fd5b6000819050919050565b6065816054565b8114606f57600080fd5b50565b600081359050607f81605e565b92915050565b6000602082840312156098576097604f565b5b600060a4848285016072565b9150509291505056fea264697066735822122035b90a279bfd69292250dbe6e9f45c70ac30c03c0f50b99a887b24d9b292edce64736f6c63430008110033

2.4 EVM用に設計された命令セット

イーサリアム開発者は、EVM 用の一連の命令セット、つまり前述の OpCode (OpCode) を特別に設計しました。これについては、以前に簡単に紹介しましたが、ここで詳しく紹介します。

命令セット コードは、コード実行エンジンがサポートするアセンブリ命令セット内の一連の命令と操作対象のデータで構成されます。一般に、異なる実行エンジンでサポートされる命令セットは異なり、命令セットはコード実行エンジンによってカスタマイズできます。エンジン開発者。EVM 実行エンジンは OpCode に基づいて実行される
ため、OpCode は EVM 命令セットとも呼ばれ、完全な EVM 命令セット テーブルはここでクエリできます。

また、次のような簡単な図も示します。

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-rA5qqFz4-1677507590202)(./images/ethereum_opcodes_example.jpg)]

読みやすいように、表形式での翻訳は次のとおりです。

uint8 ニモニック スタック入力 スタック出力 表現 ノート
翻訳: バイトコード コマンド名 命令実行に必要なスタック要素 命令の実行後にスタックに書き込まれる要素 表現 述べる
00 ストップ なし なし ストップ() 契約の実行を停止する
01 追加 スタックの一番上/a/b/スタックの一番下 /a+b/ a+b スタックの上位 2 つの int256 または uint256 要素に対して加算演算を実行します。

テーブルの StackInput 列の要素の配置順序は、スタックの先頭から始まることに注意してください。これを例で説明します。たとえば、減算演算命令がテーブルの StackInput にあるSUB
場合/a/b/、次のコード:

// EVM指令交互平台:https://www.evm.codes/playground
PUSH 1 (代表b)
PUSH 2 (代表a)
SUB  // 减法运算

この命令の出力は、FFFFFF... (-1 の uint256 形式) ではなく 1 です。

EVM には現在、合計 140 以上の命令があります。一部の命令のパラメータの数は固定されていないことに注意してください。簡単にするために、すべてのオペコードを次のカテゴリに分類できます (セクションをリストします)。

  • スタック操作オペコード (POP、PUSH、DUP、SWAP)
  • 算術/比較/ビットごとのオペコード (ADD、SUB、GT、LT、AND、OR)
  • 環境オペコード (CALLER、CALLVALUE、NUMBER)
  • メモリ操作オペコード (MLOAD、MSTORE、MSTORE8、MSIZE)
  • ストア操作のオペコード (SLOAD、SSTORE)
  • プログラムカウンター関連のオペコード (JUMP、JUMPI、PC、JUMPDEST)
  • 停止オペコード (STOP、RETURN、REVERT、INVALID、SELFDESTRUCT)

リーダーは、命令セット テーブルと、evm.codes内の対応する命令によって消費されるガスの量をクエリできます。
同時に、この Web サイトは、オンライン オペコード プログラミング、オペコード、バイトコード、およびソリッドティ コード間のリアルタイム変換もサポートしています。

Ethereum でサポートされている命令セットの最も正確なリストは、
Geth のソース コードで照会する必要があります。このリンクは、
Geth の v1.10.26 バージョンの命令セットに関連する Go コードを指します。

2.5 上記の組み立て手順を詳しく説明します

コードが長いので、展開して表示します
 
 
    /* "0x00_learn_bytecode.sol":57:241  contract Example {... */
mstore(0x40, 0x80)   // 将0x80这个值存入Memory中0x40的位置,这表示在Memory中开辟0x80这么多的空间以供临时使用,单位字节,转换一下就是 8x16^1+0x16^0=128Byte
/* "0x00_learn_bytecode.sol":111:112  0 */
0x00 // 将0x00入栈(省略PUSH)
/* "0x00_learn_bytecode.sol":100:112  uint abc = 0 */
0x01 // 将0x01入栈(省略PUSH)
sstore // 将0x00这个值存入storage中0x01的位置,即storage[0x01] = 0x00
/* "0x00_learn_bytecode.sol":118:168  constructor() {... */
callvalue // 将本次调用注入的以太币数量入栈,没有就是0 (不管是创建合约,还是调用合约都是一次消息调用,都可注入以太币)
dup1      // 复制栈顶的数值,即为本次调用注入的以太币数量,此时的栈中元素情况:[栈顶, 0, 0] ,这里假设注入0以太币。
iszero    // 取出栈顶的值并判断是否为0,若是则入栈1,否则入栈0,stack: [栈顶,1,0,0]
tag_1     // tag_1 入栈,stack:[栈顶,tag_1,1,0,0]。 注:tag_1只是汇编指令中的语法,并非EVM指令,通常标识一个函数起点,本质上是一个操作码序列的offset。
jumpi     // 取出栈顶2个元素,即1,tag_1,判断第二接近栈顶的元素若非0,则跳转到tag_1位置,否则向下执行。显然这里会跳转tag_1
0x00      // 若不跳转,则到达这一行,入栈0x00,stack:[栈顶,0,tag_1,1,0,0]
dup1      // 复制栈顶元素,stack:[栈顶,0,0,tag_1,1,0,0]
revert    // 回退操作,这将中断执行,并且回滚所有之前的逻辑。
// 段注释:从callvalue到revert这一段表示往合约地址发送以太币,将会导致执行回退(因为没有给构造函数添加payable标识),简称payable检查。

tag_1:    // 这里开始表示constructor()内部的逻辑,即 _owner = msg.sender
pop   // 弹出并丢弃一个栈顶元素
/* "0x00_learn_bytecode.sol":151:161  msg.sender */
caller // 将msg.sender 入栈
/* "0x00_learn_bytecode.sol":142:148  _owner */
0x00   // 0x00 入栈
dup1   // 复制 0x00
/* "0x00_learn_bytecode.sol":142:161  _owner = msg.sender */
0x0100 // 入栈 0x0100
exp    // 指数运算,取出2个栈顶元素,0x0100^0x00 = 1,此时stack:[栈顶,1,0,msg.sender]
dup2   // 复制栈顶下面一个元素,stack:[栈顶,0,1,0,msg.sender]
sload  // 从storage中取出key为栈顶元素的value,并入栈,stack:[栈顶,storage[0x00],1,0,msg.sender]
dup2   // stack:[栈顶,1,storage[0x00],1,0,msg.sender]
0xffffffffffffffffffffffffffffffffffffffff // stack:[栈顶,0xffffffffffffffffffffffffffffffffffffffff,1,storage[0x00],1,0,msg.sender]
mul   // 下面一段指令比较简单,不再注释
not
and
swap1
dup4
0xffffffffffffffffffffffffffffffffffffffff
and
mul
or
swap1
sstore
pop
/* "0x00_learn_bytecode.sol":57:241  contract Example {... */
dataSize(sub_0)  // 这不是一种指令,可以理解为"PUSH dataSize(sub_0)"的缩写,意思是将下面的sub_0代码块的size压入栈顶
dup1      // 复制栈顶的size值
dataOffset(sub_0) // 与dataSize类似,这理解为"PUSH dataOffset(sub_0)"的缩写,意思是将下面的sub_0代码块的offset压入栈顶
0x00      // 0x00入栈,假设size为0x36,offset=0x1c,则stack: [栈顶,0,0x1c,0x36,0x36]
codecopy  // codecopy(destOffset,offset,size),这个指令消费三个栈元素,所以它的作用是从code区offset(0x1c)的位置复制一段字节大小为size(0x36)的数据到Memory区的destOffset(0x00)位置
0x00      // 入栈 0x00,stack:[栈顶, 0, 0x36]
return // return指令含义是本次调用执行结束,并消费2个栈元素,返回Memory区offset为0x00,size为0x36的一段数据
stop // 停止执行
// 段注释:从tag_1到stop都是constructor的逻辑,主要工作是状态变量的初始化(_owner= msg.sender)以及复制并返回sub_0区域的代码。

sub_0 : assembly {
/* "0x00_learn_bytecode.sol":57:241  contract Example {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1 :
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0x4edd1483
eq
tag_3
jumpi
tag_2 :
0x00
dup1
revert
/* "0x00_learn_bytecode.sol":173:239  function set_val(uint _value) public {... */
tag_3 :
tag_4
0x04
dup1
calldatasize
sub
dup2
add
swap1
tag_5
swap2
swap1
tag_6
jump    // in
tag_5 :
tag_7
jump    // in
tag_4 :
stop
tag_7:
/* "0x00_learn_bytecode.sol":226:232  _value */
dup1
/* "0x00_learn_bytecode.sol":220:223  abc */
0x01
/* "0x00_learn_bytecode.sol":220:232  abc = _value */
dup2
swap1
sstore
pop
/* "0x00_learn_bytecode.sol":173:239  function set_val(uint _value) public {... */
pop
jump    // out
/* "#utility.yul":88:205   */
tag_10:
/* "#utility.yul":197:198   */
... 省略一长段 utility.yul 代码
auxdata : 0xa264697066735822122035b90a279bfd69292250dbe6e9f45c70ac30c03c0f50b99a887b24d9b292edce64736f6c63430008110033
}

あるいは、イーサリアム クライアント geth がインストールされている場合は、evm コマンドを使用して、learn_bytecode/Example.binオフセットによってバイトコード ファイルを読み取り可能なオペコードに変換できます。

コードが長いので、展開して表示します
 
 
lei@WilldeMacBook-Pro test_solidity % evm disasm learn_bytecode/Example.bin
6080604052600060015534801561001557600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060e3806100646000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634edd148314602d575b600080fd5b60436004803603810190603f91906085565b6045565b005b8060018190555050565b600080fd5b6000819050919050565b6065816054565b8114606f57600080fd5b50565b600081359050607f81605e565b92915050565b6000602082840312156098576097604f565b5b600060a4848285016072565b9150509291505056fea264697066735822122035b90a279bfd69292250dbe6e9f45c70ac30c03c0f50b99a887b24d9b292edce64736f6c63430008110033
00000: PUSH1 0x80
00002: PUSH1 0x40
00004: MSTORE
00005: PUSH1 0x00
00007: PUSH1 0x01
00009: SSTORE
0000a: CALLVALUE
0000b: DUP1
0000c: ISZERO
0000d: PUSH2 0x0015
00010: JUMPI
00011: PUSH1 0x00
00013: DUP1
00014: REVERT
00015: JUMPDEST
00016: POP
00017: CALLER
00018: PUSH1 0x00
0001a: DUP1
0001b: PUSH2 0x0100
0001e: EXP
0001f: DUP2
00020: SLOAD
00021: DUP2
00022: PUSH20 0xffffffffffffffffffffffffffffffffffffffff
00037: MUL
00038: NOT
00039: AND
0003a: SWAP1
0003b: DUP4
0003c: PUSH20 0xffffffffffffffffffffffffffffffffffffffff
00051: AND
00052: MUL
00053: OR
00054: SWAP1
00055: SSTORE
00056: POP
00057: PUSH1 0xe3
00059: DUP1
0005a: PUSH2 0x0064
0005d: PUSH1 0x00
0005f: CODECOPY
00060: PUSH1 0x00
00062: RETURN
00063: INVALID
00064: PUSH1 0x80
00066: PUSH1 0x40
00068: MSTORE
00069: CALLVALUE
0006a: DUP1
0006b: ISZERO
0006c: PUSH1 0x0f
0006e: JUMPI
0006f: PUSH1 0x00
00071: DUP1
00072: REVERT
00073: JUMPDEST
00074: POP
00075: PUSH1 0x04
00077: CALLDATASIZE
00078: LT
00079: PUSH1 0x28
0007b: JUMPI
0007c: PUSH1 0x00
0007e: CALLDATALOAD
0007f: PUSH1 0xe0
00081: SHR
00082: DUP1
00083: PUSH4 0x4edd1483
00088: EQ
00089: PUSH1 0x2d
0008b: JUMPI
0008c: JUMPDEST
0008d: PUSH1 0x00
0008f: DUP1
00090: REVERT
00091: JUMPDEST
00092: PUSH1 0x43
00094: PUSH1 0x04
00096: DUP1
00097: CALLDATASIZE
00098: SUB
00099: DUP2
0009a: ADD
0009b: SWAP1
0009c: PUSH1 0x3f
0009e: SWAP2
0009f: SWAP1
000a0: PUSH1 0x85
000a2: JUMP
000a3: JUMPDEST
000a4: PUSH1 0x45
000a6: JUMP
000a7: JUMPDEST
000a8: STOP
000a9: JUMPDEST
000aa: DUP1
000ab: PUSH1 0x01
000ad: DUP2
000ae: SWAP1
000af: SSTORE
000b0: POP
000b1: POP
000b2: JUMP
000b3: JUMPDEST
000b4: PUSH1 0x00
000b6: DUP1
000b7: REVERT
000b8: JUMPDEST
000b9: PUSH1 0x00
000bb: DUP2
000bc: SWAP1
000bd: POP
000be: SWAP2
000bf: SWAP1
000c0: POP
000c1: JUMP
000c2: JUMPDEST
000c3: PUSH1 0x65
000c5: DUP2
000c6: PUSH1 0x54
000c8: JUMP
000c9: JUMPDEST
000ca: DUP2
000cb: EQ
000cc: PUSH1 0x6f
000ce: JUMPI
000cf: PUSH1 0x00
000d1: DUP1
000d2: REVERT
000d3: JUMPDEST
000d4: POP
000d5: JUMP
000d6: JUMPDEST
000d7: PUSH1 0x00
000d9: DUP2
000da: CALLDATALOAD
000db: SWAP1
000dc: POP
000dd: PUSH1 0x7f
000df: DUP2
000e0: PUSH1 0x5e
000e2: JUMP
000e3: JUMPDEST
000e4: SWAP3
000e5: SWAP2
000e6: POP
000e7: POP
000e8: JUMP
000e9: JUMPDEST
000ea: PUSH1 0x00
000ec: PUSH1 0x20
000ee: DUP3
000ef: DUP5
000f0: SUB
000f1: SLT
000f2: ISZERO
000f3: PUSH1 0x98
000f5: JUMPI
000f6: PUSH1 0x97
000f8: PUSH1 0x4f
000fa: JUMP
000fb: JUMPDEST
000fc: JUMPDEST
000fd: PUSH1 0x00
000ff: PUSH1 0xa4
00101: DUP5
00102: DUP3
00103: DUP6
00104: ADD
00105: PUSH1 0x72
00107: JUMP
00108: JUMPDEST
00109: SWAP2
0010a: POP
0010b: POP
0010c: SWAP3
0010d: SWAP2
0010e: POP
0010f: POP
00110: JUMP
00111: INVALID
00112: LOG2
00113: PUSH5 0x6970667358
00119: opcode 0x22 not defined
0011a: SLT
0011b: KECCAK256
0011c: CALLDATALOAD
0011d: opcode 0xb9 not defined
0011e: EXP
0011f: opcode 0x27 not defined
00120: SWAP12
00121: REVERT
00122: PUSH10 0x292250dbe6e9f45c70ac
0012d: ADDRESS
0012e: opcode 0xc0 not defined
0012f: EXTCODECOPY
00130: opcode 0xf not defined
00131: POP
00132: opcode 0xb9 not defined
00133: SWAP11
00134: DUP9
incomplete push instruction at 309

上記のオペコードシーケンスの左側は、対応する右側の命令のコード領域内のオフセットを 16 進数で表したもので、実際にはいわゆるコード領域です。と同様CODECOPYJUMP
命令内の offset パラメータと dest パラメータはすべて、コード領域内の対応する命令のオフセットを参照します。
EVM は、この一連のオペコードを上から下に順番に実行します。 命令に遭遇するとJUMPRETURN
ジャンプするか、実行を中断します。たとえば、上記のオペコード シーケンスが初めて実行されるときは、最大でも、次の命令に00062対応する RETURN 命令まで実行されます。この行は完了です。
このオペコード シーケンスは厳密にはデプロイメント コードであるため、デプロイメント中にのみ実行されます。デプロイメント後、このコードはリターン後にオペコード シーケンスを返し、00062EVM によって保存され、コントラクトが呼び出されたときに実行されます。外部的に。


参考

おすすめ

転載: blog.csdn.net/sc_lilei/article/details/129251495