
함수 호출 시 필요한 데이터를 임시로 저장하는 메모리 영역
후입선출(LIFO: Last In First Out) 구조
높은 주소 → 낮은 주소 방향으로 데이터 저장
| 구분 | 역할 |
|---|---|
| 매개변수 전달 | 함수 호출 시 인자(매개변수) 저장 |
| 반환주소 저장 | 함수 실행 후 돌아갈 위치 저장 |
| 지역변수 저장 | 함수 내부에서 사용하는 변수 저장 |
| 레지스터 보존 | 호출자/피호출자 레지스터 값 임시 저장 |
| 레지스터 | 역할 |
|---|---|
| rsp | Stack Pointer, 스택 최상단 가리킴 |
| rbp | Base Pointer, 스택 프레임의 기준점 |
| rip | Instruction Pointer, 다음 명령어 위치 |
각 함수가 독립적으로 사용할 수 있도록 구성된 스택 영역
함수 호출 시 생성 → 함수 종료 시 정리
| 구분 | 위치 |
|---|---|
| 반환주소 | 호출한 위치 주소 |
| 이전 rbp | 이전 함수 스택 프레임 기준점 |
| 매개변수 | 호출자가 넘긴 인자값들 |
| 지역변수 | 해당 함수가 사용하는 변수들 |
%include "io64.inc"
PRINT_DEC, PRINT_STRING, NEWLINE 사용 가능)section .text
global CMAIN
section .text: 실행 코드 영역global CMAIN: 프로그램 진입점 지정CMAIN:
mov rbp, rsp ; 현재 스택 위치를 RBP에 저장 (디버깅용)
;push 1
;push 2
;push 3
;pop rax
;pop rbx
;pop rcx
push 1 ; 두 번째 인자 (1)
push 2 ; 첫 번째 인자 (2)
call MAX ; MAX 함수 호출
call MAXcall이 자동으로 ret 주소 pushMAX 진입PRINT_DEC 8, rax
NEWLINE
add rsp, 16 ; 인자 2개 pop (8+8=16)
rax에 반환된 결과 출력add rsp, 16);pop rbx
;pop rax
push rax, push rbx로 보존 후 복원 가능xor rax, rax
ret
MAX:
push rbp ; 이전 rbp 저장 (호출자 프레임 보존)
mov rbp, rsp ; 나의 프레임 시작점 설정
rbp+16, rbp+24로 인자 접근 가능 (64bit 환경)mov rax, [rbp+16] ; 첫 번째 인자 (2)
mov rbx, [rbp+24] ; 두 번째 인자 (1)
rbp+16: call 직후 push된 첫 인자rbp+24: 그 위에 push된 두 번째 인자cmp rax, rbx
jg L1
mov rax, rbx ; rbx가 더 크면 rax에 저장
L1:
pop rbp
ret
call했던 다음 명령어로)section .data
msg db 'Hello World', 0x00
스택 구성 예시 (64비트 환경)
[rbp+24] → 두 번째 인자 (1)
[rbp+16] → 첫 번째 인자 (2)
[rbp+8] → 반환주소 (call 자동 push)
[rbp] → 이전 함수의 rbp
↑
rsp (현재 스택 top)
| 단계 | 동작 |
|---|---|
| ① | 인자 push (역순) |
| ② | call로 진입 (ret 주소 push) |
| ③ | rbp push (이전 프레임 보존) |
| ④ | 매개변수 읽기 (rbp+16, rbp+24) |
| ⑤ | 로직 처리 (비교 & 반환값 저장) |
| ⑥ | rbp pop (프레임 복원) |
| ⑦ | ret (호출자 복귀) |
| ⑧ | add rsp로 인자 정리 |
좋아요! 주신 어셈블리 코드, 블로그 노트(스택/스택 프레임 정리), 강의 텍스트 본을 한데 모아, 실습·개념·디버깅·퀴즈까지 포함한 “한 번에 끝내는 x86-64 스택 & 스택 프레임 학습자료”로 재구성했습니다.
(모두 NASM + io64.inc 기준, 64비트 관점으로 설명합니다.)
push/pop, call/ret, rbp/rsp로 매개변수·반환주소·지역변수 위치를 정확히 계산한다.LIFO(후입선출), 높은 주소 → 낮은 주소 방향으로 “확장”.
함수 호출 시
push하면 8바이트 감소, pop하면 8바이트 증가.push rbp ; 이전 프레임의 기준점 저장
mov rbp, rsp ; 내 프레임의 기준점 고정
; (필요 시) sub rsp, locals ; 지역변수 공간 확보
; ... 함수 본문 ...
; (필요 시) add rsp, locals ; 지역변수 반납
pop rbp
ret
call func는 반환주소 8바이트를 자동 push.
진입 직후 프레임 기준(64비트, RBP 사용 시) 주소:
[rbp + 8] : 반환주소[rbp + 16] : 1번째 인자[rbp + 24] : 2번째 인자지역변수는 rbp - 8, rbp - 16 … 처럼 음수 오프셋으로 잡는다.
%include "io64.inc"
section .text
global CMAIN
CMAIN:
mov rbp, rsp ; (디버깅용) 현재 스택 위치를 RBP에 맞춰보기
; [데모] 스택으로 인자 전달하여 MAX 호출
push 1 ; 두 번째 인자
push 2 ; 첫 번째 인자
call MAX ; call이 반환주소를 push
PRINT_DEC 8, rax ; 반환값 출력 (rax에 최대값)
NEWLINE
add rsp, 16 ; 호출자 정리(caller clean-up): 인자 2개 반납(2 * 8B)
xor rax, rax
ret
MAX:
push rbp
mov rbp, rsp
; [rbp+16] = 첫 번째 인자, [rbp+24] = 두 번째 인자
mov rax, [rbp+16]
mov rbx, [rbp+24]
cmp rax, rbx
jg .L1
mov rax, rbx
.L1:
pop rbp
ret
section .data
msg db 'Hello World', 0
call 직후 스택엔 [반환주소][인자1][인자2]…가 쌓여 있음. 인자 정리를 안 하면 RET가 엉뚱한 주소로 돌아가서 크래시.push rbx / … / pop rbx로 보존하는 습관 권장.rbp - N 구간 확보 후 사용.| 항목 | Windows x64 | System V AMD64 (리눅스/맥) | 본 학습 코드 |
|---|---|---|---|
| 1~4번째 정수 인자 | RCX, RDX, R8, R9 | RDI, RSI, RDX, RCX (그다음 R8, R9) | 스택으로 직접 push |
| 호출자 “쉐도우 스페이스” | 32바이트 필요 | 없음 | 사용 안 함 |
| 보존 레지스터(예) | RBX, RBP, RDI, RSI, R12~R15 | RBX, RBP, R12~R15 | RBX를 사용했으면 보존 권장 |
| 스택 정렬 | 호출 시 16B 정렬 유지 | 호출 직전 rsp % 16 == 8 (call 후 함수 진입 시 16B 정렬) | 데모라 엄격히 안 맞췄지만, 실전은 정렬 고려 |
TIP: 학습 목적으론 “스택으로 인자 전달”이 오프셋 구조를 배우기에 가장 쉽습니다.
실전(특히 C/C++ 연동)에서는 각 OS의 ABI를 반드시 준수하세요.
인자 정리 확인
add rsp, 8*n으로 반드시 반납.프로로그/에필로그 쌍
push rbp ↔ pop rbp 짝 맞는지, 중간에 ret 경로 누락 없는지.보존 레지스터
push/pop으로 보존.스택 정렬
RSP를 기준으로 주소계산 금지
push가 끼면 모든 오프셋이 틀어짐 → RBP 기준으로만.MIN 함수 만들기 (워밍업)MAX를 복제해 MIN으로 바꾸고, 비교 분기만 반대로 작성.push 7, push 3, call MIN → 결과 3 확인.[rbp+16], [rbp+24] 위치는 그대로, 비교만 반대로.MAX3(a,b,c)push c → push b → push a → call MAX3[rbp+16]=a, [rbp+24]=b, [rbp+32]=crax ← max(a,b) 후 rax와 c 비교.sub rsp, 16으로 16바이트 지역영역 확보.[rbp-8]에 중간 최대값을 저장했다가 다시 꺼내 비교.add rsp, 16로 반납.MAX 시작 시 push rbx → 끝에 pop rbx.rbx 사용 로직을 넣어도 호출자 레지스터 오염 없음.; MAX(a, b): rax에 최대값 반환
MAX:
push rbp
mov rbp, rsp
; 보존 레지스터 사용 시 보존
push rbx
; (옵션) 지역변수 16바이트 확보
sub rsp, 16
mov rax, [rbp+16] ; a
mov rbx, [rbp+24] ; b
cmp rax, rbx
cmovl rax, rbx ; rax < rbx 이면 rax ← rbx (분기 없는 조건이동)
; 지역변수 반납
add rsp, 16
pop rbx
pop rbp
ret
cmov*는 분기 오염(branch mispredict) 줄이는 데 유리하고, 코드도 간결합니다.
실전 C/C++ 연동 시 필수. (RCX=첫째, RDX=둘째)
; MAX_RCX_RDX(rcx=a, rdx=b) → rax = max(a,b)
MAX_RCX_RDX:
push rbp
mov rbp, rsp
push rbx ; 보존
; 쉐도우 스페이스(필요 시) sub rsp, 32 ; 호출자 쪽에서 확보하는 게 규약이지만
; 독립 테스트용으로 잡아도 됨(호출 규약에 유의)
mov rax, rcx
mov rbx, rdx
cmp rax, rbx
cmovl rax, rbx
; add rsp, 32
pop rbx
pop rbp
ret
함수 MAX 진입 직후 (64-bit, RBP 고정 프레임)
↑ 높은 주소
| [rbp+24] = b (두 번째 인자)
| [rbp+16] = a (첫 번째 인자)
| [rbp+8] = 반환주소
rbp→| [rbp] = 이전 RBP (push rbp로 저장됨)
| [rbp-8] = 지역변수1 (예시)
| [rbp-16] = 지역변수2 (예시)
↓ 낮은 주소
Q. MAX에서 [rbp+8]엔 무엇이 있나요?
A. 반환주소. call이 자동으로 push한 값.
Q. push 1 → push 2 후 call MAX를 했는데, MAX 내부에서 첫 번째 인자는 어디?
A. [rbp+16] (그다음 인자 [rbp+24]).
Q. add rsp, 16을 빼먹으면 왜 크래시가 날 수 있나요?
A. 인자들이 스택에 남아 RET가 잘못된 주소로 복귀하려 하기 때문.
Q. RSP 기준으로 매개변수 주소를 잡으면 왜 위험한가요?
A. 중간에 push/pop이 끼면 RSP가 변해서 오프셋이 뒤틀림.
Q. x64에서 보존해야 할 레지스터 예시는?
A. RBX, RBP(공통). (Windows: RDI, RSI, R12~R15도 보존 대상)
ret 직후 크래시add rsp, 8*n 또는 호출 규약 준수push/pop으로 보존[rbp+offset] 사용sum(n, a1, ..., an))를 스택에서 순회하며 합산.ret만 호출하면? (반환주소가 없으니 즉시 크래시!)필요하시면 위 자료를 노션/블로그용 마크다운 템플릿으로도 정리해 드릴게요.
또, 현재 코드로 Windows x64 / System V AMD64 ABI 준수 버전(레지스터 인자 전달)도 바로 만들어 드릴 수 있어요.