EVM 아키텍처와 동작 과정을 자세히 알아보고자 글을 작성합니다. 또한, 해당 글은 이더리움 Docs에서 소개하는 Ethereum EVM illustrated PPT의 내용을 참고 및 인용하여 작성되었습니다.
들어가기에 앞서 먼저 헤매지 않기 위해 선수 지식이 필요합니다.
키워드 : Memory, Stack, Smart Contract, Merkle tree, etc...
EVM은 왜 필요한가?
비트코인은 UTXO 모델과 제한된 스크립팅 언어를 사용해 단순한 송금 조건만 검증할 수 있었기 때문에, 반복문이나 동적 메모리/상태 저장 같은 복잡한 로직을 온체인에서 실행할 수 없었습니다.
이로 인해 분산형 거래소나 DApp은 비트코인 네트워크 위에서 전적으로 운용될 수 없었고, 항상 외부 서버나 오프체인 로직에 의존해야만 했습니다. 이 한계를 극복하기 위해 이더리움은 계정 기반의 통합 상태 모델을 도입하고, 그 위에서 Turing 완전성을 보장하는 가상 머신인 EVM을 설계했습니다.
EVM은 스마트 컨트랙트의 바이트코드를 네트워크 모든 노드가 똑같이 해석/실행할 수 있는 표준화된 환경을 제공하며, 여타 다른 VM과 같이 스택과 임시 데이터를 저장하는 메모리, 스토리지를 분리해 효율성과 확장성을 챙겼습니다.
EVM의 동작 과정
EVM은 트랜잭션 안에 들어오는 컨트랙트 즉 바이트 코드를 해석/실행하여 storage를 업데이트합니다.

아래 사진은 EVM의 아키텍처입니다.
먼저 가상 ROM이 존재합니다. 가상 ROM은 상태 트리에 저장된 codeHash를 외부 코드 블롭 저장소에서 로드 됩니다.
또한, Stack, Memory, PC, Gas, Storage가 존재합니다. 여기서 Storage는 키->값 매핑 인터페이스만 정의하며 실제 외부 물리적 저장소는 클라이언트 구현체가 선택해 달아 놓은 외부 DB입니다. 즉 실제 스토리지 엔진은 EVM 내부에 존재하지 않으며 EVM은 스토리지 인터페이스를 통해 외부 DB와 연동합니다.

스택
모든 작업은 스택에서 수행되며, PUSH/POP/COPY/SWAP 등과 같은 여러 명령어를 액세스 할 수 있습니다.

메모리
메모리는 선형적이며 바이트 단위로 주소 지정 가능합니다.
MSTORE/MSTORE8/MLOAD 명령어로 액세스 가능합니다. 각 명령어들의 의미는 아래와 같습니다.
- MSTORE : 메모리에 32바이트 값을 저장 (중간 연산 결과 저장)
- MSTORE8 : 메모리에 1바이트 값을 저장 (바이트 단위 데이터 조작, 문자열/바이트 열처리)
- MLOAD : 메모리에서 32바이트를 읽어 스택에 푸시 (함수 파라미터 읽기, 데이터 접근)

스토리지
스토리지는 256비트 단어를 256비트 단어로 매핑하는 키-값 저장소입니다.
SSTORE/SLOAD 명령어로 액세스 합니다. 각 명령어들의 의미는 아래와 같습니다.
- SSTORE : 32바이트(256 비트) 값을 저장 (상태 변수 변경, 배열 등 데이터 구조에 새 값 저장)
- SLOAD : 32바이트(256 비트) 값을 읽어 스택에 푸시 (컨트랙트 상태 변수 조회, 배열 등 영구 데이터 접근)

아래와 같이 EVM code는 Bytecode에서 Assembly처럼 변환됩니다.

플로우를 통해 이해해 보기
아래 사진의 플로우를 따라가면 이해가 쉬워집니다.
처음엔 스마트 컨트랙트가 배포되어 있다고 가정한 뒤 해당 컨트랙트 ByteCode를 읽어 옵니다.
// 바이트코드 & 어셈블리
0x600a600c01 // PUSH1 0x0a, PUSH1 0x0c, ADD
0x60200052 // PUSH1 0x20, PUSH1 0x00, MSTORE
0x600054 // PUSH1 0x00, SLOAD
0x600155 // PUSH1 0x01, PUSH1 0x00, SSTORE
// 동작 순서
1. PUSH1 0x0a → 스택 ← 10
2. PUSH1 0x0c → 스택 ← 12
3. ADD → 스택.pop(12+10)=22
4. PUSH1 0x20 → 스택 ← 32 (메모리 워드 크기)
5. PUSH1 0x00 → 스택 ← 0 (오프셋)
6. MSTORE → memory[0..31] = 22
7. PUSH1 0x00 → 스택 ← 0 (슬롯 키)
8. SLOAD → 스토리지[0] = (값 없음→0) → 스택 ← 0
9. PUSH1 0x01 → 스택 ← 1
10. PUSH1 0x00→ 스택 ← 0 (슬롯 키)
11. SSTORE → 스토리지[0] = 1 (영구 반영)
이렇듯 위와 같이 바이트를 읽어 어셈블 하게 실행하는 것을 확인할 수 있습니다.

큰 맥락에서 정리하자면
- 트랜잭션 수신 → Gas 한도 설정
- 코드 로드 & 환경 초기화
- to 주소 CA일 때 codeHash로 DB에서 바이트코드 불러와 가상 ROM에 적재
- PC=0, stack=[], memory=[], storageRoot 초기화
- 페치–디코드–실행 루프
- opcode = code[PC] 읽고 Gas 차감
- Stack 연산 (PUSH/POP/ADD 등)
- Memory 연산 (MLOAD/MSTORE 등) → 임시 배열/구조체/리턴 데이터 처리
- Storage 연산 (SLOAD/SSTORE) → 상태 변수 읽기/쓰기 → MPT/DB 갱신
- PC를 명령어 크기만큼 이동 (또는 JUMP)
- 종료 & 커밋
- RETURN/STOP/REVERT 만나면 종료 → memory에서 리턴 데이터 추출 → 남은 Gas 환불
- 변경된 Storage를 외부 DB에 기록해 새 stateRoot 반영
참고 자료
https://ethereum.org/en/developers/docs/evm/
https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
'Blockchain' 카테고리의 다른 글
| 이더리움 계정 기반 상태(EOA, CA) (1) | 2025.08.03 |
|---|---|
| PBFT 합의는 왜 두 번의 절차를 거쳐야만 할까? (2) | 2025.07.27 |
| Kurtosis, blockscout 알아보기 (1) | 2025.07.26 |
| 타원곡선 개인키/공개키 생성 & Go-Ethereum 내부 코드 살펴보기 (3) | 2025.07.24 |
| 블록체인을 처음 학습한다면 추천하는 오픈소스 (0) | 2023.09.13 |