¿Por qué el cálculo de la dirección de salto es tan complejo en el código Solidity compilado?

Tengo el siguiente contrato de Solidez simple:

pragma solidity ^0.4.0;
contract Test {

   function Test() {
       intfunc(5);
   }

   uint8 store;

   function intfunc (uint8 a) internal {
       store = a * 9;
   }
}

Lo estoy compilando usando Remix , y obtengo un código de bytes que no puedo explicar entre las direcciones 0x11 y 0x1E (estoy incluyendo direcciones hexadecimales a la izquierda para mayor comodidad):

//Standard preamble:
0x00: PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xB JUMPI INVALID

//Beginning of Test() constructor:
0x0B: JUMPDEST JUMPDEST PUSH1 0x20 PUSH1 0x5

//Here's the really strange code:
0x11: PUSH5 0x100000000
0x17: PUSH1 0x7
0x19: PUSH1 0x25
0x1B: DUP3
0x1C: MUL
0x1D: OR
0x1E: DIV

//Here we jump to the intfunc() function
0x1F: JUMP

//Here we come back from intfunc() and jump to rest of the Test() constructor
0x20: JUMPDEST JUMPDEST PUSH1 0x3B JUMP

//intfunc() itself:
0x25: JUMPDEST PUSH1 0x0 DUP1 SLOAD PUSH1 0xFF NOT AND PUSH1 0x9 DUP4 MUL PUSH1 0xFF AND OR SWAP1 SSTORE JUMPDEST POP JUMP

//The rest of the Test() constructor and the rest of the code is here...
0x3B: JUMPDEST ...
//(The rest isn't really relevant to this question)

¿Cuál es el trato con el código entre 0x11 y 0x1E? ¿Por qué es tan engorroso? ¿No puede ser reemplazado por un simple " PUSH1 0x25 "? ¿No es solo una pérdida de combustible pasar por todos estos pasos extraños solo para calcular el valor 0x25?

Además, ¿de dónde viene el número 7 en la instrucción 0x17? Parece completamente inútil.

Tenga en cuenta que obtengo un código similar para los modos Remix "optimizado" y "no optimizado".

¡Cualquier idea sería apreciada!

Interesado en la respuesta a esto. ¿Alguna vez llegaste más lejos? :-)
Lamentablemente no
De acuerdo con todos tus puntos! es muy peculiar Deberías mirar el mismo código compilado con 0.4.0 del compilador en Remix: mucho más conciso, y nada de estas tonterías. Es una de las razones por las que estoy explorando LLL ; hay muy poca intervención por parte del compilador.

Respuestas (2)

Este comportamiento se introdujo en este Github PR . Parece estar relacionado con una optimización en torno al almacenamiento de etiquetas para llamadas a funciones cuando se encuentra en el contexto del constructor.

El desplazamiento a la izquierda de 32 bits MUL 0x0100000000y ORlas operaciones se insertan por función pushCombinedFunctionEntryLabelen el archivo libsolidity/codegen/CompilerUtils.cpp . ExpressionCompiler.cpp inserta el subsiguiente desplazamiento a la derecha de 32 bits donde llama a rightShiftNumberOnStack .

Probablemente, esto sea realmente útil en algunas circunstancias para empacar etiquetas de salto que deben almacenarse en un contrato de almacenamiento (y, por lo tanto, ahorrar una gran cantidad de gasolina: el almacenamiento es costoso). En el caso de este simple contrato, parece ser un remanente innecesario.

si compilamos un contrato simple como:

    contract C {

   uint store=45;

    }

obtendremos (estoy usando el compilador 0.4):

00 PUSH1 60
02 PUSH1 40
04 MSTORE

05 PUSH1 2d //value to store 45
07 PUSH1 00 //storage address
09 PUSH1 00 // useless
11 POP      //useless
12 SSTORE

sin embargo, si cambiamos el cambio uintde uint8situación. obtendremos en su lugar un bytecode más largo:

00 PUSH1 60
02 PUSH1 40
04 MSTORE

05 PUSH1 2d //value
07 PUSH1 00 //storage address
09 PUSH1 00//mask offset
11 PUSH2 0100// multiplier
14 EXP
15 DUP2
16 SLOAD
17 DUP2
18 PUSH1 ff
20 MUL
21 NOT
22 AND
23 SWAP1
24 DUP4
25 MUL
26 OR
27 SWAP1
28 SSTORE
29 POP

¿Entonces, cuál es el problema?
cuando usamos uintusamos directamente una palabra de 32 bytes, sin embargo, cuando usamos uint8solo necesitamos el primer byte en una palabra de almacenamiento para colocarlo junto con otros valores en una ranura de almacenamiento, por lo que debemos realizar alguna manipulación para evitar la sobrescritura de datos.

Creo que el compilador rellenará el 0x2d a 32 bytes, por lo que los otros valores en la ranura se sobrescribirán y mantendremos solo el primer byte (2d). Para evitar este problema, usamos sloadpara cargar el valor anterior presente en la palabra y usamos operaciones de bits (MUL NOT Y SWAP1 DUP4 MUL OR) para calcular (los div y mul utilizados son para cambiar los valores) el nuevo valor que se almacenará en el ranura combinando el valor de relleno 0x2d00000...00000 y el valor anterior, al final llamamos sstorepara guardar el resultado.

Esto no tiene nada que ver con el cálculo de la dirección de salto en la pregunta.
@benjaminion, ¿has leído estas preguntas? ¿Cuál es el problema con el código entre 0x11 y 0x1E? ¿Por qué es tan engorroso? ¿No es solo una pérdida de gasolina seguir todos estos pasos extraños solo para calcular el valor 0x25? Además, ¿de dónde viene el número 7 en la instrucción 0x17? Parece completamente inútil.
Sí, por supuesto, y creo que mi respuesta lo cubre. No tiene nada que ver con uint8: intente completar el código del OP con uint16 o más grande, no cambia el comportamiento de este cálculo de salto entre 0x11 y 01E.
@benjaminion tenías razón, me apresuré en mi respuesta porque pensé que esto estaba relacionado con uint8 (en el compilador antiguo), no me di cuenta de las direcciones del código de operación. Te otorgaré la recompensa.
Gracias, apreciado. Fue mucho trabajo investigar el código fuente del compilador y Github para investigar esto. Básicamente son solo los restos de una optimización fallida.