suminworld

concept

[임시] 강의자료 정리: 함수 호출 과정 - Virtual Memory

숨usm 2025. 10. 5. 04:26

연세대학교 운영체제론 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 함수 예시

 
 
c
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

문제 상황

 
 
c
int foo(int x) {
    int y = bar(x);
    return y + 1;
}

상황:

  1. foo()가 %rdi에 x 저장
  2. foo()가 bar() 호출
  3. bar()도 %rdi를 인자로 사용
  4. 문제: bar() 실행 중 %rdi가 덮어써짐

질문: 누가 복구할까?


Caller-saved: "내가 저장하고 복원할게"

호출자가 책임지는 레지스터

 
 
┌──────────────────────────────────────────┐
│ Caller-saved 레지스터                    │
├──────────────────────────────────────────┤
│ %rax, %rcx, %rdx, %rsi, %rdi, %r8-r11   │
└──────────────────────────────────────────┘

foo()의 책임:

 
 
asm
# 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()의 책임:

 
 
asm
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   │ 지역 변수, 루프 상수 등      │
│                │ 장기 유지 값                 │
└────────────────┴──────────────────────────────┘

실제 예시

 
 
c
int foo(int x) {
    int y = bar(x);
    return y + 1;
}

int bar(int x) {
    return x + 2;
}

어셈블리:

 
 
asm
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: 사용 후 복원