Warum ist die Sprungadressenberechnung in kompiliertem Solidity-Code so komplex?

Ich habe den folgenden einfachen Solidity-Vertrag:

pragma solidity ^0.4.0;
contract Test {

   function Test() {
       intfunc(5);
   }

   uint8 store;

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

Ich kompiliere es mit Remix und erhalte Bytecode, den ich zwischen den Adressen 0x11 und 0x1E nicht erklären kann (ich füge der Einfachheit halber links Hex-Adressen hinzu):

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

Was hat es mit dem Code zwischen 0x11 und 0x1E auf sich - warum ist das so umständlich? Kann es nicht einfach durch ein einfaches „ PUSH1 0x25 “ ersetzt werden? Ist es nicht einfach eine Zeitverschwendung, all diese seltsamen Schritte zu durchlaufen, nur um den 0x25-Wert zu berechnen?

Woher kommt außerdem die Zahl 7 in der Anweisung 0x17? Es scheint völlig sinnlos.

Beachten Sie, dass ich ähnlichen Code sowohl für "optimierte" als auch für "nicht optimierte" Remix-Modi erhalte.

Jeder Einblick wäre willkommen!

Interessiert an der Antwort darauf. Bist du jemals weiter gekommen? :-)
Unglücklicherweise nicht
Stimme dir in allen Punkten zu! Es ist sehr eigenartig. Sie sollten sich denselben Code ansehen, der mit 0.4.0 des Compilers in Remix kompiliert wurde: viel prägnanter und ohne diesen Unsinn. Das ist einer der Gründe, warum ich LLL erforsche ; Der Compiler greift kaum ein.

Antworten (2)

Dieses Verhalten wurde in diesem Github PR eingeführt . Es scheint mit einer Optimierung bezüglich des Speicherns von Tags für Funktionsaufrufe im Konstruktorkontext zusammenzuhängen.

Die Linksverschiebung um 32 Bit MUL 0x0100000000und ORdie Operationen werden per Funktion pushCombinedFunctionEntryLabelin die Datei libsolidity/codegen/CompilerUtils.cpp eingefügt . Die nachfolgende Rechtsverschiebung um 32 Bit wird von ExpressionCompiler.cpp dort eingefügt, wo sie rightShiftNumberOnStack aufruft .

Wahrscheinlich ist dies unter Umständen wirklich sinnvoll, um Sprungmarken zu verpacken, die im Lohnlager gelagert werden müssen (und damit viel Benzin sparen - Lagern ist teuer). Bei diesem einfachen Vertrag scheint es nur ein unnötiges Überbleibsel zu sein.

wenn wir einen einfachen Vertrag erstellen wie:

    contract C {

   uint store=45;

    }

wir erhalten (ich verwende Compiler 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

aber wenn wir das ändern , ändert uintsich uint8die Situation. wir erhalten stattdessen einen längeren Bytecode:

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

Also, was ist das Problem?
Wenn wir verwenden , verwenden uintwir direkt ein 32-Byte-Wort, aber wenn wir verwenden uint8, benötigen wir nur das erste Byte in einem Speicherwort, um es neben anderen Werten in einem Speicherslot zu platzieren, also müssen wir einige Manipulationen vornehmen, um ein Überschreiben von Daten zu vermeiden.

Ich denke, der Compiler wird die 0x2d auf 32 Bytes auffüllen, sodass die anderen Werte im Steckplatz überschrieben werden und wir nur das erste Byte (2d) behalten. Um dieses Problem zu vermeiden sload, laden wir den vorherigen Wert des Wortes und verwenden Bitoperationen (MUL NOT UND SWAP1 DUP4 MUL OR), um den neuen Wert zu berechnen (div und mul werden verwendet, um die Werte zu verschieben). Slot, der den aufgefüllten Wert 0x2d00000...00000 und den vorherigen Wert kombiniert, rufen wir am Ende sstoreauf, um das Ergebnis zu speichern.

Dies hat nichts mit der Sprungadressenberechnung in der Frage zu tun.
@benjaminion hast du diese Fragen gelesen: Was hat es mit dem Code zwischen 0x11 und 0x1E auf sich - warum ist es so umständlich? Ist es nicht nur eine Verschwendung von Benzin, all diese seltsamen Schritte zu durchlaufen, nur um den 0x25-Wert zu berechnen? Woher kommt außerdem die Zahl 7 in der Anweisung 0x17? Es scheint völlig sinnlos.
Ja natürlich, und ich denke, meine Antwort deckt es ab. Es hat nichts damit zu tun uint8- versuchen Sie, den OP-Code mit uint16 oder größer zu vervollständigen, es ändert nichts am Verhalten dieser Sprungberechnung zwischen 0x11 und 01E.
@benjaminion du hattest Recht, ich habe meine Antwort überstürzt, weil ich dachte, dass dies mit uint8 zusammenhängt (im alten Compiler). Ich habe die Opcode-Adressen nicht bemerkt. Ich werde dir das Kopfgeld zusprechen.
Danke, geschätzt. Es war eine Menge Arbeit, im Compiler-Quellcode und Github herumzuwühlen, um dies zu untersuchen. Es sind im Grunde nur die Überreste einer gescheiterten Optimierung.