EVM 스택 완벽 정리: 256비트 워드·옵코드·Stack Too Deep
EVM 스택이 이더리움 바이트코드를 실행하는 원리와 PUSH·DUP·SWAP 연산, 스택 오류 및 Solidity Stack Too Deep 대응법을 설명합니다.

Solidity만 볼 때는 드러나지 않던 저수준 작업이 트랜잭션 트레이스에서는 PUSH, DUP, SWAP, 산술 명령의 긴 행렬로 나타나요. 이때 필요한 이해의 틀이 **EVM 스택(EVM Stack)**입니다. 옵코드가 피연산자와 중간 결과를 잠시 두는 공간이며, 실용적인 블록체인 기초와 실제 바이트코드 실행을 연결해 줍니다.
구내식당의 쟁반 더미를 떠올려 보세요. 새 쟁반은 맨 위에 놓고, 꺼낼 때도 맨 위부터 꺼냅니다. 가까운 쟁반을 복사하거나 맨 위 쟁반과 자리를 바꿀 수는 있지만, 메모리 배열처럼 중간 위치를 자유롭게 읽지는 못해요. 이 후입선출(Last-In, First-Out) 제약 덕분에 실행이 단순하고 예측 가능해지지만, 트레이스와 컴파일러 오류가 처음에는 낯설게 보입니다.
EVM 스택이란 무엇인가요?
이더리움 가상머신(Ethereum Virtual Machine)은 스택 머신(Stack Machine)입니다. 이더리움 EVM 공식 문서에 따르면 피연산자 스택에는 최대 1,024개 항목이 들어가고, 각 항목은 256비트 워드예요. 한 워드는 32바이트입니다. 현재 값이 작은 정수, 주소, 불리언, 다른 데이터 영역을 가리키는 포인터여도 스택 슬롯 하나의 폭은 같습니다.
새 실행 프레임의 스택은 비어 있어요. 바이트코드 명령인 옵코드(Opcode)가 한 단계씩 스택을 바꿉니다. 대부분의 명령은 맨 위에서 입력을 꺼내고 결과를 다시 올려요. 스택은 임시 공간이므로 컨트랙트 스토리지가 아니며 프레임 종료 뒤에는 유지되지 않습니다.
이더리움에서 스택이라는 말은 서로 다른 두 한도를 가리킬 수 있어요.
- 피연산자 스택 또는 표현식 스택은 옵코드가 쓰는 값을 보관합니다.
- **호출 깊이(Call Depth)**는 실행 프레임 사이에 중첩된 메시지 호출 수를 셉니다.
두 한도에 모두 1,024라는 숫자가 등장하지만 Solidity 보안 공식 문서는 둘이 별개라고 명확히 설명해요. stack too deep 컴파일 오류 역시 런타임 스택이 1,024개에 근접했다는 뜻이 아닙니다. 보통 동시에 살아 있어야 하는 값을 명령어가 직접 닿을 수 있는 스택 구간에 컴파일러가 배치하기 어려울 때 발생해요.
스택 실행은 어떻게 작동하나요?
각 옵코드에는 정해진 스택 입력과 출력이 있습니다. 이더리움 공식 옵코드 참조표에서 그 형태를 바로 확인할 수 있어요. 예를 들어 ADD는 워드 두 개를 꺼내 2²⁵⁶을 법으로 한 합을 하나 올립니다. POP은 워드 하나를 꺼내고 아무 결과도 남기지 않아요.
아주 짧은 바이트코드를 따라가 보겠습니다.
60 02 // PUSH1 0x02 스택: [2]
60 03 // PUSH1 0x03 스택: [3, 2]
01 // ADD 스택: [5]
50 // POP 스택: []여기서는 왼쪽을 스택 맨 위로 표시했어요. 첫 PUSH1은 코드의 다음 바이트를 읽어 2를 올립니다. 두 번째 명령이 3을 올린 뒤 ADD가 3과 2를 꺼내 계산하고 5를 올려요. 마지막 POP이 그 값을 버립니다.
이 구조는 중간 답을 이름이 붙은 레지스터가 아니라 쟁반 더미에 보관하는 계산기와 비슷해요. Solidity 변수는 소스 코드를 읽기 쉽게 만들고, 컴파일러는 그 변수를 구현하는 푸시·로드·복제·메모리 이동 순서를 계획합니다.
PUSH·DUP·SWAP·POP 이해하기
네 가지 옵코드 계열을 보면 스택 모델이 가장 선명해집니다.
PUSH는 상수를 맨 위에 올려요
PUSH0은 0을 올립니다. PUSH1부터 PUSH32까지는 바이트코드 바로 뒤의 1~32바이트를 읽어 256비트 워드로 올려요. 짧은 상수는 짧은 명령으로 표현할 수 있지만, 결과가 차지하는 스택 슬롯은 여전히 한 워드입니다.
DUP는 닿을 수 있는 값을 복사해요
DUP1은 맨 위 항목을 복사하고 DUP16은 위에서 열여섯 번째 항목을 맨 위로 복사합니다. 원본은 그 자리에 남아요. 바이트코드는 이 방식으로 가까운 값을 없애지 않고 다시 씁니다.
SWAP은 가까운 순서를 바꿔요
SWAP1은 맨 위와 두 번째 항목을 맞바꿉니다. SWAP16은 맨 위와 열일곱 번째 항목을 교환해요. 스택 높이는 그대로 유지됩니다.
POP은 맨 위를 제거해요
POP은 항목 하나를 버립니다. 컴파일된 코드는 더 이상 필요하지 않은 값을 정리할 때 사용해요. 각 분기가 끝난 뒤에도 이후 명령이 기대하는 배치를 유지해야 합니다.
전통적인 DUP1DUP16, SWAP1SWAP16 범위 때문에 “스택에는 1,024개 값이 들어간다”와 “컴파일러가 작은 구간만 직접 다룰 수 있다”는 서로 다른 설명이에요. 전체 용량을 임의 접근 배열처럼 자유롭게 읽을 수 있는 것이 아닙니다.
스택 vs 메모리·스토리지·콜데이터
스택은 EVM 데이터 영역 가운데 하나일 뿐이에요. 네 영역을 같은 공간처럼 생각하면 가스 추정이 어긋나고 인라인 어셈블리가 위험해집니다.
| 영역 | 보관하는 것 | 접근 방식 | 수명 |
|---|---|---|---|
| 스택(Stack) | 피연산자와 중간 256비트 워드 | 맨 위 중심, 옵코드가 정한 접근 | 현재 실행 프레임 |
| 메모리(Memory) | 변경 가능한 임시 바이트 | 바이트 오프셋, 접근에 따라 확장 | 현재 실행 프레임 |
| 콜데이터(Calldata) | 읽기 전용 호출 입력 | 바이트 오프셋 | 현재 실행 프레임 |
| 스토리지(Storage) | 영구 컨트랙트 상태 | 256비트 키와 256비트 값 | 트랜잭션 이후에도 유지 |
스택에는 객체 전체가 아니라 포인터가 놓이는 경우가 많아요. 동적 바이트 배열은 EVM 메모리에 있고 스택 워드에는 그 오프셋이 들어갈 수 있습니다. 상태 변수는 컨트랙트 스토리지 슬롯에 있고, SLOAD는 스택에서 스토리지 키를 꺼낸 뒤 읽은 값을 다시 올려요. CALLDATALOAD도 오프셋을 받아 콜데이터에서 얻은 32바이트를 올립니다.
트레이스를 읽을 때 이 구분이 중요해요. 스택에 큰 16진수 워드가 있다고 해서 그것이 금액인지, 주소인지, 부호 있는 정수인지, 해시인지, 포인터인지 바로 알 수는 없습니다. 의미는 옵코드 순서, ABI, 실행 맥락을 함께 봐야 드러납니다.
Solidity에서 Stack Too Deep이 발생하는 이유
고수준 Solidity에서는 매개변수, 반환값, 지역 변수에 이름을 붙일 수 있어요. 컴파일러는 동시에 필요한 값을 스택 위치와 때로는 메모리에 배치해야 합니다. 너무 많은 값이 한꺼번에 살아 있어야 하면 EVM 절대 한도와 거리가 멀어도 특정 코드 생성 경로에서 stack too deep 오류가 날 수 있어요.
Solidity 공식 Yul 문서는 Yul 변수가 보통 스택 슬롯을 받고, 변수 참조가 EVM 코드의 DUP 명령으로 바뀐다고 설명합니다. 또한 안전 조건이 맞으면 직접 닿기 어려운 변수를 메모리로 옮기는 Stack Limit Evader 최적화도 설명해요.
실전 대응은 코드와 컴파일러 버전에 따라 달라집니다.
- 한 표현식이나 블록에서 동시에 살아 있는 값을 줄이세요.
- 큰 함수를 의미 있는 내부 헬퍼로 나누세요.
- API 자체가 더 명확해질 때 관련 입력을 구조체(Struct)로 묶으세요.
- 이미 구할 수 있는 값을 복제한 지역 변수를 제거하세요.
- 기본 파이프라인과 IR 기반 파이프라인을 테스트·가스 측정과 함께 비교하세요.
옵티마이저나 viaIR 설정을 오류만 없애려는 목적으로 무작정 바꾸면 안 됩니다. Solidity IR 기반 코드 생성 가이드는 두 코드 생성 경로와 의미 차이를 문서화해요. 컴파일러 버전을 고정하고 공식 알려진 버그 목록을 확인한 뒤, 배포 바이트코드와 동작을 다시 시험해야 합니다.
실패 유형과 보안 리스크
스택 언더플로(Stack Underflow)
옵코드는 존재하지 않는 값을 꺼낼 수 없어요. 공식 옐로 페이퍼 해설은 필요한 항목이 부족한 상태를 예외 종료 조건으로 규정합니다. 직접 만든 바이트코드나 분석기는 모든 분기의 입력 높이를 계산해야 해요.
스택 오버플로(Stack Overflow)
명령 실행 뒤 항목이 1,024개를 넘게 되어도 예외 종료합니다. 컴파일러가 만든 일반적인 코드에서는 방지되지만, 인터프리터·분석기·바이트코드 작성자는 합의 규칙을 정확히 구현해야 해요.
피연산자 순서 착각
덧셈은 교환법칙이 있어 순서 실수가 잘 드러나지 않습니다. 뺄셈, 나눗셈, 비교, 메모리 쓰기, 호출, 반환은 달라요. 트레이스를 볼 때 화면의 어느 쪽이 맨 위인지 추측하지 말고 해당 포크의 옵코드 표를 확인하세요.
값과 포인터 혼동
정상적으로 보이는 오프셋도 의도한 메모리나 콜데이터 범위를 벗어날 수 있어요. 스토리지 키는 전혀 다른 상태 슬롯을 가리킬 수 있습니다. 인라인 어셈블리(Inline Assembly)는 여러 고수준 검사를 우회하므로, 256비트 안에 들어간다는 이유만으로 값을 믿지 말고 길이·오프셋·자료형·반환 데이터를 검증해야 합니다.
모든 EVM 체인이 같다고 가정
EVM 호환 네트워크라도 옵코드와 가스 스케줄을 활성화하는 시점이 다를 수 있어요. 목표 네트워크에 맞춰 컴파일하고 활성 포크를 확인하세요. 잘못된 명령어 집합으로 해석한 트레이스는 오해를 만들 수 있습니다.
EVM 스택 트레이스 체크리스트
- 체인·블록·활성 EVM 리비전(Revision)을 확인했나요?
- 트레이스 화면에서 어느 쪽이 스택 맨 위인지 표시했나요?
- 각 옵코드가 꺼내는 값과 올리는 값을 기록했나요?
- 모든 분기와 점프 목적지에서 스택 높이를 추적했나요?
- 리터럴 값·메모리 오프셋·스토리지 키를 구분했나요?
- 부호와 더 작은 Solidity 자료형을 명시적으로 디코딩했나요?
- 소스 맵과 컴파일러 설정을 실제 배포 바이트코드와 대조했나요?
- 의심 경로를 로컬 포크나 디버거에서 재현했나요?
- 인라인 어셈블리와 외부 반환 데이터를 집중 검토했나요?
자주 묻는 질문 (FAQ)
EVM 스택은 영구적으로 저장되나요?
아니요. 현재 실행 프레임에만 속합니다. 영구적인 컨트랙트 상태는 스토리지에 저장해요.
스택 항목 하나는 항상 32바이트인가요?
각 항목은 256비트 워드입니다. 더 작은 Solidity 값도 한 워드를 차지할 수 있고, 저수준에서 쓸 때는 사용하지 않는 상위 비트를 정리해야 할 수 있어요. 소스 코드의 자료형 이름이 런타임 스택에 라벨로 저장되는 것은 아닙니다.
스택 깊이와 컨트랙트 호출 깊이는 같은가요?
아닙니다. 피연산자 스택과 중첩 메시지 호출 깊이는 별개의 구조예요. 한쪽의 숫자만 보고 다른 쪽 한도 문제라고 진단하면 안 됩니다.
스택이 메모리보다 항상 저렴한가요?
보편적으로 비교할 수 없습니다. 스택 조작 옵코드에는 고유 비용과 접근 제한이 있고, 메모리는 바이트 주소 기반 임시 데이터를 지원하며 확장 비용을 냅니다. 컴파일러는 두 공간을 함께 사용해요. 목표 포크에서 실제 컴파일 경로를 측정하세요.
가스를 아끼려면 원시 EVM 바이트코드를 직접 써야 하나요?
명확한 이유와 충분한 테스트가 있을 때만 고려하세요. 좁은 경우에는 추상화 비용을 줄일 수 있지만, 컴파일러 검사를 잃고 스택 균형·제어 흐름·포크 호환성·감사 가능성을 직접 책임져야 합니다.
주요 1차 자료
- Ethereum.org: Ethereum Virtual Machine
- Ethereum.org: EVM Opcode Reference
- Ethereum Yellow Paper: Formal EVM Specification
- Ethereum.org: Understanding the Yellow Paper's EVM Specification
- Solidity: Yul
- Solidity: IR-Based Code Generation Changes
- Solidity: List of Known Bugs
스택은 작은 실행 장부예요
EVM 스택은 계산의 바로 지금 상태를 기록합니다. 어떤 피연산자가 준비됐는지, 어떤 중간값이 아직 필요한지, 다음 옵코드가 무엇을 합법적으로 꺼낼 수 있는지를 보여 줘요. 바이트코드를 스택 형태가 연속해서 바뀌는 과정으로 보면 트레이스가 16진수 벽이 아니라 재현 가능한 계산으로 읽히기 시작합니다.
이 글은 교육 목적이며 투자 조언이 아닙니다. 스마트 컨트랙트 결함과 크립토 자산은 원금 전액 손실을 일으킬 수 있어요. 최신 명세를 직접 확인하고 실제 컴파일러와 포크 조합을 테스트하세요. 잃어도 되는 범위만 감수하고 스스로 조사하세요(DYOR).
함께 읽으면 좋은 글

EVM 메모리 완벽 정리: 레이아웃·확장 가스·Solidity 안전 수칙
EVM 메모리의 수명과 확장 가스 계산, Solidity 프리 메모리 포인터, 인라인 어셈블리에서 피해야 할 오류를 설명합니다.

이더리움 컨트랙트 스토리지 슬롯: 패킹·매핑·eth_getStorageAt 읽는 법
Solidity가 이더리움 컨트랙트 스토리지 슬롯을 배치하는 원리와 매핑·배열 위치 계산, eth_getStorageAt 검증 방법을 설명합니다.

이더리움 콜데이터 완전 정리: 트랜잭션 입력 데이터 해석법
이더리움 콜데이터의 함수 셀렉터와 ABI 인코딩 구조, 익스플로러 해석 원리, 컨트랙트 서명 전 확인할 보안 항목을 설명합니다.
관련 주제 살펴보기

이더리움 글램스테르담 업그레이드: ePBS·BAL과 확정된 사실
글램스테르담은 2026년 4분기 예정이에요. 확정된 범위, 세폴리아 일정, ePBS, 블록 레벨 접근 리스트와 남은 변수를 설명합니다.
크립토 주소 포이즈닝: 송금 전 지갑 주소를 검증하는 방법
주소 포이즈닝은 거래 내역에 닮은 지갑 주소를 심는 사기예요. 작동 원리와 안전한 전체 주소 검증 체크리스트를 알아보세요.