블록체인 세계에서 가장 널리 사용되는 스마트 계약 플랫폼인 Ethereum Virtual Machine(EVM)은 고급 언어(예: Solidity)로 작성된 코드를 실제로 블록체인 상에서 실행 가능한 형태로 변환하고 실행하는 핵심 엔진이다. 이 과정에서 ‘바이트코드(bytecode)’와 ‘오퍼코드(opcode)’라는 개념이 등장하며, 이를 정확히 이해하는 것은 스마트 계약의 동작 원리, 가스 비용, 보안 취약점 분석 등에 있어 매우 중요하다.
먼저 “바이트코드 구조”란 무엇인가부터 살펴보자. 스마트 계약 작성자가 Solidity 등으로 코드를 작성하면, 컴파일러는 이를 EVM이 처리할 수 있는 이진 형태인 바이트코드로 변환한다. 그리고 이 바이트코드 안에는 1바이트 단위로 정의된 명령(오퍼코드)와, 그 뒤 이어지는 즉시값(immediate value) 또는 데이터(data)가 함께 병열되어 있다. learnevm.com+1
그다음으로 “오퍼코드 동작 원리”이다. EVM이 바이트코드를 실행할 때는 프로그램 카운터(PC)를 기준으로 바이트코드를 순차적으로 읽어나가며, 스택(Stack), 메모리(Memory), 저장소(Storage), 가스(Gas) 등의 실행 환경을 조작한다. Quicknode+1
이 글에서는 이더리움 EVM의 바이트코드 구조를 단계별로 분해하고, 대표적인 오퍼코드들이 실제로 어떤 동작을 하는지 살펴본다. 나아가 바이트코드 실행이 어떻게 스마트 계약 상태 변화를 이끌어내는지 그 흐름을 정리할 것이다.
바이트코드는 배포되거나 호출된 계약(Account) 내부에 저장된 실행 코드로, 16진수 문자열 형태로 표현된다. 이 바이트코드 내부에는 다음 요소들이 포함된다:
예컨대, 처음 바이트코드가 “0x60 …” 형태로 시작하면 이는 PUSH1 (0x60) 명령어가 곧 이어진다는 의미이다. learnevm.com+1
EVM은 프로그램 카운터(PC)를 0부터 시작하며, 바이트코드의 각 바이트(혹은 오퍼코드+즉시값)를 순차적으로 읽는다. 오퍼코드가 즉시값을 요구할 경우, PC는 그 즉시값 바이트만큼 추가로 이동한다. 그러나 JUMP, JUMPI 같은 제어 흐름 명령은 PC를 특정 위치로 설정한다. learnevm.com+1
계약이 생성될 때, 트랜잭션의 data 필드에 포함된 바이트코드는 “생성 코드(constructor code)”로 실행된 뒤, 실행 코드(execution code)를 반환하고, 이 반환된 코드가 실제로 블록체인 상에 저장된다. 배포 이후 호출될 때는 이 실행 코드가 호출된다. Quicknode
내부적으로는 다음과 같은 흐름이 가능하다:
0x60 80 60 40 52 60 XX …
여기서 0x60은 PUSH1, 다음 바이트 ‘80’은 즉시값 0x80, 이어서 0x60(다시 PUSH1), ‘40’(즉시값 0x40), 0x52(MSTORE) 등으로 읽힌다. Quicknode+1
오퍼코드는 EVM이 직접 해석·실행할 수 있는 기본 명령어 단위이다. 일반적으로 1바이트(8비트)를 사용하며, 각 명령에는 고정된 기능이 있고, 몇몇 명령은 즉시값이 뒤따른다. learnevm.com+1
오퍼코드는 다음 실행 환경을 제어한다:
대표적인 오퍼코드 그룹을 살펴보면 다음과 같다. Ethereum Stack Exchange+1
ADD (0x01), MUL (0x02), SUB (0x03) 등LT (0x10), GT (0x11), EQ (0x14), ISZERO (0x15)POP (0x50), MLOAD (0x51), MSTORE (0x52), SLOAD (0x54), SSTORE (0x55), JUMP (0x56), JUMPI (0x57), JUMPDEST (0x5b)PUSH1 (0x60) ~ PUSH32 (0x7f)DUP1 (0x80) ~ DUP16, SWAP1 (0x90) ~ SWAP16LOG0 (0xa0) ~ LOG4 (0xa4)CREATE (0xf0), CALL (0xf1), DELEGATECALL (0xf4), SELFDESTRUCT (0xff)간단한 예로, 바이트코드 0x60 02 60 03 01가 있다면 다음과 같이 해석할 수 있다:
0x60 02 → PUSH1 0x02 : 스택에 값 0x02 푸시0x60 03 → PUSH1 0x03 : 스택에 값 0x03 푸시0x01 → ADD : 스택에서 두 값을 꺼내 더하고, 결과(0x05)를 스택에 푸시이처럼 오퍼코드는 주로 스택 기반 구조로 동작한다. learnevm.com+1
JUMP (0x56)는 스택 최상단에 위치한 값을 PC로 설정한다.JUMPI (0x57)는 스택에서 두 값을 꺼내 첫 번째 값이 ‘조건’으로 사용되며, 조건이 참이면 두 번째 값을 PC로 설정한다.JUMPDEST (0x5b)는 점프 대상이 될 수 있는 위치를 표시한다. EVM은 안전을 위해 점프 가능한 위치를 미리 표시해야 한다. Ethereum Stack Exchange+1각 오퍼코드는 고정 가스 비용이 있으며, 일부는 동적 비용(예: 메모리 확장 비용)을 가진다. 예컨대 메모리를 확장하면 memory_size_word ** 2 / 512 + (3 * memory_size_word) 형태의 비용 산정 방식이 존재한다. evm.codes
가스가 부족하면 REVERT, INVALID 등의 명령어로 실행이 중단되고 상태 변화가 취소된다. Quicknode
STOP, RETURN, REVERT, SELFDESTRUCT 등이 호출되면 실행 종료된다. learnevm.com계약 호출 시 트랜잭션의 data 필드 첫 4바이트는 함수 선택자(function selector)이다. 이후 인수(argument)가 인코딩되어 따라온다. 계약 코드 내부에서는 CALLDATALOAD, CALLDATASIZE, CALLDATACOPY 등의 오퍼코드로 호출 데이터를 읽어 처리한다. Quicknode
계약이 생성될 때에는 생성 코드가 실행되어 최종 계약 코드를 반환하고 배포된다. 그 후에는 반환된 코드만이 실행된다. 배포용 바이트코드는 일시적이다. Quicknode
예컨대 SSTORE 명령은 스토리지 키와 값을 스택에서 꺼내서 저장소에 기록한다. 이때 저장소가 변경되며, 가스 비용이 크게 발생한다. SLOAD는 저장소 읽기를 수행한다. 저장소는 영구 상태이므로 다음 호출에서도 유지된다. evm.codes+1
오류가 발생하면 REVERT나 INVALID 등의 명령으로 실행이 중단된다. 이때는 상태 변경이 모두 취소되고, 내부적으로 반환된 데이터(혹은 없는 데이터)가 호출자에게 전달될 수 있다. Quicknode
고급 언어에서 작성한 코드라도 컴파일된 바이트코드를 보면 불필요한 오퍼코드가 삽입된 경우가 있다. 오퍼코드 비용이 직접 가스 소비로 이어지므로, 바이트코드 구조를 이해하는 것은 가스 비용 절감 및 효율적 계약 설계에 기여한다.
바이트코드 수준에서 제어 흐름을 분석하면 함수 호출 경로, 조건문 우회 가능성, 리엔트런시(Reentrancy) 가능성 등을 판단할 수 있다. 예컨대 JUMPDEST나 CALL의 사용 위치 등을 바이트코드로 파악하면 취약점 분석이 가능하다.
블록체인 탐색기에서 실제로 배포된 계약의 바이트코드를 보고 문제를 진단하거나, ABI와 일치하는지 확인할 수 있다. 바이트코드와 오퍼코드를 이해하면 “이 코드가 실제로 어떤 일을 하나?”라는 의문에 답할 수 있다.
요약하자면, EVM 바이트코드는 고급 언어 → 컴파일 → 바이트코드 형태로 변환된 후, EVM 내부의 스택·메모리·저장소 환경 위에서 오퍼코드 단위로 실행된다. 이 구조와 동작 원리를 이해하는 것은 스마트 계약의 효율성, 보안, 디버깅 모든 측면에서 필수적이다.