【Contrato inteligente】Análisis de ejecución de contratos de Ethereum

Tabla de contenido

Palabras clave : motor de ejecución EVM, instrucciones de ensamblaje, códigos de operación, códigos de bytes

Si los lectores piensan que este artículo es bueno, pueden ir a la primera dirección del artículo del autor para obtener más información.

1. Conceptos básicos

1.1 EVM

EVM es una máquina virtual big-endian basada en pilas . Esta máquina virtual no es VMware, sino una máquina virtual similar a JVM, por lo que podemos entender EVM como entendemos JVM.

Similar a la JVM, la EVM también es una computadora diseñada y creada sobre una computadora real para admitir un conjunto de conjuntos de instrucciones personalizados. También contiene una pila y dos dominios de almacenamiento, memoria y almacenamiento.

Sí, si personaliza un conjunto de conjuntos de instrucciones, generalmente necesita implementar un lenguaje ensamblador correspondiente, y por encima del ensamblador está el lenguaje de alto nivel utilizado por los desarrolladores, como solidity, vyper, etc.

Sin embargo, a diferencia de JVM, EVM se puede instalar directamente en varias máquinas físicas.EVM está diseñado para integrarse en el cliente Ethereum, es decir, EVM se ejecuta en el sistema Ethereum. La función del EVM es ejecutar el contrato inteligente de Ethereum.
El contrato se crea a través de una transacción en una cuenta externa, y el código de bytes del contrato se adjuntará a la transacción data. Del mismo modo, las transacciones también pueden data
llevar a cabo varios tipos de interacciones con los contratos, como llamar y destruir contratos.

1.2 Código de bytes del contrato

El bytecode del contrato consiste en una serie de operadores (también llamados instrucciones), cualquier operador puede codificarse en un byte literal, excepto PUSHn .
El conjunto de instrucciones EVM admite varias instrucciones PUSH, como PUSH1, PUSH2etc. Los siguientes números se refieren al tamaño de los bytes de datos que se insertan en la pila, PUSH1 son los datos que se insertan en la pila en 1 byte, y así sucesivamente. Dado que PUSHn transporta datos,
el espacio que ocupa esta instrucción es variable ( PUSH1 0x00ocupa 2 bytes, PUSH2 0X00103 bytes, etc.).

1.3 Constructor del contrato

Una vez que el contrato se haya creado correctamente, su constructor se eliminará, es decir, el constructor no aparecerá en el contrato implementado.

1.4 Interactuar con el contrato

Un contrato expone algo de ABI (interfaz binaria de aplicación) para permitir que el mundo exterior interactúe con él.

1.5 Datos de llamadas

Es la información adjunta al campo de la transacción cuando se llama al contrato data
, y generalmente contiene un identificador de método de 4 bytes.El método de construcción del identificador de método es: keccak256("somefunc(uint)uint")[:4], que son los primeros 4 bytes después del hash keccak256 de la función firma.

1.6 Contador de programas

El contador coopera con la pila, la memoria y el almacenamiento para completar la ejecución del código de bytes del contrato.

La esencia del contador es un desplazamiento de la secuencia de código de operación desplegada, que puede entenderse como un puntero de ejecución, y la posición señalada por el contador es la posición donde se ejecutará la siguiente instrucción. La siguiente es una breve secuencia de códigos de operación (si no entiende, puede leer el texto primero y luego mirar hacia atrás):

// 这段操作码序列的总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 Entorno de ejecución (Contexto)

Cuando la EVM comience a ejecutar el bytecode del contrato, creará un contexto temporal e independiente para el contrato, específicamente, creará varias áreas de memoria separadas, cada una con diferentes propósitos.

  • Área de código: área estática de solo lectura, que almacena el código de bytes del contrato. Se pueden leer CODESIZEe CODECOPYinstrucciones, y los códigos de otros contratos se pueden leer EXTCODESIZE
    e EXTCODECOPYinstrucciones;
  • Stack: Es un espacio de matriz con un elemento de 32 bytes y una capacidad (longitud) de 1024, que se utiliza para almacenar los parámetros requeridos por la instrucción EVM y el resultado devuelto. Las instrucciones solo pueden acceder a los elementos de la pila a partir de la parte superior de la pila. Por lo general, las instrucciones PUSH1, DUP1, SWAP1, POP
    operan en la pila;
  • Memoria: es un espacio de matriz de elementos de un solo byte, que se utiliza para almacenar datos transitorios durante la ejecución del contrato. Se accede al espacio de memoria por desplazamiento de bytes. Por lo general, la instrucción MLOAD, MSTORE, MSTORE8
    operará en la memoria (puede ver el prefijo M antes de la instrucción);
  • Almacenamiento: a diferencia de las dos estructuras anteriores, es una estructura de mapa para almacenar datos persistentes, y tanto la clave como el valor son del tipo uint256. Por lo general, el comando SLOAD, SSTORE
    operará Almacenamiento (puede ver el prefijo S antes del comando);
  • calldata: Son los datos adjuntos cuando ocurre la transacción, y es un área estática de solo lectura. Por ejemplo, cuando se crea el contrato, el contenido de calldata es el código del constructor. Normalmente el comando CALLDATALOAD, CALLDATASIZE, CALLDATACOPY
    puede leerlo.
  • datos de retorno: es el área donde se almacena el valor de retorno del contrato. Puede ser modificado por instrucciones RETURN, REVERTy RETURNDATASIZE, RETURNDATACOPYleído por instrucciones;

1.8 OpCode (código de operación/instrucción EVM/mnemónico)

Es un conjunto de conjuntos de instrucciones adaptados para EVM, admite funciones como operaciones aritméticas, operaciones lógicas, operaciones de bits y saltos condicionales, y es un lenguaje completo de Turing. Los lenguajes como la solidez construida sobre él son, por supuesto, lenguajes completos de Turing.

OpCode se puede llamar código de operación/instrucción de ensamblaje EVM/nemotécnico (nemotécnico), su función es ayudar a las personas a leer la lógica del código. Los resultados finales de compilación e implementación del contrato se componen de una serie de datos de OpCode y operación.
EVM ejecuta la lógica del contrato extrayendo OpCode uno por uno de la secuencia de bytecode compilada para su ejecución. Si un determinado OpCode no se ejecuta (como parámetros/gas insuficientes), EVM revertirá todos los cambios.

En los documentos oficiales, el nombre OpCode se usa con más frecuencia, por lo que los lectores pueden usar OpCode como palabra clave al realizar consultas.

No todos los OpCodes consumirán gas, y algunos OpCodes devolverán gas. Hay dos operaciones conocidas que devolverán gas, una es destruir el contrato (devolver 24000 gas) y la otra es limpiar el almacenamiento (devolver 15000 gas).
Pero debe tenerse en cuenta que cuando se ejecuta el contrato, la devolución de gas aún existe en un contador de reembolso separado y no aumentará directamente el saldo de gas. Si no hay suficiente gas más tarde, aún causará la reversión. Es decir, el gas que se acaba de devolver no se puede utilizar cuando se ejecuta el contrato, y
el gas solo se puede devolver a la cuenta después de que se complete la ejecución. Finalmente, la cantidad de gas devuelta no debe exceder la mitad del gas consumido para la ejecución de la transacción, es decir, al menos la mitad del gas consumido debe ser pagado a los mineros.

Actualmente hay más de 140 OpCode, cada uno de los cuales se puede codificar en un byte (combinado con datos para formar un bytecode), PUSH
a excepción de las instrucciones, porque la instrucción puede transportar datos de cualquier longitud (no obtenidos de la pila, pero al escribir código) Es fijo), generalmente lo que ve es PUSH1o PUSH2
El número después del comando indica la longitud en bytes de los datos transportados.
Actualmente, hay comandos PUSH1~ PUSH. PUSH32Los parámetros requeridos por cada OpCode se obtienen de la pila (entrada de la pila), y los resultados del cálculo se envían a la pila (salida de la pila).

Dado que cada OpCode debe codificarse en un tamaño de un solo byte, se permite diseñar un máximo de 256 instrucciones (el rango decimal de un solo byte es 0~255, y el sistema hexadecimal es 0x00~0xFF, es decir, hasta se pueden expresar hasta 256 valores diferentes).

1.9 Consumo de gasolina

Como incentivo para proporcionar recursos para la ejecución de transacciones, se pagará una cierta cantidad de eth a los mineros. Esta cantidad está determinada por dos factores, la cantidad enviada y la cantidad de trabajo requerida para completar la transacción.

La tarifa de gas se divide en dos tipos: tarifa fija y tarifa dinámica. La tarifa fija la establece la plataforma Ethereum para ciertas operaciones. Por ejemplo, una transacción de transferencia simple consume 21000 de gas a un costo fijo; la tarifa dinámica se calcula de acuerdo con la siguiente fórmula:

gas_price * gas_limit = total max gas costs

Los valores de estas dos variables los establece el iniciador de la transacción. gas_price es el precio eth de 1 unidad de gas, como gas_price=10wei, que wei
es la unidad de moneda ether, por lo que no entraré en detalles aquí. gas_limit es la cantidad máxima de gas que el usuario que inicia la transacción está dispuesto a pagar por la ejecución de la transacción.
Este gas no siempre se consumirá y el gas no consumido se devolverá al iniciador de la transacción una vez que se complete la transacción. Si la cantidad total de gas establecida no es suficiente para respaldar la finalización de la transacción, no solo fallará la transacción, sino que no se reembolsará el gas consumido.

La composición de la tarifa de gas de una transacción que lleva el código de bytes del contrato: la transacción en sí cuesta 21000gas + la tarifa de gas de OpCode.

Entre ellos, la tarifa de gas de OpCode se divide en dos tipos, una es tarifa fija, la otra es tarifa dinámica, que generalmente está determinada por el tamaño o la cantidad de parámetros requeridos por el comando, y se puede consultar en evm.codes para detalles.

1.10 Proceso de Ejecución del Contrato

Necesitamos entender el proceso general de EVM ejecutando el contrato:

  • Cada instrucción ejecutada en el EVM se denomina OpCode (código de operación);
  • Cuando se ejecuta el contrato, el código de bytes compilado se convertirá en códigos de operación legibles y datos manipulados por el EVM, y luego se ejecutará;
  • Primero, el contador del programa (PC, similar a un registro) lee uno del código de bytes del contrato y luego recupera información como la función de operación y el costo del gas correspondiente al código de operación de JumpTable (una matriz con una longitud de 256);
    luego Deduzca el gas (si el código de operación es un costo de gas dinámico, debe calcularse), si es suficiente, ejecute el código de operación, si no es suficiente, deduzca el gas por completo y revierta la instrucción ejecutada. (Dependiendo de la instrucción, puede operar en la pila, memoria o StateDB)
  • Luego, cuando este código de operación se ejecuta con éxito, el contador del programa se incrementa y luego ingresa al siguiente ciclo (ejecuta la siguiente instrucción).

El proceso de ejecución específico está en un bucle for. Aquí, el código de función EVMInterpreter.Run() del contrato de ejecución geth se pega directamente tal como está. Léalo junto con los comentarios:

El código es largo, expandir para ver
 
 
// 这是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. Explicación detallada del proceso

El código de solidez que escribimos se puede compilar en el código ensamblador correspondiente a través de remix o compiladores locales como sloc y slocjs, y luego convertirse en código de caracteres hexadecimales puros ejecutado por la máquina.

  1. El código ensamblador y el código de bytes de Remix se pueden ver en la ruta [Solidity Compiler --> Compile Details];
  2. También puede descargar el compilador solc localmente y usar el compilador para compilar el código para obtener el código ensamblador y el código de bytes.

Descargue el compilador solc que admite la implementación de cpp con todas las funciones (recomendado) y consulte la guía de instalación oficial
para descargar solcjs que admite algunas funciones: npm install -g solc

Esto se ilustra con el siguiente fragmento de código simple;

// 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;
  }
}

Este es un código de solidez legible por humanos que se usa para implementar lógica personalizada. Para facilitar la ejecución de la máquina, debe compilarse en un código ensamblador de bajo nivel (también llamado código de operación) y luego convertirlo en código hexadecimal por parte de la máquina.
El código ensamblador se puede considerar como la forma de código más cercana a la capa de ejecución de la CPU. A través del código ensamblador, podemos ver el rendimiento real del código de solidez en la capa ensambladora con mayor claridad, por ejemplo, qué instrucciones ensambladoras utiliza una función, lo cual es muy útil para nosotros para solucionar problemas
, especialmente en la fase de depuración. Lo siguiente convierte la solidez en una forma compacta de secuencia de código de operación :

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

La secuencia del código de operación consta completamente de instrucciones y datos de EVM y organiza todas las instrucciones y datos de forma lineal.

Tome la parte anterior de la secuencia del código de operación del contrato de ejemplo como ejemplo para explicar:PUSH1 0x80 PUSH1 0x40 MSTORE

  • En primer lugar, el código de operación no es un código de bytes, y el código de operación aún se puede leer. El código de bytes es completamente una cadena de caracteres hexadecimales ilegibles, como 0128asdasda9s87d98asd, y cada código de operación se puede convertir en un byte.
  • PUSH1 0x80 PUSH1 0x40Indica que se inserta 1 byte de 0x80 en la pila, seguido de 0x40 (tenga en cuenta que un elemento de la pila tiene como máximo 32 bytes o 256 bits)
  • MSTORELa instrucción es una operación para guardar un valor en la memoria temporal de la EVM. Recibe 2 parámetros. El primer parámetro es la dirección de memoria utilizada para almacenar el valor, y el segundo parámetro es el valor a almacenar. Tenga en cuenta que esta instrucción es su parámetro de acuerdo con las
    regulaciones Obtenerlo de la pila (en lugar de una entrada externa), por lo que la lógica aquí es MSTORE 0x40 0x80 (almacene el valor 0x80 en la dirección 0x40)
  • Para conocer el significado de otras instrucciones, consulte la tabla del conjunto de instrucciones, que se enumeran a continuación.

La secuencia del código de operación no es propicia para nuestra lectura contra el código. Entonces necesitamos generar código ensamblador línea por línea:

// 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
}

Este código se divide en dos partes, sub_0 assemblyel límite, el código de arriba es el código de implementación y el código en el área sub_0 es el código de tiempo de ejecución
. De acuerdo con los comentarios en el código anterior, podemos leer el código ensamblador de manera relativamente más clara en comparación con el código de solidez.
El campo al final auxdata
es un valor binario con codificación CBOR que se utiliza para describir los metadatos del contrato, como la versión de solidez. Para obtener más información, consulte Metadatos
.

2.1 Acerca de la implementación de código

Como sugiere el nombre, se ejecuta cuando se implementa. Principalmente realiza tres tareas:

  1. Cheque pagadero, si el constructor del contrato no declara pagadero, inyectar éter durante la implementación hará que la implementación falle;
  2. Ejecute el constructor e inicialice las variables de estado declaradas;
  3. Calcule el código de bytes del código de tiempo de ejecución y devuélvalo a la EVM y guárdelo en StateDB (la clave es la dirección del contrato);

2.2 código de tiempo de ejecución

Se ejecuta cuando se recibe una llamada externa después del despliegue. solcAunque el código de tiempo de ejecución se calcula después de ejecutar el código de implementación, su método de cálculo es de lógica fija, por lo que la herramienta del compilador puede generarlo directamente.

2.3 Código de bytes final

El bytecode final son datalos datos transportados en el campo de transacción para la implementación del contrato. Es una cadena larga de caracteres hexadecimales y se genera de la siguiente manera:

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

El código de bytes final también se genera según un formato fijo: código de implementación + código de tiempo de ejecución + datos auxiliares.

Entre ellos, el código de tiempo de ejecución + auxdata se puede generar de la siguiente manera:

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

2.4 Conjunto de instrucciones diseñado para EVM

Los desarrolladores de Ethereum diseñaron especialmente un conjunto de conjuntos de instrucciones para EVM, es decir, el OpCode (OpCode) antes mencionado, que se presentó brevemente anteriormente, y aquí hay una introducción detallada.

El código del conjunto de instrucciones se compone de una serie de instrucciones en el conjunto de instrucciones de ensamblaje compatibles con el motor de ejecución de código y los datos que se operarán. Los conjuntos de instrucciones compatibles con diferentes motores de ejecución son generalmente diferentes, y el conjunto de instrucciones puede ser personalizado por el desarrollador de motores El motor de ejecución de EVM se ejecuta en función de OpCode,
por lo que OpCode también se denomina conjunto de instrucciones de EVM, y la tabla completa del conjunto de instrucciones de EVM se puede consultar aquí .

Aquí también hay una ilustración simple de la siguiente manera:

[Falló la transferencia de imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y cargarla directamente (img-rA5qqFz4-1677507590202)(./images/ethereum_opcodes_example.jpg)]

Para facilitar la lectura, la traducción en forma tabular es la siguiente:

uint8 Mnemotécnico Entrada de pila Salida de pila Expresión notas
Traducción: código de bytes nombre de comando Pila de elementos necesarios para la ejecución de instrucciones Elementos escritos en la pila después de ejecutar la instrucción expresión Observación
00 DETENER ninguno ninguno DETENER() Detener la ejecución del contrato
01 AGREGAR parte superior de la pila/a/b/parte inferior de la pila /a+b/ a+b Realiza una operación de suma en los dos elementos superiores int256 o uint256 de la pila

Tenga en cuenta que el orden de disposición de los elementos en la columna StackInput en la tabla comienza desde la parte superior de la pila. Esto se puede ilustrar con un ejemplo. Por ejemplo, si la instrucción de operación de resta está en StackInput en la tabla, SUB
entonces /a/b/hay el siguiente código:

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

La salida de esta instrucción es 1 en lugar de FFFFFF... (la forma uint256 de -1).

La EVM cuenta actualmente con un total de más de 140 instrucciones. Cabe señalar que el número de parámetros de algunas instrucciones no es fijo. Para simplificar, podemos dividir todos los códigos de operación en las siguientes categorías (secciones enumeradas):

  • Códigos de operación de manipulación de pila (POP, PUSH, DUP, SWAP)
  • Códigos de operación aritméticos/comparación/bit a bit (ADD, SUB, GT, LT, AND, OR)
  • Códigos de operación del entorno (CALLER, CALLVALUE, NUMBER)
  • Códigos de operación de operación de memoria (MLOAD, MSTORE, MSTORE8, MSIZE)
  • Almacenar códigos de operación de operación (SLOAD, SSTORE)
  • Códigos de operación relacionados con el contador del programa (JUMP, JUMPI, PC, JUMPDEST)
  • Detener los códigos de operación (DETENER, VOLVER, REVERTIR, NO VÁLIDO, AUTODESTRUCCIÓN)

Los lectores pueden consultar la tabla de conjuntos de instrucciones y la cantidad de gas consumido por las instrucciones correspondientes en evm.codes .
Al mismo tiempo, este sitio web también es compatible con la programación de códigos de operación en línea, la conversión en tiempo real entre códigos de operación, códigos de bytes y códigos de solidez.

La lista más precisa de conjuntos de instrucciones compatibles con Ethereum debe
consultarse en el código fuente de Geth. Este enlace
apunta al código Go relacionado con el conjunto de instrucciones de la versión v1.10.26 de Geth.

2.5 Explique detalladamente las instrucciones de montaje anteriores

El código es largo, expandir para ver
 
 
    /* "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
}

Alternativamente, si tiene instalado el cliente ethereum geth , puede usar el comando evm para learn_bytecode/Example.binconvertir archivos de código de bytes en códigos de operación legibles por compensación:

El código es largo, expandir para ver
 
 
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

En la secuencia de código de operación anterior, el lado izquierdo es el desplazamiento en el área de código de la instrucción derecha correspondiente en hexadecimal, que en realidad es el llamado área de código. Por ejemplo CODECOPY, JUMP
los parámetros offset y dest en la instrucción se refieren al desplazamiento de la instrucción correspondiente en el área de código.
El EVM ejecuta esta secuencia de opcodes secuencialmente de arriba hacia abajo. Si encuentra una instrucción JUMP, RETURN
salta o interrumpe la ejecución. Por ejemplo, cuando la secuencia de opcode anterior se ejecuta por primera vez, se ejecuta como máximo hasta que la 00062instrucción RETURN correspondiente a esta línea está completa.
Esto es Debido a que esta secuencia de código de operación es estrictamente un código de implementación, solo se ejecuta durante la implementación. Después de la implementación, este código devolverá una 00062secuencia de código de operación después de la devolución para que la EVM la guarde y se ejecute cuando finalice el contrato. llamado externamente en el futuro.


referencia

Supongo que te gusta

Origin blog.csdn.net/sc_lilei/article/details/129251495
Recomendado
Clasificación