연세대학교 운영체제론 OSTEP 강의 자료 정리
전체 구조
┌─────────────┬──────────────────────────────┐
│ 단계 │ 역할 │
├─────────────┼──────────────────────────────┤
│ 1. Prologue │ 스택 프레임 생성 │
│ 2. Body │ 함수 코드 실행 │
│ 3. Epilogue │ 스택 프레임 복원 및 반환 │
└─────────────┴──────────────────────────────┘
0. call 명령 직후 (callee 진입 시점)
스택은 높은 주소에서 낮은 주소로 성장
높은 주소 (예: 0x500)
┌──────────────────┐
│ 이전 함수의 프레임│
├──────────────────┤ ← %rbp = 0x500 (이전 기준점)
│ ... │
├──────────────────┤ ← call 직전 %rsp
│ 리턴 주소 │ ← call이 자동으로 push
├──────────────────┤ ← %rsp = 0x4f8 (현재 top)
│ │
└──────────────────┘
낮은 주소
중요: call 명령이 리턴 주소를 자동으로 push하여 %rsp가 8바이트 감소한 상태
레지스터 상태:
- %rip: 호출된 함수의 시작 주소 (예: 0x0)
- %rsp: 0x4f8 (리턴 주소 아래)
- %rbp: 0x500 (아직 이전 함수의 기준점)
1. PROLOGUE (스택 프레임 만들기)
주소 0x0: pushq %rbp
이전 함수의 %rbp를 스택에 저장
동작:
%rsp -= 8
[%rsp] = %rbp
이유: 함수 종료 시 복원하기 위함
구조도:
┌──────────────────┐
│ 이전 함수 프레임 │
├──────────────────┤
│ 리턴 주소 │
├──────────────────┤
│ 이전 %rbp (0x500)│ ← pushq 실행
├──────────────────┤ ← %rsp = 0x4f0
│ │
└──────────────────┘
%rbp: 0x500 (아직 그대로)
주소 0x1: movq %rsp, %rbp
현재 스택 top을 새 기준점으로 설정
동작:
%rbp = %rsp
결과: 기준점 고정
구조도:
┌──────────────────┐
│ 리턴 주소 │
├──────────────────┤
│ 이전 %rbp (0x500)│
├──────────────────┤ ← %rsp = %rbp = 0x4f0 (새 기준점)
│ │
│ (지역 변수 영역) │
└──────────────────┘
요약:
- push %rbp: 이전 기준점 백업
- mov %rsp, %rbp: 새 기준점 확립
2. BODY (함수 본문)
(1) 인자 저장 (System V AMD64 규약)
┌─────────┬───────────┬──────────────────┐
│ 인자 │ 레지스터 │ 저장 위치 │
├─────────┼───────────┼──────────────────┤
│ 1번째 │ %rdi/%edi │ %rbp - 4 │
│ 2번째 │ %rsi │ %rbp - 16 │
│ 지역변수│ - │ %rbp - 20 (예시) │
└─────────┴───────────┴──────────────────┘
참고: 최적화 옵션에 따라 인자/지역변수의 스택 저장은 생략될 수 있음
주소 0x4: movl $0x0, %eax
%eax = 0
역할: 리턴값 준비 (return 0)
주소 0x9: movl %edi, 0xfffffc(%rbp)
첫 번째 인자(argc) 스택에 저장
0xfffffc = -4
위치: %rbp - 4 = 0x4ec
내용: argc
구조도:
├──────────────────┤ ← %rbp (0x4f0)
│ argc │ ← %rbp - 4
├──────────────────┤
주소 0xc: movq %rsi, 0xfffff0(%rbp)
두 번째 인자(argv) 스택에 저장
0xfffff0 = -16
위치: %rbp - 16 = 0x4e0
내용: argv
구조도:
├──────────────────┤ ← %rbp
│ argc │ ← %rbp - 4
│ (padding) │ ← 8바이트 정렬
│ argv │ ← %rbp - 16
├──────────────────┤
(2) 지역 변수 접근 및 연산: x = x + 3
주소 0x10: movl 0xffffec(%rbp), %edi
변수 x를 레지스터로 로드
0xffffec = -20
위치: %rbp - 20 = 0x4dc
동작: 메모리(0x4dc) → %edi
강의안 주석:
- -20은 컴파일러가 선택한 오프셋
- 다른 컴파일러는 -8, -12일 수도 있음
- "음수 오프셋" 원리는 동일
구조도:
├──────────────────┤ ← %rbp (0x4f0)
│ argc │ ← %rbp - 4
│ argv │ ← %rbp - 16
│ x = 0 │ ← %rbp - 20
├──────────────────┤
실행 후:
%edi = 0
주소 0x13: addl $0x3, %edi
레지스터에서 계산
%edi = %edi + 3 = 0 + 3 = 3
메모리 접근 없음 (빠름)
주소 0x19: movl %edi, 0xffffec(%rbp)
계산 결과를 메모리에 저장
동작: %edi(3) → 메모리(0x4dc)
구조도:
│ x = 3 │ ← %rbp - 20 (업데이트)
연산 요약:
- 모든 계산은 레지스터에서 수행
- 메모리 접근은 로드/스토어 시에만
3. EPILOGUE (스택 복원)
주소 0x1c: popq %rbp
이전 함수의 기준점 복원
동작:
%rbp = [%rsp]
%rsp += 8
결과: 이전 기준점으로 복귀
구조도:
┌──────────────────┐
│ 이전 함수 프레임 │ ← %rbp = 0x500 (복원)
├──────────────────┤
│ 리턴 주소 │
├──────────────────┤ ← %rsp = 0x4f8
│ (현재 함수 자료) │
└──────────────────┘
주소 0x1d: ret
함수 종료 및 복귀
동작:
%rip = [%rsp] (리턴 주소 로드)
%rsp += 8 (스택 복원)
함수 종료
최종 상태:
%rbp = 0x500 (이전 함수 기준점)
%rsp = 0x500 (call 직전 값으로 복귀)
%rip = (이전 함수 코드 주소)
중요: ret가 리턴 주소를 pop하므로, %rsp는 call 직전 상태로 완전히 복귀
레지스터 요약
┌──────────┬────────────────────────┬───────────┐
│ 레지스터 │ 의미 │ 예시 │
├──────────┼────────────────────────┼───────────┤
│ %rip │ Instruction Pointer │ 0x10 │
│ %rsp │ Stack Pointer (변동) │ push/pop │
│ %rbp │ Base Pointer (고정) │ 기준점 │
├──────────┼────────────────────────┼───────────┤
│ %rdi/ │ 함수 인자 (1~6번째) │ argc, │
│ %rsi/ │ System V 규약 │ argv │
│ %rdx/ │ │ │
│ %rcx/ │ │ │
│ %r8/ │ │ │
│ %r9 │ │ │
├──────────┼────────────────────────┼───────────┤
│ %rax │ 함수 리턴값 (64bit) │ return │
│ %eax │ 함수 리턴값 (32bit) │ return 0 │
└──────────┴────────────────────────┴───────────┘
핵심 원리 요약
┌───────────┬─────────────────┬──────────────────┐
│ 단계 │ 명령 │ 의미 │
├───────────┼─────────────────┼──────────────────┤
│ Prologue │ push %rbp │ 이전 기준점 저장 │
│ │ mov %rsp, %rbp │ 새 기준점 설정 │
├───────────┼─────────────────┼──────────────────┤
│ Body │ mov/add/... │ 지역 변수 및 연산│
├───────────┼─────────────────┼──────────────────┤
│ Epilogue │ pop %rbp │ 기준점 복원 │
│ │ ret │ 복귀 │
└───────────┴─────────────────┴──────────────────┘
지역 변수 접근 원칙:
- 항상 %rbp - N 형태 (음수 오프셋)
- N은 컴파일러가 결정 (정렬/최적화)
- 강의안 예시: -4, -16, -20
- 그림 예시: -8 (단순화)
심화: 알아두면 좋은 포인트
1. 스택 정렬 (Stack Alignment)
System V x86-64 ABI는 call 시점에 %rsp가 16바이트 정렬되어야 함
이유:
- SSE 명령어 최적화 (16바이트 정렬 필요)
- ABI 규약 준수
영향:
- 프롤로그에서 sub $N, %rsp로 정렬 조정
- N은 16의 배수로 맞춤
2. 프레임 포인터 생략
-fomit-frame-pointer 최적화 옵션 사용 시
차이점:
- %rbp를 범용 레지스터로 사용
- %rsp 기준으로만 지역 변수 접근
- 프롤로그/에필로그 단순화
장점: 레지스터 하나 더 사용 가능
단점: 디버깅 어려움 (스택 추적)
3. Red Zone (레드존)
System V x86-64 전용 (Windows x64에는 없음)
정의:
- %rsp 아래 128바이트 영역
- Leaf 함수가 임시 사용 가능
- push/pop 없이 사용 가능
조건:
- 인터럽트/시그널 발생 안 하는 환경
- call을 하지 않는 함수 (leaf function)
예시:
addl %edi, -8(%rsp) # Red Zone 사용
ret
[x86-64 레지스터 체계 정리]
레지스터 크기별 이름 체계
x86-64는 64비트 아키텍처이지만, 하위 호환성을 위해 32/16/8비트 접근을 지원
┌───────────┬──────────────────────┬─────────────┐
│ 비트 크기 │ 레지스터 이름 │ 설명 │
├───────────┼──────────────────────┼─────────────┤
│ 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 │
│ (64bit에서 추가) │ (32bit) │ │ (8bit) │
└─────────────────────────────┴─────────────────┴────────┴────────┘
└─────────────────────────────────────┘
%rax (64bit)
왜 %rdi 대신 %edi를 쓸까?
이유 1: 데이터 타입 크기
C 언어의 기본 타입:
int → 32비트
long → 64비트 (x86-64에서)
char* → 64비트 (포인터)
int argc는 32비트이므로 32비트 레지스터 사용
이유 2: 자동 제로 확장 (Zero Extension)
32비트 레지스터에 값을 쓰면, CPU가 자동으로 상위 32비트를 0으로 초기화
명령어: movl $3, %edi
동작:
%edi = 0x00000003 (32비트 기록)
↓
%rdi = 0x0000000000000003 (상위 32비트 자동 0)
구조도:
실행 전:
%rdi = 0x12345678ABCDEF00
movl $3, %edi 실행
실행 후:
%rdi = 0x0000000000000003
↑↑↑↑↑↑↑↑ 자동으로 0
장점
┌────────────────────────────────────────────┐
│ 1. 코드 간결 │
│ - 별도로 상위 비트 클리어 불필요 │
│ │
│ 2. C 언어와 자연스러운 대응 │
│ - int는 32비트 │
│ - %edi, %eax 사용이 자연스러움 │
└────────────────────────────────────────────┘
main 함수 예시
int main(int argc, char *argv[])
어셈블리 대응:
┌──────┬──────────┬─────────────────────────┐
│ 인자 │ 레지스터 │ 어셈블리 │
├──────┼──────────┼─────────────────────────┤
│ argc │ %rdi │ movl %edi, -0x4(%rbp) │
│ │ │ (32비트 정수 저장) │
├──────┼──────────┼─────────────────────────┤
│ argv │ %rsi │ movq %rsi, -0x10(%rbp) │
│ │ │ (64비트 포인터 저장) │
└──────┴──────────┴─────────────────────────┘
핵심:
- 정수(int) → 32비트 → %edi
- 포인터(char*) → 64비트 → %rsi
Caller-saved vs Callee-saved
문제 상황
int foo(int x) {
int y = bar(x);
return y + 1;
}
상황:
- foo()가 %rdi에 x 저장
- foo()가 bar() 호출
- bar()도 %rdi를 인자로 사용
- 문제: bar() 실행 중 %rdi가 덮어써짐
질문: 누가 복구할까?
Caller-saved: "내가 저장하고 복원할게"
호출자가 책임지는 레지스터
┌──────────────────────────────────────────┐
│ Caller-saved 레지스터 │
├──────────────────────────────────────────┤
│ %rax, %rcx, %rdx, %rsi, %rdi, %r8-r11 │
└──────────────────────────────────────────┘
foo()의 책임:
# foo() 내부
pushq %rdi # x 보존 (백업)
call bar # bar() 호출
popq %rdi # x 복원
# 이유: bar()가 %rdi를 마음대로 쓸 수 있음
구조도:
foo() 실행 중:
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: "내가 쓰더라도 되돌려줄게"
피호출자가 책임지는 레지스터
┌──────────────────────────────────────────┐
│ Callee-saved 레지스터 │
├──────────────────────────────────────────┤
│ %rbx, %rbp, %r12-r15 │
└──────────────────────────────────────────┘
bar()의 책임:
bar:
pushq %rbx # 저장 (프롤로그)
# ... %rbx 사용
popq %rbx # 복원 (에필로그)
ret
# 이유: foo()가 %rbx에 중요한 값을 저장했을 수 있음
구조도:
bar() 진입 시:
%rbx에 foo의 중요한 값이 있을 수 있음
Prologue:
pushq %rbx # 백업
%rbx = ... # 자유롭게 사용
Epilogue:
popq %rbx # 복원
ret # foo로 복귀
# foo의 %rbx는 그대로!
비유로 이해하기
┌─────────────────┬────────────────────┬──────────────┐
│ 구분 │ 역할 │ 비유 │
├─────────────────┼────────────────────┼──────────────┤
│ Caller-saved │ "내 물건은 내가 │ 내 가방 챙김 │
│ │ 챙긴다" │ │
├─────────────────┼────────────────────┼──────────────┤
│ Callee-saved │ "남의 물건은 내가 │ 빌린 건 │
│ │ 책임진다" │ 돌려줌 │
└─────────────────┴────────────────────┴──────────────┘
전체 정리표
┌────────────────┬──────────────────────────────┬────────────┐
│ 구분 │ 레지스터 │ 누가 보존? │
├────────────────┼──────────────────────────────┼────────────┤
│ Caller-saved │ %rax, %rcx, %rdx, │ 호출자 │
│ │ %rsi, %rdi, %r8-r11 │ │
├────────────────┼──────────────────────────────┼────────────┤
│ Callee-saved │ %rbx, %rbp, %r12-r15 │ 피호출자 │
└────────────────┴──────────────────────────────┴────────────┘
용도별:
┌────────────────┬──────────────────────────────┐
│ Caller-saved │ 임시 계산, 인자 전달 │
├────────────────┼──────────────────────────────┤
│ Callee-saved │ 지역 변수, 루프 상수 등 │
│ │ 장기 유지 값 │
└────────────────┴──────────────────────────────┘
실제 예시
int foo(int x) {
int y = bar(x);
return y + 1;
}
int bar(int x) {
return x + 2;
}
어셈블리:
foo:
pushq %rbp # callee-saved 보존
movq %rsp, %rbp
movl %edi, -4(%rbp) # x 저장
movl -4(%rbp), %edi # bar 인자 준비
call bar # bar(x)
# bar()가 %edi 마음대로 씀 (caller-saved)
addl $1, %eax # return값(%eax)에 1 더함
popq %rbp # callee-saved 복원
ret
bar:
pushq %rbp # callee-saved 보존
movq %rsp, %rbp
addl $2, %edi # x+2
movl %edi, %eax # return x+2
popq %rbp # callee-saved 복원
ret
최종 요약
┌────────────────┬─────────────────────┬─────────────────┐
│ 범주 │ 레지스터 │ 설명 │
├────────────────┼─────────────────────┼─────────────────┤
│ 특수 목적 │ %rip, %rsp, %rbp │ 흐름, 스택 제어 │
├────────────────┼─────────────────────┼─────────────────┤
│ 인자 전달 │ %rdi~%r9 │ System V 규약 │
├────────────────┼─────────────────────┼─────────────────┤
│ 리턴값 │ %rax/%eax │ 함수 반환 │
├────────────────┼─────────────────────┼─────────────────┤
│ Caller-saved │ %rax, %rcx, %rdx, │ 호출자 보호 │
│ │ %rsi, %rdi, %r8-r11 │ │
├────────────────┼─────────────────────┼─────────────────┤
│ Callee-saved │ %rbx, %rbp, %r12-15 │ 피호출자 보호 │
└────────────────┴─────────────────────┴─────────────────┘
핵심:
- %edi vs %rdi: 32bit int → 자동 zero-extend
- Caller-saved: 호출 전 백업
- Callee-saved: 사용 후 복원
'concept' 카테고리의 다른 글
| 운영체제 프로세스 개념 정리 (0) | 2025.10.17 |
|---|---|
| x86-64 레지스터의 자동 Zero-Extension 규칙 (0) | 2025.10.05 |
| x86-64 어셈블리 명령어 (0) | 2025.10.05 |
| [Virtual Memory] x86-64 함수 호출 & Virtual Memory 기반 스택 프레임 정리 (SysV ABI) (0) | 2025.10.05 |
| x86-64 스택 프롤로그(Stack Prologue)와 스택 프레임(Stack Frame) (0) | 2025.10.02 |