함수 호출, 스택, 레지스터의 삼각관계
C에서의 function(a, b) 한 줄은 어셈블리어 세계에서는 꽤 복잡한 과정으로 번역됩니다. 이번 3.7절은 바로 그 "함수 호출의 내부 구조", 즉 스택을 어떻게 쓰고, 레지스터를 어떻게 관리하며, 인자와 반환값은 어떻게 주고받는지를 설명합니다.
이번 포스트부터는 우리가 배운 어셈블리 구조를 바탕으로, 함수 호출, 조건문, 반복문 같은 C 코드의 익숙한 구문들이 어셈블리에서 어떻게 구현되는지를 직접 매핑해보는 방식으로 진행할 예정입니다.
C 함수 호출, 내부적으로 어떤 일이 벌어질까?
int sum(int a, int b) {
return a + b;
}
int main() {
int x = sum(3, 4);
}
main에서 sum이 호출되어 반환값이 변수 x에 할당되는 간단한 코드가, 어셈블리어 수준에서는 어떤 흐름으로 작동하는지 살펴보겠습니다.
C -> 어셈블리 매핑: sum 함수의 호출 흐름
1. 인자 전달
sum(3, 4);
- 인자 3, 4는 각각 %edi, %esi 레지스터에 담깁니다.
x86-64 (System V ABI)에서는 인자 6개까지를 %rdi, %rsi, %rdx, %rcx, %r8, %r9 순서로 레지스터에 전달하고,
7번째 인자부터는 스택을 통해 전달합니다.
2. call 명령어를 통해 함수로 점프
call sum은 "함수를 마치고 돌아올 위치"인 다음 명령어 주소(복귀 주소)를 스택에 저장(push)한 뒤, 함수 sum으로 이동합니다. 이 주소는 나중에 ret 명령어로 복귀할 때 사용됩니다.
다시말해 call 명령어는 ret과 함께 사용되며, 스택 기반의 복귀 흐름을 만듭니다.
3. 함수 내부 실행 & 스택 프레임 설정
sum:
pushq %rbp
movq %rsp, %rbp ; 새로운 스택 프레임 생성
movl %edi, -4(%rbp) ; a 저장
movl %esi, -8(%rbp) ; b 저장
movl -4(%rbp), %eax
addl -8(%rbp), %eax
popq %rbp
ret
스택 프레임은 push %rbp → mov %rsp, %rbp 순서로 생성되고, 지역 변수는 보통 %rbp 기준 음수 offset에 저장됩니다.
4. 반환값 전달
- sum의 결과는 int형이므로 %eax에 저장되며, long형 반환이면 %rax를 사용합니다.
- 그리고 main 함수는 %eax 값을 받아 변수 x에 할당합니다.
x86-64에서는 함수 반환값은 항상 %rax에 저장됩니다.
함수 하나당 생기는 스택 프레임 구조
[ 이전 %rbp가 저장된 메모리 주소 ] ← 현재 함수의 %rbp 값이 가리키는 위치
[ 복귀 주소(리턴 주소) ]
[ 인자 7번 이후 ]
[ 지역 변수들 ] ← %rsp (스택은 아래로 확장됨)
- %rbp는 함수 기준점을 고정
- %rsp는 현재 스택 꼭대기
→ 함수마다 해당 함수의 실행을 위한 독립적인 작업 공간이 만들어집니다.
스택 프레임이 없는 함수도 있을까?
함수 호출 시에 항상 스택 프레임이 만들어지는 건 아닌데요. 함수 안에서 지역 변수를 사용하지 않고, 다른 함수를 호출하지도 않는 경우, 컴파일러는 스택 프레임 생성을 생략할 수 있습니다.
이를 리프 함수(leaf function)라고 부르며, 이런 함수는 단순히 레지스터만으로 작업을 수행하고 바로 반환(ret)합니다. 이러한 최적화는 함수 호출 비용을 줄이고 성능을 높이는 데 유리합니다.
서로 약속해서 나눠 쓰는 레지스터
레지스터 | 사용 규약 |
Caller-saved | %rdi, %rsi, %rdx, %rcx, %r8, %r9, %rax, %r10, %r11 |
→ 호출 전에 백업 필요 |
|
Callee-saved | %rbx, %rbp, %r12~%r15 |
→ 함수 내부에서 쓰려면 복원해야 함 |
이 규약을 지키면 함수들이 서로의 레지스터 값을 안전하게 유지할 수 있습니다.
함수 호출 정리
C 개념 | 어셈블리 매핑 |
함수 호출 | call, 복귀 주소 push, ret |
인자 전달 | %rdi, %rsi 등 레지스터 |
지역 변수 | 스택 프레임의 오프셋 위치 |
반환값 | %rax |
이 구조를 이해하면 디버깅 시 스택을 추적, 함수 호출 흐름 파악, 보안/버그 분석 시 리턴 주소, 오버플로우 개념을 명확히 할 수 있습니다.
마치면서
C에서의 함수 호출은 어셈블리에서 정해진 스택 구조와 레지스터 규약에 따라 정교하게 구현됩니다. 그 흐름을 매핑해서 이해하면, C 코드가 어떻게 실행되는지를 스택과 레지스터 수준에서 정확히 따라갈 수 있게 됩니다.
이제 함수 호출의 구조를 정확히 이해했다면, 다음 포스트에서는 배열처럼 연속된 메모리 구조를 다루는 배열 접근을 다뤄보겠습니다. 감사합니다.
'크래프톤 정글 > 컴퓨터구조(CSAPP)' 카테고리의 다른 글
[CSAPP 3장 완전 정복] 3.8.5 동적 배열(VLA), 주소 계산은 어떻게 달라질까? (0) | 2025.04.08 |
---|---|
[CSAPP 3장 완전 정복] 3.8 배열, 포인터, 주소 계산의 모든 것 (0) | 2025.04.07 |
[CSAPP 3장 완전 정복] 3.4 메모리와 레지스터, 정보를 어떻게 읽고 쓸까? (0) | 2025.04.06 |
[CSAPP 3장 완전 정복] x86-64 어셈블리 필수 배경지식 핵심 정리 (0) | 2025.04.06 |
[CSAPP 3장 완전 정복] 3.5 기계 수준의 연산, 어떻게 작동할까? (0) | 2025.04.05 |