Перейти к содержанию
Learning Platform
Продвинутый
40 минут
EVM Стек Память Хранилище Опкоды Storage Layout Байткод

Требуемые знания:

  • 03-state-trie-mpt

EVM: стек, память и хранилище

Зачем это нужно

Каждый смарт-контракт Ethereum выполняется на EVM (Ethereum Virtual Machine) — стековой виртуальной машине, встроенной в каждую ноду сети. Если вы хотите понимать, почему одна операция стоит 3 gas, а другая — 22100 gas, почему порядок переменных в Solidity влияет на стоимость транзакции, и как работает байткод контракта — вам нужно разобраться в архитектуре EVM.

Цель урока: Вы сможете объяснить архитектуру EVM, пошагово выполнить последовательность опкодов, нарисовать storage layout контракта и рассчитать стоимость расширения памяти.

Архитектура EVM

EVM — это стековая машина (stack machine). В отличие от регистровых архитектур (x86, ARM), EVM не имеет именованных регистров. Все операции работают со стеком.

Сравнение с Bitcoin Script

В уроке Bitcoin Script (BTC-04) мы уже видели стековую виртуальную машину. EVM — это радикально расширенная версия той же идеи:

ХарактеристикаBitcoin ScriptEVM
ТипСтековая машинаСтековая машина
Тьюринг-полнотаНет (нет циклов)Да (JUMP/JUMPI)
Стек1000 элементов, произвольный размер1024 элемента, 256-bit words
ПамятьНетByte-addressable, volatile
ХранилищеНет256-bit key-value, persistent
ГазНет (размер скрипта)Точный учет каждой операции
Опкоды~100~140+

Шесть компонентов EVM

EVM состоит из шести основных компонентов. Нажмите на каждый для подробностей:

Архитектура EVM
Stack256-bit words, max 1024
Memorybyte-addressable, volatile
Storage256-bit key-value, persistent
Calldataread-only input
Program Countercurrent position in bytecode
Gas Counterremaining gas budget
Bytecode → PC → Opcode → Stack/Memory/Storage → Gas Counter
Нажмите на компонент для подробностей

Интуитивная аналогия

Представьте EVM как рабочее место мастера:

  • Стек — стопка карточек на столе. Можно положить карточку сверху (PUSH) или снять верхнюю (POP). Максимум 1024 карточки
  • Память — блокнот для черновиков. Доступен во время работы, стирается когда мастер уходит
  • Хранилище — сейф. Данные сохраняются навсегда, но открывать и закрывать сейф дорого
  • Calldata — конверт с заданием от заказчика. Можно читать, нельзя менять
  • Program Counter — закладка в инструкции. Показывает, какой шаг выполняется сейчас
  • Gas Counter — бюджет на работу. Каждое действие стоит газ. Если бюджет закончился — всё откатывается

Опкоды EVM

Опкоды — это инструкции EVM. Каждый опкод занимает 1 байт (от 0x00 до 0xFF). Некоторые опкоды (PUSH1-PUSH32) имеют операнды — данные, следующие за опкодом в байткоде.

Группы опкодов

ГруппаПримерыОписание
StackPUSH1-PUSH32, POP, DUP1-DUP16, SWAP1-SWAP16Управление стеком
ArithmeticADD, MUL, SUB, DIV, MOD, EXPАрифметика (mod 2^256)
ComparisonLT, GT, EQ, ISZEROСравнение
BitwiseAND, OR, XOR, NOT, SHL, SHRПобитовые операции
MemoryMLOAD, MSTORE, MSTORE8, MSIZEРабота с памятью
StorageSLOAD, SSTOREРабота с хранилищем
FlowJUMP, JUMPI, JUMPDEST, STOP, RETURN, REVERTУправление потоком
ContextCALLER, CALLVALUE, CALLDATALOADКонтекст вызова
HashingKECCAK256 (SHA3)Хеширование
CallsCALL, DELEGATECALL, STATICCALL, CREATEМежконтрактные вызовы
LoggingLOG0-LOG4Генерация событий (events)

Пошаговое выполнение опкодов

Это ключевая интерактивная диаграмма урока. Выберите последовательность байткода и пройдите через каждый опкод, наблюдая изменения стека, хранилища и газа:

Выполнение опкодов EVM
Bytecode (PC: --)
602A600055
Stack (0/1024)
пусто
Storage (persistent)
пусто
OpcodeНачало
Gas Remaining30,000
Gas Used (step)--
Начальное состояние. Стек пуст, хранилище пусто.
Шаг 0 из 3 | зеленый = push, желтый = storage modified

Формальная запись: PUSH1 + SSTORE

Рассмотрим байткод 60 2A 60 00 55:

Байт  Опкод     Описание
0x00  PUSH1     Поместить 1 байт на стек
0x01  0x2A      Данные (42 в десятичной)
0x02  PUSH1     Поместить 1 байт на стек
0x03  0x00      Данные (слот 0)
0x04  SSTORE    Записать value=stack[0] в slot=stack[1]

Стек (LIFO):

После PUSH1 0x2A:  [0x2A]           (top)
После PUSH1 0x00:  [0x2A, 0x00]     (top = 0x00)
SSTORE берет:       slot=0x00, value=0x2A
После SSTORE:      []               (стек пуст)
Storage:           slot[0] = 0x2A

Storage Layout: как Solidity хранит переменные

Компилятор Solidity назначает каждой переменной слот в хранилище контракта. Каждый слот — 32 байта (256 бит). Правила:

  1. Простые типы (uint256, address, bool) — последовательные слоты, начиная с 0
  2. Packing — переменные меньше 32 байт, объявленные подряд, упаковываются в один слот
  3. Mapping — base slot содержит ничего; значение mapping[key] хранится в слоте keccak256(key . slot_number)
  4. Dynamic arrays — длина в base slot; данные начинаются с keccak256(slot_number)
Storage Layout: переменные и слоты
// Contract storage layout
uint256 a; // slot 0
uint128 b; // slot 1 (lower 16 bytes)
uint128 c; // slot 1 (upper 16 bytes)
mapping(address => uint256) d; // slot 2 (base)
Slot 0uint256 a
32 bytes
Slot 1uint128 b + uint128 c
16 + 16 = 32 bytespacked
Slot 2mapping(address => uint256) d
base slot
Нажмите на слот для подробностей. Mapping использует FNV-хеш для демонстрации (вместо keccak256).

Зачем это знать?

  • Оптимизация газа: Чтение/запись одного слота дешевле двух. Правильный порядок переменных = меньше слотов
  • Безопасность: Upgradeable прокси требуют сохранения storage layout между версиями
  • Отладка: forge inspect Contract storage-layout показывает реальный layout
  • Низкоуровневый доступ: DELEGATECALL использует storage вызывающего контракта — несовпадение layout = катастрофа

Пример оптимизации

// Плохо: 3 слота
contract Bad {
    uint128 a;  // slot 0 (16 bytes, не заполнен)
    uint256 b;  // slot 1 (32 bytes, новый слот — не помещается в slot 0)
    uint128 c;  // slot 2 (16 bytes, не заполнен)
}

// Хорошо: 2 слота
contract Good {
    uint128 a;  // slot 0 (lower 16 bytes)
    uint128 c;  // slot 0 (upper 16 bytes) — packed!
    uint256 b;  // slot 1
}

Память EVM: расширение и стоимость

Память EVM — это линейный массив байтов, адресуемый побайтово. В начале выполнения память пуста. При обращении к ранее не использованному адресу память расширяется — и за это нужно платить газом.

Формула стоимости

Стоимость памяти зависит от количества 32-байтных слов:

memory_cost = words * 3 + words^2 / 512

Первый компонент (words * 3) — линейный. Второй (words^2 / 512) — квадратичный. Это означает, что небольшие объемы памяти дешевые, но стоимость быстро растет:

WordsBytesCost (gas)Примечание
1323Один слот
3969Типичный вызов
1032030
1003200319Квадратичность заметна
1000320004953Дорого
Расширение памяти EVM
Начальное состояниеПамять пуста. Размер = 0 байт. Указатель свободной памяти (free memory pointer) по умолчанию = 0x80.
Memory (0 bytes = 0 words)
пусто
Gas Cost: memory_cost = words * 3 + words^2 / 512
0g
0 words14 words
Memory Size0 bytes
Expansion Cost--
Total Memory Gas--
Шаг 0 из 3 | Стоимость растет квадратично с размером памяти

Free Memory Pointer

Solidity хранит указатель свободной памяти в ячейке 0x40. По умолчанию он равен 0x80 (первые 128 байт зарезервированы):

0x00-0x3f  Scratch space (временные данные)
0x40-0x5f  Free memory pointer (указывает на начало свободной памяти)
0x60-0x7f  Zero slot (используется как default value)
0x80+      Свободная память (начало данных)

Математическое описание

Stack Machine формально

EVM — это кортеж (code, pc, stack, memory, storage, gas):

code    : byte[]         -- байткод контракта
pc      : uint           -- program counter (0..len(code)-1)
stack   : uint256[]      -- стек (max 1024)
memory  : byte[]         -- volatile memory
storage : Map<uint256, uint256>  -- persistent storage
gas     : uint           -- remaining gas

Переход состояния для каждого опкода:

PUSH1 v:  pc' = pc+2, stack' = stack ++ [v], gas' = gas - 3
ADD:      pc' = pc+1, stack' = stack[:-2] ++ [stack[-1] + stack[-2]], gas' = gas - 3
SSTORE:   pc' = pc+1, stack' = stack[:-2], storage'[stack[-1]] = stack[-2],
          gas' = gas - (cold ? 22100 : 100)
MSTORE:   pc' = pc+1, stack' = stack[:-2], memory'[stack[-1]..+32] = stack[-2],
          gas' = gas - 3 - expansion_cost

Стоимость памяти формально

M(a) = a * G_memory + floor(a^2 / 512)

где:
  a = ceil(max_accessed_byte / 32)  -- количество слов
  G_memory = 3                       -- стоимость за слово

Стоимость расширения при доступе к новому максимальному адресу:

expansion_cost = M(a_new) - M(a_old)

Практика

  1. Откройте диаграмму Выполнение опкодов EVM и пройдите обе последовательности
  2. Для каждого шага запишите: opcode, состояние стека до/после, gas used
  3. В диаграмме Storage Layout исследуйте, как mapping вычисляет слот для разных ключей
  4. В диаграмме Расширение памяти проследите, как квадратично растет стоимость

Итоги

  • EVM — стековая машина с 6 компонентами: стек, память, хранилище, calldata, PC, gas
  • Стек: 256-bit words, max 1024 глубина. LIFO. Все операции работают через стек
  • Память: byte-addressable, volatile, расширяется с квадратичной стоимостью
  • Хранилище: 256-bit key-value pairs, persistent, самое дорогое (SSTORE cold = 22100 gas)
  • Solidity назначает переменным последовательные слоты; малые типы пакуются
  • Mapping хранит значения в слотах keccak256(key . base_slot)
  • Правильный порядок переменных в контракте экономит газ за счет slot packing

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс