본문으로 건너뛰기
BlockchainMar 28, 2026

Deep EVM #1: Wie die EVM Ihren Code ausfuehrt — Opcodes, Stack und Gas

OS
Open Soft Team

Engineering Team

Die EVM ist eine Stack-Maschine

Die Ethereum Virtual Machine ist nicht wie der x86-Prozessor in Ihrem Laptop. Sie hat keine Register. Stattdessen ist die EVM eine Stack-Maschine — jede Berechnung schiebt Daten auf oder entnimmt sie von einem 1024-Elemente-Stack, wobei jedes Element ein 256-Bit-Wort (32 Byte) ist.

Wenn Sie einen Smart Contract aufrufen, erhaelt die EVM den Bytecode des Contracts — eine flache Sequenz von Ein-Byte-Opcodes — und beginnt die Ausfuehrung bei Byte 0. Es gibt keine Funktionstabelle, keinen ELF-Header, keine Linking-Phase. Der Bytecode ist das Programm.

// Solidity:
// uint256 result = 2 + 3;

// Kompiliert zu Bytecode:
// PUSH1 0x02  PUSH1 0x03  ADD

// Stack-Trace:
// []           -> PUSH1 0x02 -> [2]
// [2]          -> PUSH1 0x03 -> [2, 3]
// [2, 3]       -> ADD        -> [5]

Jeder Opcode entnimmt seine Operanden vom Stack-Top und legt das Ergebnis zurueck. Der ADD-Opcode entnimmt zwei Werte, addiert sie und legt die Summe auf den Stack. Dies unterscheidet sich grundlegend von Register-Architekturen, bei denen Sie Quell- und Zielregister angeben.

Opcode-Kategorien

Die EVM definiert etwa 140 Opcodes, gruppiert nach funktionalen Kategorien:

Arithmetik und Vergleich

  • ADD, SUB, MUL, DIV, MOD — grundlegende 256-Bit-Ganzzahlarithmetik. Alle kosten 3 Gas (G_verylow-Stufe).
  • SDIV, SMOD — vorzeichenbehaftete Division und Modulo im Zweierkomplement.
  • ADDMOD, MULMOD — modulare Arithmetik: (a + b) % N und (a * b) % N in einem Opcode. Kritisch fuer Operationen auf elliptischen Kurven, kosten 8 Gas.
  • EXP — Potenzierung. Kostet 10 Gas + 50 pro Byte des Exponenten, was ihn zu einem der teuersten arithmetischen Opcodes macht.
  • LT, GT, SLT, SGT, EQ, ISZERO — Vergleichs-Opcodes, die 1 (wahr) oder 0 (falsch) auf den Stack legen.

Bitweise Operationen

  • AND, OR, XOR, NOT — bitweise Logik, jeweils 3 Gas.
  • SHL, SHR, SAR — Linksverschiebung, logische Rechtsverschiebung, arithmetische Rechtsverschiebung (eingefuehrt mit Constantinople, EIP-145). Vorher erforderten Verschiebungen MUL/DIV mit Zweierpotenzen.
  • BYTE — Extrahiert ein einzelnes Byte aus einem 32-Byte-Wort. BYTE(0, x) gibt das hoechstwertige Byte zurueck.

Stack-Manipulationen

  • POP — oberstes Element entfernen.
  • PUSH1 — PUSH32 — 1 bis 32 Bytes unmittelbarer Daten auf den Stack legen. PUSH1 ist der haeufigste Opcode in bereitgestelltem Bytecode.
  • DUP1 — DUP16 — das N-te Stack-Element an die Spitze duplizieren.
  • SWAP1 — SWAP16 — das oberste Element mit dem N-ten Element darunter tauschen.

Umgebungs- und Blockinformationen

  • CALLER (msg.sender), CALLVALUE (msg.value), CALLDATALOAD, CALLDATASIZE, CALLDATACOPY — Zugriff auf den Transaktionskontext.
  • NUMBER, TIMESTAMP, BASEFEE, CHAINID — Block-Level-Informationen.
  • BALANCE, EXTCODESIZE, EXTCODECOPY — Abfrage von Daten anderer Konten.

Gas-Zeitplan

Jeder Opcode hat Gaskosten. Gas erfuellt zwei Funktionen: Es verhindert Endlosschleifen (Halteproblem) und bewertet Rechenressourcen fair.

Die Gaskosten verteilen sich auf Stufen:

StufeGasBeispiele
Null0STOP, RETURN, REVERT
Basis2ADDRESS, ORIGIN, CALLER
Sehr niedrig3ADD, SUB, LT, GT, AND, OR, POP
Niedrig5MUL, DIV, MOD
Mittel8ADDMOD, MULMOD, JUMP
Hoch10JUMPI
SpeziellvariabelSLOAD, SSTORE, CALL, CREATE

Die teuersten Opcodes sind diejenigen, die mit dem Zustand interagieren:

// Gaskosten fuer Zustandszugriff (nach EIP-2929):
// SLOAD (kalt):  2100 Gas
// SLOAD (warm):   100 Gas
// SSTORE (kalt, 0->ungleich Null): 22100 Gas
// SSTORE (warm):    100 Gas (+ 20000 wenn 0->ungleich Null)
// CALL (kalt):   2600 Gas
// CALL (warm):    100 Gas
// BALANCE (kalt): 2600 Gas
// BALANCE (warm):   100 Gas

Kalter und warmer Zugriff (EIP-2929)

EIP-2929 (Berlin-Upgrade, April 2021) fuehrte das Konzept der Zugriffsliste ein — eine Menge von Adressen und Speicherslots pro Transaktion, auf die bereits zugegriffen wurde.

Beim ersten Zugriff auf einen Speicherslot oder eine externe Adresse in einer Transaktion gilt er als “kalt” und kostet zusaetzliches Gas. Wiederholte Zugriffe sind “warm” und guenstig. Deshalb ist die Reihenfolge der Speicher-Slot-Lesevorgaenge fuer die Gasoptimierung wichtig.

// In Solidity ist dieses Muster teuer:
function bad() external view returns (uint256) {
    // Erster Slot-Lesevorgang: 2100 Gas (kalt)
    uint256 a = myStorage;
    // ... Logik ...
    // Zweiter Lesevorgang: 100 Gas (warm)
    uint256 b = myStorage;
    return a + b;
}

// Im Speicher zwischenspeichern:
function good() external view returns (uint256) {
    uint256 cached = myStorage; // 2100 Gas (kalt), nur einmal
    return cached + cached;     // 6 Gas (ADD + DUP)
}

Ausfuehrungsablauf: Was in einer Transaktion passiert

Wenn Sie eine Transaktion senden, die einen Contract aufruft, ist dies die vollstaendige Ausfuehrungssequenz:

  1. Transaktionsvalidierung — Nonce-Pruefung, Guthaben >= value + gas * gasPrice, Signaturverifikation.
  2. Intrinsischer Gasabzug — 21.000 Gas fuer die Transaktion selbst, plus 16 Gas fuer jedes Nicht-Null-Byte der Calldata und 4 fuer Null-Bytes.
  3. Kontext-Einrichtung — Die EVM erstellt einen Ausfuehrungskontext: Code, Calldata, Aufrufer, Wert, verbleibendes Gas.
  4. Programmzaehler beginnt bei 0 — Die EVM liest den Opcode an Position 0 und fuehrt ihn aus.
  5. Sequentielle Ausfuehrung — Jeder Opcode wird ausgefuehrt, Gas wird abgezogen. JUMP und JUMPI ermoeglichen nichtlinearen Kontrollfluss, aber nur zu Positionen, die mit JUMPDEST markiert sind.
  6. Beendigung — Die Ausfuehrung endet durch STOP (Erfolg, keine Rueckgabedaten), RETURN (Erfolg, mit Rueckgabedaten), REVERT (Fehler, Zustand wird zurueckgesetzt) oder Gaserschoepfung.
  7. Zustandsfestschreibung oder Rollback — Bei Erfolg werden alle Zustandsaenderungen festgeschrieben. Bei Rollback werden alle Aenderungen in diesem Aufrufkontext rueckgaengig gemacht.

Programmzaehler und JUMP

Der Programmzaehler (PC) ist ein implizites Register, das die aktuelle Position im Bytecode verfolgt. Die meisten Opcodes erhoehen den PC um 1 (oder 1 + N fuer PUSH-Opcodes). Zwei Opcodes aendern den PC direkt:

  • JUMP — entnimmt eine Zieladresse vom Stack, setzt PC auf diesen Wert. Die Zieladresse muss einen JUMPDEST-Opcode enthalten, sonst wird die Transaktion rueckgaengig gemacht.
  • JUMPI — bedingter Sprung. Entnimmt Zieladresse und Bedingung. Wenn die Bedingung ungleich Null ist — Sprung; andernfalls sequentielle Ausfuehrung fortsetzen.

So implementiert die EVM if/else, Schleifen und Funktionsdispatch. Der Solidity-Compiler generiert einen Funktionsselektor, der die ersten 4 Bytes der Calldata laedt, mit bekannten Signaturen vergleicht und JUMPI zum entsprechenden Codeblock ausfuehrt.

// Funktionsdispatch (vereinfachter Bytecode):
// CALLDATALOAD(0) -> SHR(224) -> function_selector
// DUP1 PUSH4 0xa9059cbb EQ PUSH2 0x00a4 JUMPI  // transfer(address,uint256)
// DUP1 PUSH4 0x70a08231 EQ PUSH2 0x00d2 JUMPI  // balanceOf(address)
// PUSH1 0x00 DUP1 REVERT                         // Fallback: revert

Unteraufrufe: CALL, STATICCALL, DELEGATECALL

Contracts koennen andere Contracts mit drei Aufruf-Opcodes aufrufen:

  • CALL — Standardaufruf. Erstellt einen neuen Ausfuehrungskontext mit eigenem Stack und Speicher. Der Aufgerufene arbeitet unabhaengig; wenn er zurueckgesetzt wird, werden nur seine Aenderungen rueckgaengig gemacht.
  • STATICCALL — Nur-Lese-Aufruf (EIP-214). Jeder zustandsaendernde Opcode (SSTORE, CREATE, LOG, SELFDESTRUCT) innerhalb des Aufgerufenen verursacht einen sofortigen Rollback.
  • DELEGATECALL — Fuehrt den Code des Aufgerufenen aus, aber im Speicherkontext des Aufrufers. msg.sender und msg.value bleiben vom urspruenglichen Aufruf erhalten. So funktionieren Proxy-Muster und Bibliotheken.

Jeder Aufruf-Opcode nimmt 7 Stack-Argumente: gas, address, value (ausser STATICCALL/DELEGATECALL), argsOffset, argsLength, retOffset, retLength. Gaskosten: 100 (warm) oder 2600 (kalt) plus Zuschlag fuer Wertuebertragung.

Gas-Rueckerstattungen und ihre Grenzen

Historisch gab das Loeschen eines Speicherslots von ungleich Null auf Null eine Gas-Rueckerstattung — als Anreiz zur Zustandsbereinigung. Nach EIP-3529 (London-Upgrade) ist die maximale Rueckerstattung auf 20% des gesamten verbrauchten Gases begrenzt (vorher 50%). Dies zerstoerte die Arbitrage von Gas-Token (CHI, GST2), die Rueckerstattungen zur Gewinnerzielung ausnutzten.

Verbleibende Rueckerstattungsquellen:

  • SSTORE: Setzen eines Nicht-Null-Slots zurueck auf Null erstattet 4.800 Gas.
  • SELFDESTRUCT: Als Rueckerstattungsquelle in EIP-3529 entfernt.

Praktische Auswirkungen fuer MEV

Wenn Sie MEV-Bots bauen, ist das Verstaendnis der EVM auf Opcode-Ebene keine Option, sondern eine Wettbewerbsanforderung. Jede eingesparte Gas-Einheit bei der Ausfuehrung Ihres Bots ist Gewinnmarge. Wichtige Erkenntnisse:

  • Vor dem Senden simulieren — verwenden Sie eth_call oder eine lokale EVM (revm, EVMONE) fuer die Ausfuehrungstrace und praezise Gaskostenberechnung vor dem Senden des Bundles an den Builder.
  • Kalten Zugriff minimieren — Speicherslots ueber Zugriffslisten (EIP-2930) vorwaermen.
  • STATICCALL fuer Lesevorgaenge verwenden — etwas guenstiger und garantiert keine Zustandsmutation.
  • Opcode-Kosten kennen — ein schlecht platzierter SLOAD kann 2.100 Gas kosten; in der wettbewerbsintensiven MEV-Umgebung ist das der Unterschied zwischen Gewinn und Verlust.

Fazit

Die EVM ist elegant in ihrer Einfachheit: eine Stack-Maschine mit 256-Bit-Woertern, ein flaches Bytecode-Format und ein Gas-Abrechnungssystem, das jede Operation bewertet. Das Verstaendnis dieses Fundaments — Opcodes, Stack und Gas — ist die Voraussetzung fuer alles andere in dieser Serie: Speicherlayout, Storage-Optimierung, Sicherheitsprimitive und schliesslich das Schreiben von reinem Yul.

Im naechsten Artikel werden wir die vier Datenbereiche der EVM untersuchen: Stack, Memory, Storage und Calldata — und warum die richtige Wahl bestimmt, ob Ihr Contract $0,50 oder $50 pro Ausfuehrung kostet.