rax는 64비트 레지스터야.eax는 rax의 하위 32비트를 나타내.eax에 값을 쓰면, 자동으로 rax의 상위 32비트가 0으로 채워져.eax에 값을 쓸 때는 zero-extension이 일어나.ax, al 등에 값을 써도 그런 일은 일어나지 않아. (그땐 하위 비트만 바뀌고, 상위 비트는 그대로 유지돼.)mov eax, 0xFFFFFFFF ; eax = 0xFFFFFFFF, rax = 0x00000000FFFFFFFF
eax에 32비트 최대값을 넣으면,rax의 상위 32비트는 0으로 자동 초기화되어 rax = 0x00000000FFFFFFFF가 돼.rsp)란?rsp, 32비트에선 esp)push나 call을 하면 rsp가 자동으로 감소하고, pop이나 ret을 하면 증가해.✅ 즉, 스택에서 실제 데이터를 넣고 빼는 위치를 나타내는 포인터야.
rbp)란?rbp, 32비트에선 ebp)push rbp ; 이전 함수의 rbp 저장
mov rbp, rsp ; 현재 함수의 기준점을 rbp로 설정
✅ 즉, 스택 프레임의 기준점을 잡아주는 포인터야.
rbp - 4,rbp + 8같은 식으로 쉽게 변수에 접근할 수 있도록 도와줘.
rsp → [ 지역변수 2 ]
[ 지역변수 1 ]
[ return address ]
rbp → [ 이전 rbp ]
call 명령어로 함수 호출push로 return address 저장mov로 인자 복사 (rdi, rsi, rdx, ... 레지스터 사용)rbp와 rsp를 조작해 스택 프레임 설정mov rdi, rax ; 첫 번째 인자
call greet_function ; 함수 호출
함수 내부에서는:
push rbp
mov rbp, rsp ; 스택 프레임 설정
...
pop rbp
ret ; 호출한 곳으로 복귀
Python 코드: greet("Alice")
↓
CPython 해석기
↓
C 함수: PyEval_EvalFrameEx
↓
C 함수: PyObject_CallFunction
↓
어셈블리:
- 인자 설정 (rdi 등)
- call 함수
- push return address
- rbp, rsp 조정
- ret 복귀
함수가 호출되면 다음 순서로 스택이 움직여:
return address 저장됨 (자동적으로 call 명령어가 push)rbp 저장 (push rbp)rbp를 rsp에 맞춤 (mov rbp, rsp)sub rsp, N)rsp와 rbp의 역할 비교| 항목 | rsp (Stack Pointer) | rbp (Base Pointer) |
|---|---|---|
| 의미 | 현재 스택의 최상단 | 스택 프레임의 기준점 |
| 변화 | 함수 실행 중 계속 변함 | 함수 실행 중 고정됨 |
| 용도 | 지역 변수 공간 확보, push/pop | 함수 인자, 지역 변수 접근 |
| 주소 기준 | 상대적 주소가 어렵다 (rsp+32는 변함) | 항상 같은 위치 (rbp-4) |
int foo(int a) {
int b = a + 3;
return b;
}
이 코드의 어셈블리 스택 프레임을 보자:
push rbp ; 이전 rbp 저장
mov rbp, rsp ; 현재 스택 기준점 설정
sub rsp, 16 ; 지역변수 공간 확보
mov DWORD PTR [rbp-4], edi ; 매개변수 a 저장
mov eax, DWORD PTR [rbp-4] ; a 불러오기
add eax, 3
mov DWORD PTR [rbp-8], eax ; b 저장
여기서 보면 모든 변수 접근이 rbp를 기준으로 일어남. 왜냐?
✅ rsp는 계속 바뀌기 때문이야. push, call, alloca, signal 등으로 바뀔 수 있어.
✅ 반면에 rbp는 함수 시작할 때 한 번만 mov rbp, rsp로 설정되고 절대 안 바뀜.
rsp만 쓰면 안 될까?요즘 컴파일러는 사실 rbp 없이도 코드를 만들 수 있어. 이를 frame pointer omission (FPO) 이라고 해.
gdb 같은 디버거는 rbp를 따라 스택 프레임을 추적하니까, rbp가 없으면 call stack 복원이 어려워.그래서 디버깅이나 오류 추적을 고려하면 rbp를 쓰는 게 낫고,
최적화된 릴리즈 빌드는 rbp 없이 컴파일되기도 해.
rsp = 스택에서 push/pop을 위한 조작 포인터 (계속 변함)rbp = 스택 프레임의 기준점 (함수 실행 내내 고정됨)64비트 시스템에서, 데이터를 8바이트 단위로 정렬(aligned)하면 CPU가 더 빠르게 읽고 쓸 수 있기 때문이야.
좋은 정렬 (aligned):
주소 0x100, 0x108, 0x110 ← 8바이트 경계
나쁜 정렬 (misaligned):
주소 0x101, 0x105 ← 경계 어긋남 → 느림
rax, rbp, rsp 등은 8바이트 크기push rax ; rsp -= 8 → 8바이트 감소
pop rbx ; rsp += 8 → 8바이트 증가
x86-64 시스템에서는 System V AMD64 ABI 같은 표준이 있어.
rsp는 16바이트 정렬이 되어 있어야 해.call 명령어는 return address를 8바이트만큼 push하니까,rsp가 8로 나눠 떨어지게 됨✅ 그래서 스택에 변수를 더 push할 때도 8바이트 단위로 맞춰서 정렬하는 거야!
push rbx ; rsp -= 8
push rcx ; rsp -= 8
call foo ; rsp -= 8 (return address 저장)
→ 이제 rsp는 8바이트 단위로 잘 정렬되어 있음
Before push:
rsp → 0x7fff_ffff_fff8
After push rbx:
rsp → 0x7fff_ffff_fff0 (8 byte down)
After push rcx:
rsp → 0x7fff_ffff_ffe8 (again 8 byte down)
esp는 과거 32비트 x86 코드와의 호환성을 위해 남아 있고,
때론 일부 명령어에서 더 짧은 인코딩으로 쓸 수 있기 때문이야.
x86 아키텍처는 40년 넘게 진화했어:
| 세대 | 주요 레지스터 |
|---|---|
| 16비트 (8086) | sp |
| 32비트 (i386) | esp |
| 64비트 (x86-64) | rsp |
esp, ebp, eax 같은 32비트 레지스터를 그대로 살려야 돼.esp라는 이름의 레지스터가 존재하는 것처럼 보이게 해줌 (실제로는 rsp의 하위 32비트지만!)이건 아까 네가 잘 짚었던 부분이야:
mov esp, 0x11111111
이 명령은 사실상 아래와 같아:
rsp = 0x0000000011111111
즉, 32비트 연산은 상위 32비트를 0으로 만들어버려. 이건 의도된 동작이야.
어셈블리어는 기계어로 번역될 때, 명령어 길이가 중요해.
mov rsp, imm64 → 길이가 길어 (9~10바이트)mov esp, imm32 → 짧음 (5바이트 정도)예를 들어 32비트 상수만 필요할 때는 esp를 쓰는 게 코드 크기를 줄이고, 성능에 유리할 수 있어.
컴파일러는 이런 점을 활용해서 일부러 esp를 선택하기도 해.
아래와 같은 간단한 C 코드:
int main() {
int x = 10;
return x;
}
gcc로 컴파일하면 64비트 모드에서도 esp, ebp, eax 같은 32비트 레지스터가 여전히 나와.
왜냐하면:
int는 32비트니까 eax를 쓰고,esp, ebp를 쓸 수 있어.✅ 주소지정 방식은 위험할 수 있지만,
✅ 성능, 유연성, 메모리 직접 접근이 필요한 구조 때문에 반드시 필요해.
예를 들어 이런 명령어:
mov [rax], 0x10
이건 rax가 가리키는 주소에 0x10을 저장하는 명령어야.
[rax] 위치엔 이미 어떤 값이 있을 수 있어.mov [addr], value 같은 직접 메모리 접근이 없다면, 컴퓨터는 아무것도 못해.이건 운영체제가 책임져!
| 보호 방법 | 설명 |
|---|---|
| ✅ 가상 메모리 | 각 프로세스에 독립적인 메모리 공간 부여 |
| ✅ 접근 권한 보호 | 읽기/쓰기/실행 권한 제어 (segfault 유발) |
| ✅ 메모리 보호 정책 | 커널은 사용자 영역 접근 못함 등 |
| ✅ Stack canary, ASLR | 보안 보호 기법 추가 |
즉, 주소지정 자체는 위험하지만 필수고,
위험을 제어하는 건 하드웨어와 OS의 책임이야.
함수 호출 시 인자를 어떻게 전달하고,
리턴 값을 어디에 저장하며,
스택을 누가 정리하는지를 약속해 놓은 규칙이야.
📌 요약: 서로 다른 함수, 컴파일러, 언어끼리 함수 호출을 안전하게 연결하기 위한 약속이야!
| 항목 | 설명 |
|---|---|
| 인자는 어디에 저장? | 레지스터? 스택? 어떤 순서로? |
| 리턴 값은 어디에 저장? | 보통 rax |
| 스택 정리는 누가? | 호출자(caller)? 피호출자(callee)? |
| 어떤 레지스터는 보존해야 해? | rbx, rbp 등 (callee-saved) |
| 항목 | 내용 |
|---|---|
| 인자 전달 순서 | rdi, rsi, rdx, rcx, r8, r9 (그 이후는 스택) |
| 리턴 값 | rax |
| 스택 정리 | 호출자 (caller) |
| 보존해야 하는 레지스터 | rbx, rbp, r12~r15 등 (callee-saved) |
| 항목 | 내용 |
|---|---|
| 인자 전달 순서 | rcx, rdx, r8, r9 |
| 리턴 값 | rax |
| 스택 정리 | 호출자 (caller) |
| 보존 레지스터 | 거의 같음, 약간 차이 있음 |
📎 두 규약은 비슷하지만 레지스터 순서가 다름!
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(1, 2);
}
어셈블리에서는:
mov edi, 1 ; 첫 번째 인자 → rdi
mov esi, 2 ; 두 번째 인자 → rsi
call add ; 함수 호출
mov [rbp-4], eax ; 리턴값 저장