Virtual Memory 강의자료 기반
이 글은 System V AMD64 ABI(Linux, macOS) 기준입니다. Windows x64는 하단 비교표 참고
🎯 TL;DR - 핵심 요약
┌─────────────┬──────────────────────────────────────┐
│ Prologue │ push %rbp; mov %rsp, %rbp │
├─────────────┼──────────────────────────────────────┤
│ Body │ mov/add (레지스터 연산) │
│ │ 지역변수: %rbp - N │
├─────────────┼──────────────────────────────────────┤
│ Epilogue │ pop %rbp; ret │
└─────────────┴──────────────────────────────────────┘
📌 인자 전달: %rdi, %rsi, %rdx, %rcx, %r8, %r9
📌 반환값: %rax
📌 Caller-saved: %rax, %rcx, %rdx, %rsi, %rdi, %r8–%r11
📌 Callee-saved: %rbx, %rbp, %r12–%r15
📋 목차
- 함수 호출 3단계 구조
- 0단계: call 명령 직후
- 1단계: Prologue - 스택 프레임 생성
- 2단계: Body - 함수 본문 실행
- 3단계: Epilogue - 스택 복원
- 레지스터 체계 완전 분석
- Caller-saved vs Callee-saved
- 어셈블리 명령어 정리
- 심화 주제
- Windows x64 차이점
1. 함수 호출 3단계 구조
┌─────────────┬──────────────────────────────┐
│ 단계 │ 역할 │
├─────────────┼──────────────────────────────┤
│ 1. Prologue │ 스택 프레임 생성 │
│ 2. Body │ 함수 코드 실행 │
│ 3. Epilogue │ 스택 프레임 복원 및 반환 │
└─────────────┴──────────────────────────────┘
2. 0단계: call 명령 직후
스택은 높은 주소에서 낮은 주소로 성장
높은 주소 (예: 0x500)
┌──────────────────┐
│ 이전 함수의 프레임│
├──────────────────┤ ← %rbp = 0x500 (이전 기준점)
│ ... │
├──────────────────┤ ← call 직전 %rsp
│ 리턴 주소 │ ← call이 자동으로 push
├──────────────────┤ ← %rsp = 0x4f8 (현재 top)
│ │
└──────────────────┘
낮은 주소
call 명령의 동작
call function_name
내부 동작:
- push %rip (다음 명령 주소 저장)
- jmp function_name (함수로 점프)
결과:
- %rip: 호출된 함수의 시작 주소
- %rsp: 8바이트 감소 (리턴 주소 push)
- %rbp: 아직 이전 함수의 기준점
3. 1단계: Prologue
(1) 이전 프레임 포인터 저장
0x0: pushq %rbp
동작:
%rsp -= 8
[%rsp] = %rbp
스택 구조:
┌──────────────────┐
│ 이전 함수 프레임 │
├──────────────────┤
│ 리턴 주소 │
├──────────────────┤
│ 이전 %rbp (0x500)│ ← pushq 실행
├──────────────────┤ ← %rsp = 0x4f0
│ │
└──────────────────┘
(2) 새 기준점 설정
0x1: movq %rsp, %rbp
동작:
%rbp = %rsp
스택 구조:
┌──────────────────┐
│ 리턴 주소 │
├──────────────────┤
│ 이전 %rbp (0x500)│
├──────────────────┤ ← %rsp = %rbp = 0x4f0 (새 기준점)
│ │
│ (지역 변수 영역) │
└──────────────────┘
요약:
- push %rbp: 이전 기준점 백업
- mov %rsp, %rbp: 새 기준점 확립
4. 2단계: Body
(1) 함수 인자 저장 (System V AMD64)
┌─────────┬───────────┬──────────────────┐
│ 인자 │ 레지스터 │ 저장 위치 │
├─────────┼───────────┼──────────────────┤
│ 1번째 │ %rdi/%edi │ %rbp - 4 │
│ 2번째 │ %rsi │ %rbp - 16 │
│ 지역변수│ - │ %rbp - 20 (예시) │
└─────────┴───────────┴──────────────────┘
예제: int main(int argc, char *argv[])
0x4: movl $0x0, %eax # return 0 준비
0x9: movl %edi, 0xfffffc(%rbp) # argc 저장 (32비트)
0xc: movq %rsi, 0xfffff0(%rbp) # argv 저장 (64비트)
메모리 레이아웃:
├──────────────────┤ ← %rbp (0x4f0)
│ argc (32bit) │ ← %rbp - 4
│ (padding) │ ← 8바이트 정렬
│ argv (64bit) │ ← %rbp - 16
├──────────────────┤
(2) 지역 변수 연산: x = x + 3
0x10: movl 0xffffec(%rbp), %edi # x를 레지스터로 로드
0x13: addl $0x3, %edi # 레지스터에서 계산
0x19: movl %edi, 0xffffec(%rbp) # 결과를 메모리에 저장
과정:
1. 메모리(%rbp-20) → %edi (로드)
%edi = 0
2. 레지스터 연산
%edi = %edi + 3 = 3
3. %edi → 메모리(%rbp-20) (저장)
x = 3
핵심 원리:
- 모든 계산은 레지스터에서 수행
- 메모리 접근은 로드/스토어 시에만
5. 3단계: Epilogue
(1) 기준점 복원
0x1c: popq %rbp
동작:
%rbp = [%rsp]
%rsp += 8
스택 구조:
┌──────────────────┐
│ 이전 함수 프레임 │ ← %rbp = 0x500 (복원)
├──────────────────┤
│ 리턴 주소 │
├──────────────────┤ ← %rsp = 0x4f8
│ (현재 함수 자료) │
└──────────────────┘
(2) 함수 종료
0x1d: ret
동작:
%rip = [%rsp] (리턴 주소 로드)
%rsp += 8 (스택 복원)
최종 상태:
- %rbp = 0x500 (이전 함수 기준점)
- %rsp = 0x500 (call 직전 값)
- %rip = (이전 함수 코드 주소)
6. 레지스터 체계 완전 분석
레지스터 크기별 이름
┌───────────┬──────────────────────┬─────────────┐
│ 비트 크기 │ 레지스터 이름 │ 설명 │
├───────────┼──────────────────────┼─────────────┤
│ 64bit │ %rax, %rbx, %rdi... │ 전체 64비트 │
│ 32bit │ %eax, %ebx, %edi... │ 하위 32비트 │
│ 16bit │ %ax, %bx, %di... │ 하위 16비트 │
│ 8bit │ %al, %bl, %dil... │ 하위 8비트 │
└───────────┴──────────────────────┴─────────────┘
비트 구조 (%rax 예시):
63 32 31 16 15 8 7 0
├─────────────────────┼─────────────────┼────────┼────────┤
│ 상위 32비트 │ %eax │ %ah │ %al │
└─────────────────────┴─────────────────┴────────┴────────┘
└─────────────────────────────────────┘
%rax (64bit)
왜 %rdi 대신 %edi를 쓸까?
이유 1: 데이터 타입 크기
int argc → 32비트 → %edi
char *argv[] → 64비트 → %rsi
이유 2: 자동 제로 확장 (Zero Extension)
32비트 레지스터(목적지)에 값을 쓰면, 상위 32비트가 자동으로 0
movl $3, %edi
실행 전:
%rdi = 0x12345678ABCDEF00
실행 후:
%rdi = 0x0000000000000003
↑↑↑↑↑↑↑↑ 자동으로 0
⚠️ 주의: 목적지가 메모리일 때는 해당 크기만큼만 씁니다.
장점:
- 별도로 상위 비트 클리어 불필요
- C 언어 타입과 자연스러운 대응
레지스터 역할 요약
┌──────────┬────────────────────────┬───────────┐
│ 레지스터 │ 의미 │ 예시 │
├──────────┼────────────────────────┼───────────┤
│ %rip │ Instruction Pointer │ 0x10 │
│ %rsp │ Stack Pointer (변동) │ push/pop │
│ %rbp │ Base Pointer (고정) │ 기준점 │
├──────────┼────────────────────────┼───────────┤
│ %rdi/%rsi│ 함수 인자 (1~6번째) │ argc, │
│ %rdx/%rcx│ System V 규약 │ argv │
│ %r8/%r9 │ │ │
├──────────┼────────────────────────┼───────────┤
│ %rax │ 함수 리턴값 (64bit) │ return │
│ %eax │ 함수 리턴값 (32bit) │ return 0 │
└──────────┴────────────────────────┴───────────┘
7. Caller-saved vs Callee-saved
문제 상황
int foo(int x) {
int y = bar(x);
return y + 1;
}
질문: bar()가 %rdi를 덮어쓰면 foo()의 x는?
Caller-saved: "내가 저장하고 복원할게"
호출자가 책임지는 레지스터:
%rax, %rcx, %rdx, %rsi, %rdi, %r8-r11
foo()의 책임:
pushq %rdi # x 보존 (백업)
call bar # bar() 호출
popq %rdi # x 복원
구조도:
call bar 전:
Stack Registers
┌──────┐ %rdi: x (foo의 값)
│ │
└──────┘
pushq %rdi:
Stack Registers
┌──────┐
│ x │ ← push %rdi: x
└──────┘
bar() 실행:
Stack Registers
┌──────┐
│ x │ %rdi: (bar가 자유롭게 사용)
└──────┘
popq %rdi:
Stack Registers
┌──────┐
│ │ %rdi: x (복원!)
└──────┘
Callee-saved: "내가 쓰더라도 되돌려줄게"
피호출자가 책임지는 레지스터:
%rbx, %rbp, %r12-r15
bar()의 책임:
bar:
pushq %rbx # 저장 (프롤로그)
# ... %rbx 사용
popq %rbx # 복원 (에필로그)
ret
비유로 이해하기
┌─────────────────┬────────────────────┬──────────────┐
│ 구분 │ 역할 │ 비유 │
├─────────────────┼────────────────────┼──────────────┤
│ Caller-saved │ "내 물건은 내가 │ 내 가방 챙김 │
│ │ 챙긴다" │ │
├─────────────────┼────────────────────┼──────────────┤
│ Callee-saved │ "남의 물건은 내가 │ 빌린 건 │
│ │ 책임진다" │ 돌려줌 │
└─────────────────┴────────────────────┴──────────────┘
전체 정리표
┌────────────────┬──────────────────────────────┬────────────┐
│ 구분 │ 레지스터 │ 누가 보존? │
├────────────────┼──────────────────────────────┼────────────┤
│ Caller-saved │ %rax, %rcx, %rdx, │ 호출자 │
│ │ %rsi, %rdi, %r8-r11 │ │
├────────────────┼──────────────────────────────┼────────────┤
│ Callee-saved │ %rbx, %rbp, %r12-r15 │ 피호출자 │
└────────────────┴──────────────────────────────┴────────────┘
용도별:
┌────────────────┬──────────────────────────────┐
│ Caller-saved │ 임시 계산, 인자 전달 │
├────────────────┼──────────────────────────────┤
│ Callee-saved │ 지역 변수, 루프 상수 등 │
│ │ 장기 유지 값 │
└────────────────┴──────────────────────────────┘
8. 어셈블리 명령어 정리
데이터 이동 명령어
movq - Move Quadword (64비트)
movq %rsp, %rbp # %rsp의 값을 %rbp로 복사
movq %rsi, -16(%rbp) # %rsi를 메모리 [%rbp-16]에 저장
movl - Move Long (32비트)
movl $0x0, %eax # 상수 0을 %eax에 저장
movl %edi, -4(%rbp) # %edi를 메모리 [%rbp-4]에 저장
movl -20(%rbp), %edi # 메모리 [%rbp-20]을 %edi로 로드
스택 명령어
pushq - Push Quadword
동작:
1. %rsp -= 8
2. [%rsp] = source
실행 전: 실행 후:
├──────┤ ← %rsp ├──────┤
│ │ │ %rbp │ ← push된 값
├──────┤ ← %rsp
popq - Pop Quadword
동작:
1. destination = [%rsp]
2. %rsp += 8
실행 전: 실행 후:
├──────┤ ├──────┤ ← %rsp
│ 0x500│ ← 저장된 값 │ 0x500│
├──────┤ ← %rsp ├──────┤
산술 명령어
addl - Add Long (32비트)
addl $0x3, %edi # %edi = %edi + 3
addl %eax, %ebx # %ebx = %ebx + %eax
제어 흐름 명령어
ret - Return
동작:
1. %rip = [%rsp] (리턴 주소 꺼냄)
2. %rsp += 8 (스택 포인터 증가)
3. 해당 주소로 점프
실행 전: 실행 후:
├──────────┤ ├──────────┤ ← %rsp
│ 리턴 주소 │ ← [%rsp] │ 리턴 주소 │
├──────────┤ ← %rsp ├──────────┤
%rip = 리턴 주소 (점프!)
명령어 크기 접미사
┌─────────┬─────────┬──────────┐
│ 접미사 │ 크기 │ 예시 │
├─────────┼─────────┼──────────┤
│ b │ 8비트 │ movb │
│ w │ 16비트 │ movw │
│ l │ 32비트 │ movl │
│ q │ 64비트 │ movq │
└─────────┴─────────┴──────────┘
9. 심화 주제
1. 스택 정렬 (Stack Alignment)
System V x86-64 ABI 규약:
call 직후 진입한 함수에서:
%rsp ≡ 8 (mod 16)
이유:
- SSE 명령어 최적화 (16바이트 정렬 필요)
- ABI 규약 준수
구현:
# 다른 함수를 호출하기 전에 16바이트 정렬
sub $8, %rsp # 또는 sub $24, %rsp (16의 배수)
call function
add $8, %rsp
Leaf 함수(다른 함수를 호출하지 않는 함수)는 정렬 조정 생략 가능
2. 프레임 포인터 생략
-fomit-frame-pointer 최적화 옵션:
차이점:
- %rbp를 범용 레지스터로 사용
- %rsp 기준으로만 지역 변수 접근
- 프롤로그/에필로그 단순화
장점:
- 레지스터 하나 더 사용 가능
- 코드 크기 감소
단점:
- 디버깅 어려움 (스택 추적 불가)
- 프로파일링(perf, frame pointer unwinding) 제약
- 특히 디버깅/프로파일링 목적이라면 프레임 포인터 유지 권장
3. Red Zone (레드존)
System V x86-64 전용 (⚠️ Windows x64에는 없음)
정의:
- %rsp 아래 128바이트 영역
- Leaf 함수가 임시 사용 가능
- push/pop 없이 사용 가능
조건:
✅ call을 하지 않는 함수 (leaf function)
✅ 일반 사용자 모드 (비동기 시그널/인터럽트 주의)
❌ 커널 모드
❌ 시그널 핸들러
예시:
addl %edi, -8(%rsp) # Red Zone 사용
ret
⚠️ 주의: 비동기 시그널/인터럽트가 red zone을 덮어쓸 수 있으므로, 커널이나 시그널-헤비 코드에서는 기대하지 말 것
10. Windows x64 차이점
System V와 Windows x64 ABI 비교:
┌──────────────────┬─────────────────────┬──────────────────┐
│ 항목 │ System V (Linux) │ Windows x64 │
├──────────────────┼─────────────────────┼──────────────────┤
│ 인자 전달 │ RDI, RSI, RDX, │ RCX, RDX, R8, R9 │
│ (1~4번째) │ RCX, R8, R9 │ (+스택) │
├──────────────────┼─────────────────────┼──────────────────┤
│ Callee-saved │ RBX, RBP, R12-R15 │ RBX, RBP, RDI, │
│ │ │ RSI, R12-R15 │
├──────────────────┼─────────────────────┼──────────────────┤
│ Red Zone │ 128바이트 있음 │ 없음 │
├──────────────────┼─────────────────────┼──────────────────┤
│ Shadow Space │ 없음 │ 32바이트 필요 │
└──────────────────┴─────────────────────┴──────────────────┘
주요 차이점:
- 인자 순서: Windows는 RCX가 첫 번째
- RSI/RDI: Windows에서는 callee-saved
- Red Zone: Windows에는 없음
- Shadow Space: Windows는 호출자가 32바이트 예약 필요
📌 최종 정리
핵심 패턴
# Prologue
pushq %rbp # 1. 이전 기준점 저장
movq %rsp, %rbp # 2. 새 기준점 설정
# Body
movl %edi, -4(%rbp) # 3. 인자 저장 (32bit)
movl -20(%rbp), %edi # 4. 지역변수 로드
addl $3, %edi # 5. 레지스터 연산
movl %edi, -20(%rbp) # 6. 결과 저장
# Epilogue
popq %rbp # 7. 기준점 복원
ret # 8. 복귀
원칙
1. 지역 변수는 항상 %rbp - N 형태 (음수 오프셋)
2. 모든 계산은 레지스터에서 수행
3. 메모리 접근은 로드/스토어 시에만
4. 스택은 높은 주소 → 낮은 주소로 성장
call/ret 주소 흐름
call function:
1. push %rip (다음 명령 주소)
2. jmp function
ret:
1. %rip = [%rsp] (리턴 주소 로드)
2. %rsp += 8
3. 복귀
참고 자료
- System V AMD64 ABI Specification
- Intel® 64 and IA-32 Architectures Software Developer's Manual
버전: 2.0 (스택 정렬, Red Zone, Windows 비교 추가)
💡 Tip: 실제 어셈블리 코드는 최적화 레벨(-O0, -O2, -O3)에 따라 크게 달라집니다. 학습할 때는 -O0 옵션으로 컴파일하는 것을 권장합니다!
'concept' 카테고리의 다른 글
| 운영체제 프로세스 개념 정리 (0) | 2025.10.17 |
|---|---|
| x86-64 레지스터의 자동 Zero-Extension 규칙 (0) | 2025.10.05 |
| [임시] 강의자료 정리: 함수 호출 과정 - Virtual Memory (1) | 2025.10.05 |
| x86-64 어셈블리 명령어 (0) | 2025.10.05 |
| x86-64 스택 프롤로그(Stack Prologue)와 스택 프레임(Stack Frame) (0) | 2025.10.02 |