1. 문제 제기 (Introduction & Problem Statement)
관찰 현상 또는 질문:
C언어 등 고급 언어에서 함수 호출은 프로그램의 핵심적인 추상화 도구입니다.func(a, b)처럼 간단한 한 줄의 코드는 어떻게 기계어 수준에서 구현될까요? 이 과정에는 인자 전달, 반환 값 수신, 실행 제어권 이전, 그리고 함수 내 지역 변수를 위한 메모리 관리 등 복잡한 작업들이 포함됩니다. "CPU는 이 모든 과정을 어떤 메커니즘을 통해 정확하고 효율적으로 처리하는가?"라는 질문에서 탐구를 시작합니다.탐구 목표:
본 아티클은 x86-64 아키텍처에서 함수 호출이 이루어지는 구체적인 원리를 분석하는 것을 목표로 합니다. 이를 위해 프로그램 실행 시 메모리에 생성되는 런타임 스택(Runtime Stack) 과 그 안에 구성되는 스택 프레임(Stack Frame) 의 구조를 살펴볼 것입니다 . 또한, 인자 전달과 값 반환을 위해 스택과 레지스터를 어떻게 함께 사용하는지, 그리고 여러 함수가 레지스터를 공유하면서도 데이터가 엉키지 않도록 하는 호출 규약(Calling Convention) 에 대해 심도 있게 분석하고자 합니다.
2. 기술 분석 및 핵심 원리 (Technical Deep Dive)
2-1. (소주제 1: 런타임 스택과 스택 프레임)
프로그램이 실행되는 동안(런타임) 함수들의 호출 관계를 관리하기 위해 메모리의 특정 영역을 런타임 스택으로 사용합니다 . 이 스택은 후입선출(LIFO) 자료구조로 동작하며, rsp 레지스터가 항상 스택의 최상단(가장 낮은 주소)을 가리킵니다.
- 스택 프레임(Stack Frame):
함수가 호출될 때마다 해당 함수만을 위한 독립적인 메모리 공간인 '스택 프레임'이 런타임 스택에 생성(할당)됩니다 함수가 실행을 마치고 반환되면 이 스택 프레임은 스택에서 제거(해제)됩니다 각 스택 프레임은 다음과 같은 정보를 포함합니다- 반환 주소: 함수 실행이 끝난 후 돌아가야 할 호출자(caller) 코드의 위치입니다.
- 저장된 레지스터: 호출된 함수(callee)가 값을 덮어쓰기 전에, 호출자(caller)의 상태를 보존하기 위해 백업해 둔 레지스터 값들입니다.
- 지역 변수: 레지스터에 저장하기에는 크기가 큰 배열이나 구조체와 같은 지역 변수들이 저장됩니다.
- 함수 인자: 레지스터만으로 전달하기에 개수가 너무 많은(7개 이상) 함수 인자들이 저장됩니다
call 명령어가 실행되면 반환 주소를 스택에 푸시하고 해당 함수로 점프하며, ret 명령어가 실행되면 스택에서 반환 주소를 팝하여 원래 위치로 복귀합니다 .
2-2. (소주제 2: 제어권 이전과 데이터 전달)
x86-64 아키텍처는 함수 호출 시 스택뿐만 아니라 레지스터를 적극적으로 활용하여 성능을 최적화합니다. 주 메모리에 위치한 스택보다 CPU 내부에 있는 레지스터가 훨씬 빠르기 때문입니다.
데이터 전달 규칙:
- 인자 전달: 최대 6개의 정수 또는 포인터 인자는 약속된 레지스터들을 통해 순서대로 전달됩니다:
%rdi,%rsi,%rdx,%rcx,%r8,%r9. 7개 이상의 인자가 필요할 경우, 7번째 인자부터는 스택을 통해 전달됩니다 - 반환 값 전달: 함수가 계산한 결과(반환 값)는 관례적으로
%rax레지스터에 저장되어 호출자에게 전달됩니다.
- 인자 전달: 최대 6개의 정수 또는 포인터 인자는 약속된 레지스터들을 통해 순서대로 전달됩니다:
제어권 이전:
call명령어는 단순히 함수 코드로 점프하는 것을 넘어, 다음에 실행할 명령어의 주소(반환 주소)를 스택에 자동으로 저장합니다. 그 후 프로그램 카운터(PC) 레지스터의 값을 호출될 함수의 시작 주소로 변경하여 제어권을 넘깁니다. 반대로ret명령어는 스택에 저장된 반환 주소를 꺼내 PC에 다시 넣음으로써 제어권을 원래 위치로 돌려놓습니다.
2-3. (소주제 3: 레지스터 사용 규칙 (Calling Convention))
함수들이 서로를 호출하는 과정에서 레지스터 값을 마구잡이로 사용하면 데이터가 손상될 수 있습니다. 이를 방지하기 위해 "누가 레지스터 값을 보존할 책임이 있는가"에 대한 규칙, 즉 호출 규약이 존재합니다.
| 구분 | 호출자 저장 ([translate:Caller-Saved]) 레지스터 | 피호출자 저장 ([translate:Callee-Saved]) 레지스터 |
|---|---|---|
| 해당 레지스터 | %rax, %rcx, %rdx, %rsi, %rdi, %r8 ~ %r11 |
%rbx, %rbp, %r12 ~ %r15 |
| 규칙 | 호출자(Caller)가 백업 책임을 집니다. 함수 A가 함수 B를 호출할 때, A는 이 레지스터들의 값이 B에 의해 변경될 수 있다고 가정합니다. 따라서 B 호출 전에 중요한 값은 A가 직접 스택에 저장해야 합니다. | 피호출자(Callee)가 백업 책임을 집니다. 함수 B는 이 레지스터들을 사용하기 전에, 원래 들어있던 A의 값을 스택에 백업해 두어야 합니다. 그리고 B는 반환 직전에 원래 값을 복원해야 합니다. |
| 장점 | 피호출자(B)가 레지스터를 자유롭게 사용할 수 있어 코드가 단순해집니다. | 호출자(A)는 함수 호출 후에도 이 레지스터의 값이 보존될 것이라고 믿을 수 있어 안정적입니다. |
| 사용 시나리오 | 임시 값 저장이나 함수 인자 및 반환 값 전달 등, 값이 자주 바뀌는 용도로 사용됩니다. | 함수 호출을 거쳐서도 유지되어야 하는 중요한 상태나 값을 저장하는 데 사용됩니다. |
결론적으로, 함수 하나를 실행하는 데에는 스택과 레지스터가 모두 사용됩니다. 레지스터는 빠른 인자 전달과 값 반환, 간단한 지역 변수 처리에 사용되고, 스택은 반환 주소, 레지스터 백업, 배열/구조체 같은 무거운 지역 변수 및 초과 인자 저장에 사용됩니다.
3. 결론 및 고찰 (Conclusion & Takeaways)
- 핵심 요약:
- 함수 호출은 런타임 스택에 생성되는 '스택 프레임'을 통해 관리되며, 여기에는 반환 주소, 지역 변수, 인자 등이 저장됩니다.
- x86-64에서는 성능 향상을 위해 최대 6개의 인자와 반환 값을 레지스터로 전달하며, 7개 이상의 인자와 큰 변수들은 스택을 사용합니다.
- '호출자/피호출자 저장' 규칙은 여러 함수가 레지스터를 공유하며 발생할 수 있는 데이터 충돌을 방지하는 핵심적인 약속입니다.
- 기술적 통찰 및 나의 생각:
이번 학습을 통해 함수 호출이 단순한 코드 점프가 아니라, 스택과 레지스터를 유기적으로 사용하는 잘 짜인 프로토콜임을 깨달았습니다. 특히 '호출 규약'은 마치 사회적 약속처럼, 여러 함수(독립적인 주체)가 CPU의 한정된 자원(레지스터)을 질서 있게 공유하기 위한 규칙이라는 점이 인상 깊었습니다. 레지스터에 데이터를 저장할 수 없어 메모리 주소를 대신 사용하는 배열이나, 성능을 위해 메모리 정렬 규칙을 따르는 구조체의 동작 방식은 하드웨어 제약이 소프트웨어 설계에 어떻게 직접적인 영향을 미치는지 보여주는 좋은 예시입니다. - 향후 과제 / 추가 질문:
- 구조체에서 데이터 정렬 규칙으로 인해 발생하는 '패딩(padding)'은 메모리 낭비를 유발하는데, 이를 최소화하기 위한 프로그래밍 기법은 무엇이 있을까?
- 재귀 함수 호출 시 스택 프레임은 어떻게 계속 쌓이며, 이로 인해 발생하는 '스택 오버플로우'는 정확히 어떤 상황에서 발생하는 걸까?
4. 참고 자료 (References)
- Randal E. Bryant, David R. O'Hallaron 저, 『Computer Systems: A Programmer's Perspective』
- x86-64 System V ABI (Application Binary Interface) 문서
'Study > CSAPP' 카테고리의 다른 글
| [CSAPP] 컴파일러는 왜 내 코드를 최적화하지 못할까?: 데이터 의존성과 분기 예측의 비밀 (0) | 2025.11.12 |
|---|---|
| [CSAPP] Y86-64 ISA의 순차적 구현: SEQ 프로세서 동작 원리 분석 (0) | 2025.11.11 |
| [CSAPP] C언어 if문은 어떻게 기계어로 번역될까?: 조건 코드와 점프 명령어 분석 (0) | 2025.11.05 |
| [CSAPP] C언어에서 기계어까지: 레지스터, 데이터 이동, 스택의 모든 것 (0) | 2025.10.29 |
| [CSAPP] 정수 오버플로우와 부동소수점의 비밀: 컴퓨터는 어떻게 숫자를 다루는가? (0) | 2025.10.29 |