본문으로 건너뛰기
블록체인Mar 28, 2026

Deep EVM #1: EVM이 코드를 실행하는 방법 — 옵코드, 스택, 가스

OS
Open Soft Team

Engineering Team

EVM은 스택 머신이다

이더리움 가상 머신(EVM)은 노트북의 x86 프로세서와 다릅니다. 레지스터가 없습니다. 대신 스택 머신입니다 — 모든 연산은 각 요소가 256비트(32바이트) 워드인 1024개 요소 스택에 푸시하거나 팝합니다.

스마트 컨트랙트를 호출하면 EVM은 컨트랙트의 바이트코드 — 단일 바이트 옵코드의 평면 시퀀스 — 를 받아 바이트 0부터 실행을 시작합니다. 함수 테이블도, ELF 헤더도, 링킹 단계도 없습니다. 바이트코드가 곧 프로그램입니다.

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

// 바이트코드로 컴파일:
// PUSH1 0x02  PUSH1 0x03  ADD

// 스택 추적:
// []           -> PUSH1 0x02 -> [2]
// [2]          -> PUSH1 0x03 -> [2, 3]
// [2, 3]       -> ADD        -> [5]

모든 옵코드는 스택 상단에서 피연산자를 소비하고 결과를 다시 푸시합니다. ADD 옵코드는 두 값을 팝하고, 더한 후, 합계를 푸시합니다. 이것은 소스와 대상 레지스터를 지정하는 레지스터 기반 아키텍처와 근본적으로 다릅니다.

옵코드 카테고리

EVM은 약 140개의 옵코드를 정의하며, 기능별 카테고리로 분류됩니다:

산술 및 비교

  • ADD, SUB, MUL, DIV, MOD — 기본 256비트 정수 산술. 모두 3 가스 비용(G_verylow 등급).
  • SDIV, SMOD — 2의 보수를 사용한 부호 있는 나눗셈과 모듈로.
  • ADDMOD, MULMOD — 모듈러 산술: (a + b) % N(a * b) % N을 단일 옵코드로. 타원 곡선 연산에 중요하며 8 가스 비용.
  • EXP — 지수 연산. 10 가스 + 지수의 바이트당 50 가스로, 비용이 높은 산술 옵코드 중 하나.
  • LT, GT, SLT, SGT, EQ, ISZERO — 1(참) 또는 0(거짓)을 푸시하는 비교 옵코드.

비트 연산

  • AND, OR, XOR, NOT — 비트 논리 연산, 각 3 가스.
  • SHL, SHR, SAR — 왼쪽 시프트, 논리적 오른쪽 시프트, 산술 오른쪽 시프트(콘스탄티노플 업그레이드, EIP-145에서 추가). 이전에는 2의 거듭제곱으로 MUL/DIV가 필요했습니다.
  • BYTE — 32바이트 워드에서 단일 바이트 추출. BYTE(0, x)는 최상위 바이트를 반환.

스택 조작

  • POP — 최상위 요소 폐기.
  • PUSH1~PUSH32 — 1~32바이트의 즉시 데이터를 스택에 푸시. PUSH1은 배포된 바이트코드에서 가장 흔한 옵코드.
  • DUP1~DUP16 — N번째 스택 요소를 상단에 복제.
  • SWAP1~SWAP16 — 최상위 요소를 N번째 요소와 교환.

환경 및 블록 정보

  • CALLER (msg.sender), CALLVALUE (msg.value), CALLDATALOAD, CALLDATASIZE, CALLDATACOPY — 트랜잭션 컨텍스트 접근.
  • NUMBER, TIMESTAMP, BASEFEE, CHAINID — 블록 수준 정보.
  • BALANCE, EXTCODESIZE, EXTCODECOPY — 다른 계정 조회.

가스 스케줄

모든 옵코드에는 가스 비용이 있습니다. 가스는 두 가지 목적을 제공합니다: 무한 루프를 방지하고(정지 문제) 계산 자원의 공정한 가격 책정.

가스 비용은 등급으로 분류됩니다:

등급가스예시
Zero0STOP, RETURN, REVERT
Base2ADDRESS, ORIGIN, CALLER
Very Low3ADD, SUB, LT, GT, AND, OR, POP
Low5MUL, DIV, MOD
Mid8ADDMOD, MULMOD, JUMP
High10JUMPI
Special가변SLOAD, SSTORE, CALL, CREATE

비용이 높은 옵코드는 상태를 건드리는 것들입니다:

// 상태 접근 가스 비용 (EIP-2929 이후):
// SLOAD (cold):   2100 가스
// SLOAD (warm):    100 가스
// SSTORE (cold, 0->nonzero): 22100 가스
// SSTORE (warm):   100 가스 (+ 0->nonzero인 경우 20000)
// CALL (cold):    2600 가스
// CALL (warm):     100 가스
// BALANCE (cold): 2600 가스
// BALANCE (warm):  100 가스

Cold vs Warm 접근 (EIP-2929)

EIP-2929(베를린 업그레이드, 2021년 4월)는 접근 목록 개념을 도입했습니다 — 트랜잭션별로 접근된 주소와 스토리지 슬롯의 집합입니다.

트랜잭션 내에서 스토리지 슬롯이나 외부 주소에 처음 접근하면 “cold“이며 추가 가스가 부과됩니다. 이후 접근은 “warm“이며 저렴합니다. 이것이 스토리지 슬롯을 읽는 순서가 가스 최적화에 중요한 이유입니다.

// Solidity에서 이 패턴은 비용이 높습니다:
function bad() external view returns (uint256) {
    // 슬롯 첫 읽기: 2100 가스 (cold)
    uint256 a = myStorage;
    // ... 일부 로직 ...
    // 두 번째 읽기: 100 가스 (warm)
    uint256 b = myStorage;
    return a + b;
}

// 메모리에 캐시:
function good() external view returns (uint256) {
    uint256 cached = myStorage; // 2100 가스 (cold), 한 번만
    return cached + cached;     // 6 가스 (ADD + DUP)
}

실행 흐름: 트랜잭션에서 일어나는 일

컨트랙트를 호출하는 트랜잭션을 보낼 때 전체 실행 순서는 다음과 같습니다:

  1. 트랜잭션 검증 — 논스 확인, 잔액 >= value + gas * gasPrice, 서명 확인.
  2. 내재 가스 차감 — 트랜잭션 자체에 21000 가스, 비영 콜데이터 바이트당 16 가스, 영 바이트당 4 가스.
  3. 컨텍스트 설정 — EVM이 실행 컨텍스트 생성: 코드, 콜데이터, 호출자, 값, 남은 가스.
  4. 프로그램 카운터 0에서 시작 — EVM이 위치 0의 옵코드를 읽고 실행.
  5. 순차 실행 — 각 옵코드가 실행되고 가스가 차감됨. JUMP과 JUMPI는 비선형 제어 흐름을 허용하지만 JUMPDEST로 표시된 위치로만.
  6. 종료 — STOP(성공, 반환 데이터 없음), RETURN(성공, 반환 데이터 포함), REVERT(실패, 상태 되돌림), 또는 가스 소진으로 실행 종료.
  7. 상태 커밋 또는 롤백 — 성공 시 모든 상태 변경 커밋. 되돌림 시 이 호출 컨텍스트 내 모든 변경 롤백.

프로그램 카운터와 JUMP

프로그램 카운터(PC)는 바이트코드의 현재 위치를 추적하는 암시적 레지스터입니다. 대부분의 옵코드는 PC를 1(또는 PUSH 옵코드의 경우 1 + N)만큼 진행합니다. 두 옵코드가 PC를 직접 수정합니다:

  • JUMP — 스택에서 대상을 팝하고 PC를 해당 값으로 설정. 대상에는 JUMPDEST 옵코드가 있어야 하며, 그렇지 않으면 트랜잭션이 되돌림.
  • JUMPI — 조건부 점프. 대상과 조건을 팝. 조건이 0이 아니면 점프, 아니면 순차 진행.

이것이 EVM이 if/else, 루프, 함수 디스패치를 구현하는 방법입니다. Solidity 컴파일러는 콜데이터의 처음 4바이트를 로드하고, 알려진 함수 서명과 비교하며, 일치하는 코드 블록으로 JUMPI하는 함수 선택자를 생성합니다.

// 함수 디스패치 (간소화된 바이트코드):
// 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

서브 호출: CALL, STATICCALL, DELEGATECALL

컨트랙트는 세 가지 호출 옵코드를 사용하여 다른 컨트랙트를 호출할 수 있습니다:

  • CALL — 표준 호출. 자체 스택과 메모리를 가진 새 실행 컨텍스트 생성. 피호출자가 되돌리면 그 변경만 롤백.
  • STATICCALL — 읽기 전용 호출(EIP-214). 피호출자 내에서 상태 수정 옵코드(SSTORE, CREATE, LOG, SELFDESTRUCT)를 사용하면 즉시 되돌림.
  • DELEGATECALL — 피호출자의 코드를 실행하되 호출자의 스토리지 컨텍스트에서. msg.sender와 msg.value는 원래 호출에서 보존. 이것이 프록시 패턴과 라이브러리의 작동 방식.

각 호출 옵코드는 7개의 스택 인수를 받습니다: gas, address, value(STATICCALL/DELEGATECALL 제외), argsOffset, argsLength, retOffset, retLength. 가스 비용은 100(warm) 또는 2600(cold) + 값 전송 추가 요금.

가스 환불과 한도

역사적으로 스토리지 슬롯을 nonzero에서 zero로 정리하면 가스 환불이 제공되었습니다. EIP-3529(런던 업그레이드) 이후, 최대 환불은 총 사용 가스의 20%로 제한됩니다(이전 50%에서). 이로 인해 환불을 악용한 가스 토큰 차익거래(CHI, GST2)가 사라졌습니다.

남은 환불 소스:

  • SSTORE: nonzero 슬롯을 zero로 설정하면 4800 가스 환불.
  • SELFDESTRUCT: EIP-3529에서 환불 소스로 제거.

MEV를 위한 실용적 시사점

MEV 봇을 구축한다면, 옵코드 수준에서 EVM을 이해하는 것은 선택이 아닌 경쟁적 요구사항입니다. 봇 실행에서 절약한 모든 가스 단위가 이익 마진입니다. 핵심 인사이트:

  • 제출 전 시뮬레이션eth_call 또는 로컬 EVM(revm, EVMONE)을 사용하여 번들이 빌더에 도달하기 전에 정확한 가스 비용을 파악.
  • cold 접근 최소화 — 접근 목록(EIP-2930)을 통해 스토리지 슬롯을 사전 워밍.
  • 읽기에 STATICCALL 사용 — 약간 저렴하고 상태 변경 없음을 보장.
  • 옵코드 비용 숙지 — 잘못 배치된 SLOAD 하나가 2100 가스 비용이 될 수 있으며, 경쟁적 MEV 환경에서 이는 이익과 손실의 차이.

결론

EVM은 단순함 속에 우아합니다: 256비트 워드의 스택 머신, 평면 바이트코드 형식, 모든 연산에 가격을 매기는 가스 미터링 시스템. 이 기반 — 옵코드, 스택, 가스 — 을 이해하는 것이 시리즈의 나머지 모든 것의 전제 조건입니다: 메모리 레이아웃, 스토리지 최적화, 보안 기본 요소, 그리고 최종적으로 원시 Yul과 Huff 작성.

다음 기사에서는 EVM의 네 가지 데이터 위치를 탐구합니다: 스택, 메모리, 스토리지, 콜데이터 — 그리고 올바른 것을 선택하는 것이 컨트랙트 비용을 $0.50로 할지 $50로 할지를 결정하는 이유.