| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- Reversing
- 프로그래머스
- crackme
- __fastcall
- RVA
- 파이썬
- 리치헤더
- __vectorcall
- 32bit
- x32
- 실행파일
- Image dos header
- x64
- 크랙미
- Rich Header
- CodeEngn
- Calling Convention
- __cdecl
- Programmers
- Python
- __stdcall
- 리버싱
- image section header
- rev
- pe format
- Dos Stub
- stack frame
- ABI
- 코드엔진
- 함수 호출 규약
- Today
- Total
kj0on
[Definition] 32비트 스택 프레임 (32bit Stack Frame) 본문
0. x32 ABI
x32 ABI는 https://kj0on.tistory.com/46 참고
1. 정의
스택 프레임(Stack Frame)은 함수가 호출될 때 스택에 형성되는 하나의 논리적 메모리 블록으로, 해당 함수 실행에 필요한 정보를 일시적으로 보관해 주는 단위이다.
2. 함수 프레임 (Function Frame)
#include <stdio.h>
int fun3(void) {
return 3;
}
int fun2(void) {
fun3();
return 2;
}
int fun1(void) {
fun2();
return 1;
}
int main(void) {
printf("Hello World!\n"); // Hello World!
fun1();
return 0;
}

위 이미지는 main→printf→fun1→fun2→fun3로 깊어졌다가, fun3→fun2→fun1→main 으로 거꾸로 되돌아오는 함수의 호출 및 복귀 과정이다. 서로 다른 함수가 연쇄적으로 호출되는데 어떻게 꼬이지 않고 CPU는 다음에 돌아올 주소를 기억할까? 이 질문의 답을 확인하려면 같은 코드를 어셈블리언어로 들여다봐야 한다.

어셈블리 단으로 내려가 보면 모든 함수에서 똑같은 패턴이 반복되는 것을 확인할 수 있다. 함수는 push ebp → mov ebp, esp → sub esp, <n>로 시작하며 mov esp, ebp → pop ebp → ret <n>으로 마무리한다. 이 기본 골격은 거의 모든 함수가 공유한다. 수십 개의 함수가 붙어 있어도 틀을 복사해 놓은 것처럼 동일한 코드 조각이 늘어선다. 한가지 유의할 점은 C언어 코드가 같아서 동일하게 나타나는 부분과 callee saved registers 부분(push ebx → push esi → push edi, pop ebx → pop esi → pop edi)은 제외했다.

함수의 시작에 붙는 패턴은 프롤로그(prologue), 끝 부분에 붙는 패턴은 에필로그(epilogue)로 구분할 수 있으며, 직접 작성한 코드의 로직은 두 패턴 사이의 바디(body)에 위치하게 된다. 이러한 공통된 틀은 컴파일 단계에서 컴파일러가 자동으로 삽입한다. 덕분에 모든 함수가 일관된 스택 프레임을 유지할 수 있게 되며 중첩 호출 상황에서도 데이터와 복귀 주소가 안전하게 보호된다.
| 함수 프레임 | 어셈블리언어 | 의미 |
| 프롤로그 (prologue) | push ebp mov ebp, esp sub esp, <n> |
이전 EBP를 저장하고 EBP를 기준점으로 설정한 뒤, n 바이트를 예약해 새 스택 프레임을 구축 |
| 바디 (body) | ex) mov eax, [ebp+8] add eax, 1 mov [ebp‑4], eax call printf |
연산, 조건 분기, 함수 호출 등 직접 작성한 코드가 위치. 프롤로그에서 만든 스택 프레임 안에서 로컬 변수와 인자를 사용 |
| 에필로그 (epilogue) | (leave) mov esp, ebp pop ebp (ret <n>) pop eip add esp, <n> jmp eip |
레지스터, 스택 포인터를 원래 상태로 돌려 프레임을 제거하고, 호출자에게 제어를 반환 |
위 표는 함수 프레임의 구성과 각 구간에서 실행되는 어셈블리 코드, 그리고 역할을 간단히 정리한 것이다. 다음으로 이 함수 프레임 안에서 실제로 스택 프레임이 어떻게 만들어지는지(프롤로그/바디/에필로그가 구체적으로 무엇을 하는지)를 살펴본다. 함수 프레임에서는 실제 동작보다 구조적인 관점에서 접근하는 것이 좋으며, 프롤로그, 바디, 에필로그가 어디에 위치하는지를 중심으로 파악하는 것이 효과적이다.
3. 스택 프레임 (Stack Frame)
3-1. 코드 및 스택 구조 개요
#include <stdio.h>
int to_seconds(int hour, int minute, int second) {
int result_hour = 0;
int result_minute = 0;
int result_second = 0;
int result_total = 0;
result_hour = hour * 3600;
result_minute = minute * 60;
result_second = second;
result_total = result_hour + result_minute + result_second;
return result_total;
}
int main() {
int h = 1;
int m = 30;
int s = 15;
int total = to_seconds(h, m, s);
printf("Total seconds: %d\n", total);
return 0;
}
위의 코드는 시, 분, 초를 각각 초 단위로 환산한 뒤 합산하는 단순한 흐름으로 되어 있다. 해당 코드를 토대로 스택 프레임이 어떻게 생성되고 소멸하는지를 단계별로 살펴본다.

x86 환경을 고려해 스택은 Full Descending 방식으로 동작하므로, ESP 레지스터는 데이터를 저장할 때마다 더 낮은 주소로 이동한 뒤 해당 위치에 값을 기록한다. 낮은주소와 높은 주소의 위치는 위와 같으며 주소의 간격은 4byte다. 데이터를 기록할 때는 가독성을 고려해 빅엔디언 방식을 적용한다.

본격적으로 스택을 살펴보기에 앞서 알아두면 좋은것은 스택프레임은 함수를 호출할 때 마다 생성된다는 것이다. 한가지 유의할 점은 함수당 하나의 스택프레임을 가진다는 것이 아니라 "호출할 때 마다"라는 점이다.
3-2. 스택 프레임 분석

main 함수가 시작하는 지점을 기준으로, 함수 호출에 따라 스택 프레임이 어떻게 구성되고 값이 어떻게 저장되는지를 단계적으로 살펴본다. 초기 상태는 위 이미지와 같다.

push ebp 명령은 함수의 프롤로그(prologue)에서 가장 먼저 실행되는 명령어로, 현재 EBP 레지스터의 값을 스택에 저장한다. 이 값은 함수 실행이 끝난 뒤 에필로그(epilogue)에서 원래의 EBP 값을 복구하는 데 사용된다. main도 다른 함수들과 마찬가지로 호출되는 하나의 함수다. 따라서 push ebp를 통해 저장된 값 0x0019FF08은 main 함수가 호출되기 전에 사용되던 EBP 값, 즉 main을 호출한 함수의 EBP(스택 프레임 기준 주소)를 의미한다.

mov ebp, esp는 현재 ESP 값을 EBP에 복사하는 명령어로, EBP를 ESP가 가리키는 값으로 변경한다. 결과적으로 ESP와 EBP가 가리키는 지점이 같아진다. 이 명령은 함수의 프롤로그에서 push ebp 다음에 실행되며, 이제부터 해당 함수는 EBP를 기준으로 지역 변수와 매개변수에 안정적으로 접근할 수 있게 된다. 쉽게 말해 main 함수 스택 프레임의 시작지점을 의미한다. 즉, mov ebp, esp는 기준점을 설정하는 동작이다. SFP는 Saved Frame Pointer의 약자로, 이전(호출한 함수)의 EBP를 의미한다.

sub esp, <n> 명령은 mov ebp, esp 다음에 사용되어 스택에 <n> 바이트만큼의 공간을 확보하는 역할을 하며, 이 공간은 주로 지역 변수나 임시 데이터를 저장하기 위한 용도로 사용된다. 또한 sub esp, <n>은 단순 "공간 확보" 이상의 의미를 가진다. 해당 명령어를 통해 ESP가 가리키고 있는 위치는 현재 함수가 사용하는 지역전용 작업 구역을 분리해 두는 경계라고 할 수 있다. 만약 이 경계가 없으면 함수 내부에서 call, push, saved register, 보안 검사의 동작에 따라 로컬 데이터를 덮어 버릴 수 있다. 그렇기 때문에 이 명령어는 "지역 변수를 사용하기 위한 공간 확보" 그 이상의 의미인 "로컬 데이터 보호, 호출 규약 준수, 정렬, 보안 요구 사항을 한꺼번에 충족시키기 위한 함수 전용의 안전지대" 라고 할 수 있다. 명령어에서 <n>의 값은 함수 내 지역 변수의 크기, 스택 정렬 규칙, 임시 저장 공간 확보 여부 등에 따라 달라질 수 있다. 이 값은 컴파일러가 함수의 특성에 맞게 유동적으로 결정한다.

push ebx, push esi, push edi는 함수 호출 규약에서 지정된 레지스터의 현재 값을 스택에 저장하는 명령으로, 함수가 이 레지스터들을 수정할 가능성이 있을 때 프롤로그에 포함된다. 이렇게 저장해 두면 함수 내부동작 중 레지스터를 임시 변수처럼 쓰면서 값이 변경돼도 원래 값을 에필로그에서 복원할 수 있다. 레지스터를 보존하기 위한 명령어로, 고정적으로 프롤로그에 나타나는 패턴은 아니다. 해당 코드에서는 최적화 옵션을 비활성화 했기 때문에 프롤로그에 포함되었다. 스택 프레임의 구조나 동작 원리를 이해하는 데 직접적인 영향을 주는 요소는 아니므로, 해당 글에서는 중요하게 보지 않아도 된다.

에필로그 이후에는 사용자가 작성한 main 함수의 실제 코드, 즉 바디(body) 부분이 이어진다. 이 구간에서는 시간(h), 분(m), 초(s)와 같은 변수를 선언하고 초기화하는 동작이 수행된다. 앞서 mov ebp, esp 명령으로 기준점을 설정해두었기 때문에, 이후 지역 변수들은 EBP를 기준으로 안정적으로 접근할 수 있다. 코드에는 자세하게 나타나 있지 않지만 dword ptr [ebp - <n>]으로 각 지역변수에 접근해 값을 저장한다. 시간(h)는 [ebp - 0x4]의 위치에, 분(m)은 [ebp - 0x8]의 위치에, 초(s)는 [ebp - 0xC]의 위치에 저장된다.

to_seconds 함수를 호출하기 전, push를 통해 전달할 인자를 스택에 저장하는 동작을 하고 있다. 컴파일러는 인자 값의 위치, 재사용 여부, 레지스터 여유, 호출 규약을 종합해서 인자를 어떻게 전달할 지를 결정한다. 인자값을 어떻게 어떤 순서로 전달하는지는 함수 호출규약에서 다루기 때문에 해당 글에서는 중요하게 보지 않아도 된다. push하는 부분에서 어떤 값을 스택에 쌓는지를 확인해 보면 이전에 저장한 지역변수의 위치에서 시간(h), 분(m), 초(s) 값을 참조해 스택에 집어넣는것을 확인할 수 있다. 앞서 sub esp, <n>으로 지역 변수 영역과 호출 인자 영역 사이의 경계를 확보해 두었기 때문에, 인자들을 push해도 로컬 데이터와 겹칠 위험 없이 안전하게 저장할 수 있다.

함수에 전달할 인자를 모두 스택에 push 한 뒤, to_seconds 함수를 호출하는 동작으로 이어진다. call 명령어에는 중요한 동작이 포함되어 있는데, 현재 EIP값(다음에 실행될 명령의 주소 0x41163F)을 스택에 push한다는 점이다. 이 값은 함수 실행이 끝난 후 에필로그 단계에서 ret 명령을 통해 복원되며, 호출 지점으로 정확히 되돌아가기 위해 사용된다. EIP와 SFP는 스택 프레임에서 매우 중요한 요소로, 함수 호출 이전 상태를 기억하는 일종의 세이브 포인트 역할을 한다.

call 명령어는 내부적으로 두 가지 동작으로 구성되어 있다. 먼저 현재 EIP 값을 스택에 push하고, 이어서 호출 대상 함수의 주소로 jmp한다. 즉, call to_seconds는 실제로는 push eip → jmp to_seconds 형태로 동작한다. 따라서 EIP는 to_seconds의 시작 지점에 대한 값으로 변경되며 이후 to_seconds 함수 코드가 실행된다. to_seconds에서 처음 실행되는 명령어를 보면 다시 에필로그가 나타난다는 것을 확인할 수 있다. main의 시작지점과 동일하게 push ebp를 통해 현재 EBP를 스택에 저장한다. 이 값은 to_seconds 함수가 호출되기 전에 사용되던 EBP 값, 즉 main 함수의 EBP(스택 프레임 기준 주소 0x19FEE8)를 의미한다.

mov ebp, esp를 통해 기준점을 main 함수에서 to_seconds 함수로 설정한다. 해당 EBP를 기준으로 to_seconds 내부의 지역 변수와 매개변수에 안정적으로 접근할 수 있게 된다. 컴파일러는 자신의 스택 프레임 안에서만 EBP와 상대 오프셋을 사용해 데이터를 다루도록 설계되어 있기 때문에, main의 지역 변수 주소 범위는 to_seconds 함수에서 다루지 않게 된다.
왜 함수 호출마다 EBP를 갱신하는가? (GPT)
1. 배경 ― “고정 EBP 모델”이란?
- 전제 : 스레드마다 스택의 시작 지점을 StartEBP로 고정하고,
모든 함수가 EBP를 옮기지 않은 채 매개변수·지역변수 주소를
StartEBP – (depth × frameSize) ± 상수 오프셋 방식으로 계산한다. - 목표 : 함수 호출 시 mov ebp, esp를 생략하고도 재귀·중첩 호출이 가능하도록 하려는 가정.
2. 실행 단계에서 나타나는 문제점
| ① 주소 계산 부담 | • 스택을 참조할 때마다 현재 호출 깊이(depth) 를 구해 레지스터에 로드• depth × frameSize 산술 계산 후 StartEBP에 더·빼기 | → 명령어 수·ALU 사용량 증가 → 성능 저하, 코드 부피 증가 |
| ② depth 값의 동적 특성 | 재귀·분기·루프 때문에 호출 깊이는 컴파일 시점에 예측 불가 | → 실행 중 계산 또는 최악‑깊이 가정이 필요 |
| ③ 컴파일 타임 하드코딩의 비현실성 | “모든 깊이별 오프셋”을 미리 박으려면• 최대재귀 N × frameSize 만큼 스택 예약• 함수마다 N 단계용 코드·테이블 중복 | → 스택 공간과 코드 크기 폭발적 낭비 |
| ④ 멀티스레드·레지스터 압박 | 스레드별 depth 변수와 StartEBP 유지 → 레지스터 고정 또는 메모리 전역 접근 필요 → 스레드 동기화 비용 | → 동시성에서 성능 손실 |
| ⑤ 디버깅·언와인드·보안 기능 붕괴 | • Saved‑EBP 체인 부재 → 전통적 스택 추적·예외 언와인드 불가• 스택 쿠키, CET Shadow Stack 등 프레임 단위 보안 장치 재설계 필요 | → 툴체인·OS 대규모 수정 |
3. 성능·메모리·유지보수 영향
| 주소 계산 | [ebp±disp] → 하드웨어가 1 사이클 처리 | mov/imul/lea 등 여러 명령 필요 |
| 명령어 크기 | 2–3 바이트(짧은 disp) | 8–12 바이트 이상 |
| 스택 공간 | 호출 깊이에 맞춰 필요한 만큼만 즉시 사용 | 최악‑깊이 가정해 미리 예약 |
| 디버깅·예외 처리 | Saved‑EBP 체인으로 즉시 가능 | 별도 CFI/스택맵 구축 필요 |
| 보안 장치 | 스택 쿠키·Shadow Stack 그대로 사용 | 커스텀 보안 메커니즘 재작성 |
| 레지스터 사용 | EBP 고정, 나머지 자유 | depth·StartEBP 보관용 레지스터 고정 |
4. 결론 ― EBP를 매 호출마다 이동하는 근본적 이유
- 가장 짧고 빠른 주소 계산 – [EBP ± disp] 변위는 하드웨어가 무료로 처리한다.
- 동적 호출 격리 – 재귀·중첩·멀티스레드에서도 로컬 데이터가 절대 겹치지 않는다.
- 완전한 호환성 – 기존 디버거, 예외 언와인더, 스택 가드, CET Shadow Stack과 그대로 연동된다.
- 메모리·코드 효율 – 프레임마다 필요한 만큼만 스택을 쓰고, 함수 코드도 최소화된다.
따라서 “EBP 고정 + depth‑기반 오프셋” 모델은 이론적 실행 가능성은 있으나
성능 손해, 메모리 낭비, 툴체인·보안 재설계라는 대가가 너무 커 실무에서는 채택되지 않으며,
호출마다 EBP를 갱신하는 현행 스택‑프레임 방식이 실용성·효율·안정성 면에서 모든 면에서 우수하다는 결론이 내려진다.

sub esp, <n>의 명령어로 to_seconds 함수 전용 안전지대의 경계를 설정한다. 이 명령어를 통해 함수 내부에서 지역 데이터를 안전하게 보호하면서 접근할 수 있게 된다.

레지스터를 보존하기 위한 명령어로, 원래 값을 에필로그에서 복원할 수 있다.

실제 to_seconds의 코드가 존재하는 바디부분이다. main과 마찬가지로 앞서 mov ebp, esp를 통해 설정한 기준점 EBP를 통해 오프셋으로 지역변수에 접근한다. 각 변수를 선언하고, 값을 0으로 초기화 하는 동작을 수행한다.

이는 시간(h)을 초 단위로 환산하는 코드로, imul 명령을 통해 연산을 수행한 뒤 결과를 EAX 레지스터에 저장하고, 이를 [ebp - 0x4] 위치에 기록한다. 이 과정에서 필요한 시간(h) 값은 main 함수에서 push한 인자를 사용하며, 호출된 함수는 EBP를 기준으로 스택 상의 위치에 접근해 읽어온다. 이처럼 호출된 함수는 고정된 프레임 구조를 기반으로 인자에 접근하므로, 함수 실행 중에도 안정적으로 값을 참조할 수 있다.

분(m)을 초 단위로 환산하는 코드로 마찬가지로 EBP를 기준으로 스택 상의 위치에 접근해 읽어온다. 해당 연산 결과를 [ebp - 0x8] 위치에 기록한다.

초(s)를 읽어서 기록하는 코드로, EBP를 기준으로 스택 상의 위치에 접근해 읽어온다. 해당 값을 [ebp - 0xC] 위치에 기록한다.

앞선 연산 결과를 모두 더해주는 코드로, 시간(h), 분(m), 초(s)를 모두 합산한 결과를 [ebp - 0x10] 위치에 기록한다.

return을 통해 함수의 연산 결과 값이 호출자에게 전달 될 수 있도록 한다. 이는 주로 EAX 레지스터에 결과를 저장하는 방식으로 이루어진다. 중요하게 봐야 할 점은 return 구문 자체가 실제 어셈블리 수준에서는 mov eax, <n>와 같은 명령으로 변환된다는 것이다. 제어가 원래 위치로 복귀하는 동작은 에필로그 단계에서 수행된다.

앞서 push해 두었던 레지스터를 복원하는 코드로, 고정적으로 에필로그에 나타나는 패턴은 아니다. 해당 코드에서는 최적화 옵션을 비활성화 했기 때문에 에필로그에 포함되었다. 스택 프레임의 구조나 동작 원리를 이해하는 데 직접적인 영향을 주는 요소는 아니므로, 해당 글에서는 중요하게 보지 않아도 된다.

mov esp, ebp 명령은 함수의 에필로그(epilogue)에서 가장 먼저 실행되는 명령어로, EBP 값을 ESP에 복사하여 ESP를 EBP가 가리키는 값으로 변경한다. 결과적으로 함수 실행 중에 변경되었던 ESP가 EBP(to_seconds 함수의 스택프레임 기준점)으로 복원된다. 이는 sub esp, <n>을 통해 설정했던 경계를 해제하는 작업이며, 스택을 정리해서 확보했던 공간을 더 이상 사용하지 않겠다는 의미다. 이 과정은 단지 ESP 값을 되돌리는 것일 뿐, 스택에 저장되어 있던 실제 데이터를 삭제하거나 지우는 동작은 수행하지 않는다. 기존 값들은 메모리에 그대로 남아 있지만, 이후 다른 동작으로 인해 새로운 데이터가 덮어씌워진다. 따라서 ESP 값을 되돌린다는 것은 논리적으로 그 공간이 더 이상 유효하지 않은 상태가 된다는 것을 의미한다.

pop ebp 명령은 스택에 저장되어 있던 SFP를 다시 EBP에 복원하는 역할을 한다. 이 명령어 하나로 EBP는 to_seconds 함수 호출 이전의 main의 EBP 값이 복원되고, ESP는 자동으로 반환 주소(EIP)를 가리키게 되어 복귀 준비까지 완료된다. 매우 간단한 코드 한 줄 인데 치밀하고 정교한 설계 덕분에 이 짧은 코드가 수행하는 논리적 동작은 엄청나다.

ret <n> 명령은 내부적으로 세 가지 동작 pop eip → add esp, <n> → jmp eip으로 구성된다. 이 중 첫 번째인 pop eip는 call 명령어로 함수에 진입할 때 스택에 저장해 두었던 복귀 주소(EIP)를 꺼내어 복원하는 단계다. mov esp, ebp와 pop ebp의 과정을 거쳐 ESP는 main에서 call to_seconds를 할 때 스택에 저장해 두었던 EIP의 위치를 가리키게 된다. pop eip를 통해 현재 EIP를 갱신하며, 제어 흐름을 함수 호출 직후 위치로 되돌아갈 준비가 완료된다. ret를 pop eip → jmp eip 처럼 풀어 쓰는 것은 개념을 나누어 설명하기 위한 표현일 뿐, 실제로는 pop eip만으로도 흐름이 바뀐다.

add esp, <n> 명령은 ret <n>의 두 번째 단계로 함수 호출 시 스택에 쌓였던 인자들을 정리하는 역할을 한다. <n> 바이트만큼 ESP를 증가시켜 인자들이 차지했던 공간을 건너뛰며 스택을 정리한다. 이 동작을 통해 함수 호출 전(인자 push 전)의 스택 상태로 복구할 수 있게 된다. 단, 인자 정리 방식은 함수 호출 규약에 따라 달라질 수 있다. (ret <n>에서 <n>이 있을 경우에만 add esp, <n>을 실행한다.)

pop eip가 실행되면서 제어가 다시 main으로 넘어가고, to_seconds 함수에서 return result_total; 을 통해 EAX 레지스터에 담아 둔 연산 결과도 그대로 유지된다. 이어서 이 EAX 값을 [ebp-0x10] 위치에 저장하는데, 이 코드가 정상적으로 동작하는 이유는 to_seconds의 에필로그 과정에서 이미 main의 EBP가 복원되었기 때문이다. 따라서 [ebp-0x10]가 main의 지역 변수 영역을 가리키게 된다. 결과적으로, 반환된 값이 main의 로컬 변수로 전달된다.

printf 호출 시에도 마찬가지로, 먼저 포맷 문자열과 인자들이 push되어 스택에 적재되고, EIP와 SFP가 차례로 스택에 저장된다. 이어서 printf 내부의 프롤로그가 새로운 스택프레임을 설정하고, 바디에서 포맷 문자열을 해석해 콘솔(cmd) 창에 지정된 문자열을 출력한다. 에필로그 단계에서 ESP와 EBP를 원래 상태로 복원한다. 마지막으로 ret이 실행되면 printf를 호출 할 때 저장된 EIP가 복원되어 호출 지점으로 되돌아가면서 제어가 다시 main 함수로 넘어온다.

to_seconds와 달리 printf는 가변 인자 함수이기 때문에, 호출이 끝난 후 스택에 쌓인 인자들을 호출자인 main 함수에서 정리해야 한다는 차이점이 있다. 해당 내용은 함수 호출 규약에서 자세하게 다루기 때문에 이 글에서는 중요하게 보지 않아도 된다.

앞서 main 함수의 프롤로그에서 push해 두었던 레지스터를 복원하는 코드가 실행된다.

main 함수도 다른 함수들과 마찬가지로 호출되는 함수이기 때문에 스택 프레임을 구성하고 해제하는 과정 역시 동일하다. mov esp, ebp 명령은 sub esp, <n>를 통해 설정했던 경계를 해제하고 확보했던 지역 변수 및 임시 공간을 정리한다. 또한 ESP를 복원하는 역할도 한다.

pop ebp로 SFP를 다시 EBP에 복원한다. ESP는 자동으로 반환 주소(EIP)를 가리키게 된다. 해당 SFP와 EIP는 main 함수 진입 시 call main과 main 함수 프롤로그에서 저장해 둔 값이다.

ret의 pop eip의 동작을 통해 제어 흐름을 main 함수 호출 직후 위치로 변경한다.

x32dbg에서 확인해 보면 해당 EIP 값은 0x00411BC3으로 나타난다.

main 함수가 호출된 위치는 위와 같으며 main 함수 종료 후, 에필로그의 ret에서 pop eip를 통해 0x00411BC3의 코드가 실행된다.
3-3. 스택 프레임의 경계
한 함수가 실행되는 동안 독립적으로 점유하는 메모리 구간을 나누어 "프레임"이라 부른다. 이 경계를 명확히 해야 연속된 스택 공간 안에서 함수 호출마다 차지하는 범위를 일정하게 구분할 수 있으며, 어디까지를 하나의 단위로 묶을지를 결정할 수 있다. 즉, 스택 상에 저장된 정보 중 어느 지점을 경계로 삼아 한 함수의 프레임이 시작되고 끝나는지를 정의하는 기준이 필요하며, 이 기준에 따라 호출 흐름이나 데이터 구조를 해석하는 방식이 달라질 수 있다.
3-3-1. 구조 중심 경계

- 경계 기준
함수 프레임 구조를 고려해 함수 진입 직후 프롤로그에서 저장되는 이전 프레임의 SFP(push ebp)부터, 함수 호출 지점(call)에서 push된 반환 주소(EIP) 까지를 하나의 프레임으로 본다.
- 장점
호출 단계를 명확히 구분할 수 있다. 함수 프레임과 스택 프레임을 1대1로 대응해 호출 관계를 파악하기 용이하다.
- 단점
한 프레임안에 이전 프레임의 SFP, 로컬 변수, 호출 인자, 함수 호출 지점 EIP가 섞여 존재한다. 이로 인해 데이터의 기능별 영역 구분이 흐려지고, 함수 내에서 실제 어떤 값이 어떻게 사용되는지를 해석하기 어렵다.
3-3-2. 기능 중심 경계

- 경계 기준
함수 내부에서 실질적으로 참조하거나 사용하는 데이터를 중심으로 SFP, Local Data, EIP, Argument의 영역을 통합해 하나의 프레임으로 본다.
- 장점
함수가 실제로 읽고 쓰는 메모리 구조를 파악하기 쉬워지며, 코드의 동작 흐름과 연관 지어 의미를 해석하기에 적합하다.
- 단점
스택 프레임과 호출 단계가 1:1로 대응되지 않기 때문에, 호출 관계를 직접적으로 파악하기 어렵고 프레임을 통합해서 해석해야 호출 흐름을 재구성할 수 있다.
3-3-3. 구분

| 경계 지정 방식 | 경계기준 | 장점 | 단점 |
| 구조 중심 | 함수 프레임 구조를 고려해 함수 진입 직후 프롤로그에서 저장되는 이전 프레임의 SFP(push ebp)부터, 함수 호출 지점(call)에서 push된 반환 주소(EIP) 까지를 하나의 프레임으로 본다. | 호출 단계를 명확히 구분할 수 있다. 함수 프레임과 스택 프레임을 1대1로 대응해 호출 관계를 파악하기 용이하다. | 한 프레임안에 이전 프레임의 SFP, 로컬 변수, 호출 인자, 함수 호출 지점 EIP가 섞여 존재한다. 이로 인해 데이터의 기능별 영역 구분이 흐려지고, 함수 내에서 실제 어떤 값이 어떻게 사용되는지를 해석하기 어렵다. |
| 기능 중심 | 함수 내부에서 실질적으로 참조하거나 사용하는 데이터를 중심으로 SFP, Local Data, EIP, Argument의 영역을 통합해 하나의 프레임으로 본다. | 함수가 실제로 읽고 쓰는 메모리 구조를 파악하기 쉬워지며, 코드의 동작 흐름과 연관 지어 의미를 해석하기에 적합하다. | 스택 프레임과 호출 단계가 1:1로 대응되지 않기 때문에, 호출 관계를 직접적으로 파악하기 어렵고 프레임을 통합해서 해석해야 호출 흐름을 재구성할 수 있다. |
'Reversing > Definition' 카테고리의 다른 글
| [Definition] 64비트 스택 프레임 (64bit Stack Frame) (0) | 2025.07.15 |
|---|---|
| [Definition] 32비트 함수 호출 규약 (32bit Calling Convention) (5) | 2025.07.14 |
| [Definition] 컴파일 (Compilation) (0) | 2025.07.10 |
| [Definition] RVA to RAW (0) | 2025.07.07 |
| [Definition] VA & RVA & RAW (0) | 2025.07.06 |