프로시저는 소프트웨어에서 핵심적인 추상화 개념입니다. 프로시저는 특정 기능을 구현하는 코드를 지정된 인수 집합과 선택적 반환값과 함께 패키징하는 방법을 제공합니다. 이 함수는 프로그램의 다양한 지점에서 호출할 수 있습니다.
프로시저 P가 프로시저 Q를 호출하고, Q가 실행된 후 P로 돌아오는 작업에는 다음 메커니즘들이 포함됩니다:
C와 대부분의 다른 언어의 프로시저 호출 메커니즘의 핵심 특징은 후입선출(LIFO) 메모리 관리 원칙을 사용하는 스택 데이터 구조를 활용할 수 있다는 것입니다.
%rsp가 스택 최상위 요소를 가리킴pushq와 popq 명령어 사용
Figure 3.25: 일반적인 스택 프레임 구조
함수 P에서 함수 Q로 제어를 전달하는 것은 단순히 프로그램 카운터(PC)를 Q의 코드 시작 주소로 설정하는 것입니다. 그러나 Q가 나중에 반환할 때, 프로세서는 P의 실행을 재개해야 할 코드 위치에 대한 기록이 있어야 합니다.
| 명령어 | 설명 |
|---|---|
call Label | 프로시저 호출 (직접) |
call *Operand | 프로시저 호출 (간접) |
동작 과정:
1. 반환 주소 A를 스택에 푸시
2. PC를 Q의 시작 주소로 설정
3. 반환 주소 A = 호출 명령어 바로 다음 명령어의 주소
| 명령어 | 설명 |
|---|---|
ret | 호출에서 반환 |
동작 과정:
1. 스택에서 주소 A를 팝
2. PC를 A로 설정
# multstore 함수 시작
0000000000400540 <multstore>:
400540: 53 push %rbx
400541: 48 89 d3 mov %rdx,%rbx
...
40054d: c3 retq
# main에서 multstore 호출
400563: e8 d8 ff ff ff callq 400540 <multstore>
400568: 48 8b 54 24 08 mov 0x8(%rsp),%rdx

1. 호출 전 (Figure 3.26a)
%rip = 0x400563 (call 명령어 위치)%rsp = 0x7fffffffe840호출 후 (Figure 3.26b)
%rip = 0x400540 (multstore 시작)%rsp = 0x7fffffffe838 (스택에 반환 주소 푸시)0x400568 (반환 주소)반환 후 (Figure 3.26c)
%rip = 0x400568 (main의 다음 명령어)%rsp = 0x7fffffffe840 (원래 상태 복원)long leaf(long y) { return y+2; }
long top(long x) { return 2 * leaf(x-5); }
# leaf 함수
L1: 400540: lea 0x2(%rdi),%rax # z+2
L2: 400544: retq # Return
# top 함수
T1: 400545: sub $0x5,%rdi # x-5
T2: 400549: callq 400540 <leaf> # Call leaf(x-5)
T3: 40054e: add %rax,%rax # Double result
T4: 400551: retq # Return
# main에서 호출
M1: 40055b: callq 400545 <top> # Call top(100)
M2: 400560: mov %rax,%rdx # Resume
| 라벨 | PC | 명령어 | %rdi | %rax | %rsp | *%rsp | 설명 |
|---|---|---|---|---|---|---|---|
| M1 | 0x40055b | callq | 100 | — | 0x7fffffffe820 | — | Call top(100) |
| T1 | 0x400545 | sub | 100 | — | 0x7fffffffe818 | 0x400560 | Entry of top |
| T2 | 0x400549 | callq | 95 | — | 0x7fffffffe818 | 0x400560 | Call leaf(95) |
| L1 | 0x400540 | lea | 95 | — | 0x7fffffffe810 | 0x40054e | Entry of leaf |
| L2 | 0x400544 | retq | — | 97 | 0x7fffffffe810 | 0x40054e | Return 97 from leaf |
| T3 | 0x40054e | add | — | 97 | 0x7fffffffe818 | 0x400560 | Resume top |
| T4 | 0x400551 | retq | — | 194 | 0x7fffffffe818 | 0x400560 | Return 194 from top |
| M2 | 0x400560 | mov | — | 194 | 0x7fffffffe820 | — | Resume main |
Figure 3.27: 프로시저 호출 및 반환을 포함한 프로그램의 상세 실행
첫 번째 호출 (M1 → T1):
callq 실행으로 top(100) 호출두 번째 호출 (T2 → L1):
callq 실행으로 leaf(95) 호출 첫 번째 반환 (L2 → T3):
retq 실행으로 반환값 97과 함께 top으로 복귀두 번째 반환 (T4 → M2):
retq 실행으로 반환값 194와 함께 main으로 복귀반환 주소를 스택에 푸시하는 간단한 메커니즘으로 함수가 나중에 프로그램의 적절한 지점으로 반환할 수 있게 됩니다. C의 표준 call/return 메커니즘은 스택이 제공하는 후입선출 메모리 관리 원칙과 편리하게 일치합니다.
프로시저 호출 시 제어 전달 외에도 다음이 필요합니다:
x86-64에서는 대부분의 데이터 전달이 레지스터를 통해 이루어집니다.
| 피연산자 크기 (비트) | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 64 | %rdi | %rsi | %rdx | %rcx | %r8 | %r9 |
| 32 | %edi | %esi | %edx | %ecx | %r8d | %r9d |
| 16 | %di | %si | %dx | %cx | %r8w | %r9w |
| 8 | %dil | %sil | %dl | %cl | %r8b | %r9b |
Figure 3.28: 함수 인수 전달용 레지스터
%rax로 값을 반환프로시저 P가 n > 6개의 정수 인수로 프로시저 Q를 호출하는 경우:
void proc(long a1, long *a1p, int a2, int *a2p,
short a3, short *a3p, char a4, char *a4p)
void proc(long a1, long *a1p, int a2, int *a2p,
short a3, short *a3p, char a4, char *a4p)
// a1 in %rdi (인수 1)
// a1p in %rsi (인수 2)
// a2 in %edx (인수 3)
// a2p in %rcx (인수 4)
// a3 in %r8w (인수 5)
// a3p in %r9 (인수 6)
// a4 at %rsp+8 (인수 7)
// a4p at %rsp+16 (인수 8)
1 proc:
2 movq 16(%rsp), %rax # a4p에서 가져오기
3 addq %rdi, (%rsi) # *a1p += a1
4 addl %edx, (%rcx) # *a2p += a2
5 addw %r8w, (%r9) # *a3p += a3
6 movl 8(%rsp), %edx # a4 가져오기
7 addb %dl, (%rax) # *a4p += a4
8 ret

addq: a1 (long) - 8바이트addl: a2 (int) - 4바이트 addw: a3 (short) - 2바이트addb: a4 (char) - 1바이트주목할 점: movl 명령어(6번째 줄)는 메모리에서 4바이트를 읽지만, 다음 addb 명령어는 하위 바이트만 사용합니다.
지금까지 본 대부분의 프로시저 예제는 레지스터에 저장할 수 있는 것 이상의 지역 저장소가 필요하지 않았습니다. 그러나 지역 데이터를 메모리에 저장해야 하는 경우가 있습니다:
모든 지역 데이터를 담기에 레지스터가 충분하지 않은 경우
지역 변수에 주소 연산자가 적용되어 해당 주소를 생성해야 하는 경우
일부 지역 변수가 배열이나 구조체이므로 배열 또는 구조체 참조로 접근해야 하는 경우
일반적인 할당 방법: 스택 포인터를 감소시켜 스택 프레임에 공간 할당
long swap_add(long *xp, long *yp)
{
long x = *xp;
long y = *yp;
*xp = y;
*yp = x;
return x + y;
}
long caller()
{
long arg1 = 534;
long arg2 = 1057;
long sum = swap_add(&arg1, &arg2);
long diff = arg1 - arg2;
return sum * diff;
}
long caller()
1 caller:
2 subq $16, %rsp # 스택 프레임에 16바이트 할당
3 movq $534, (%rsp) # arg1에 534 저장
4 movq $1057, 8(%rsp) # arg2에 1057 저장
5 leaq 8(%rsp), %rsi # &arg2를 두 번째 인수로 계산
6 movq %rsp, %rdi # &arg1을 첫 번째 인수로 계산
7 call swap_add # swap_add(&arg1, &arg2) 호출
8 movq (%rsp), %rdx # arg1 가져오기
9 subq 8(%rsp), %rdx # diff = arg1 - arg2 계산
10 imulq %rdx, %rax # sum * diff 계산
11 addq $16, %rsp # 스택 프레임 해제
12 ret # 반환
S = 스택 포인터 값이라고 하면:
따라서 지역 변수들의 스택 프레임 내 위치:
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
long call_proc()
# proc에 대한 인수 설정
1 call_proc:
2 subq $32, %rsp # 32바이트 스택 프레임 할당
3 movq $1, 24(%rsp) # x1에 1 저장
4 movl $2, 20(%rsp) # x2에 2 저장
5 movw $3, 18(%rsp) # x3에 3 저장
6 movb $4, 17(%rsp) # x4에 4 저장
7 leaq 17(%rsp), %rax # &x4 생성
8 movq %rax, 8(%rsp) # &x4를 인수 8로 저장
9 movl $4, (%rsp) # 4를 인수 7로 저장
10 leaq 18(%rsp), %r9 # &x3를 인수 6으로 전달
11 movl $3, %r8d # 3을 인수 5로 전달
12 leaq 20(%rsp), %rcx # &x2를 인수 4로 전달
13 movl $2, %edx # 2를 인수 3으로 전달
14 leaq 24(%rsp), %rsi # &x1을 인수 2로 전달
15 movl $1, %edi # 1을 인수 1로 전달
# proc 호출
16 call proc
# 메모리 변경사항 검색
17 movslq 20(%rsp), %rdx # x2를 가져와서 long으로 변환
18 addq 24(%rsp), %rdx # x1+x2 계산
19 movswl 18(%rsp), %eax # x3를 가져와서 int로 변환
20 movsbl 17(%rsp), %ecx # x4를 가져와서 int로 변환
21 subl %ecx, %eax # x3-x4 계산
22 cltq # long으로 변환
23 imulq %rdx, %rax # (x1+x2) * (x3-x4) 계산
24 addq $32, %rsp # 스택 프레임 해제
25 ret # 반환

프로그램 레지스터 세트는 모든 프로시저가 공유하는 단일 자원입니다. 한 번에 하나의 프로시저만 활성화되지만, 한 프로시저(호출자)가 다른 프로시저(피호출자)를 호출할 때 피호출자가 호출자가 나중에 사용할 예정인 레지스터 값을 덮어쓰지 않도록 해야 합니다.
해당 레지스터: %rbx, %rbp, %r12–%r15
규칙:
보존 방법:
1. 변경하지 않기: 전혀 변경하지 않음
2. 저장 후 복원: 원래 값을 스택에 푸시 → 변경 → 반환 전 이전 값을 팝
해당 레지스터: 스택 포인터 %rsp를 제외한 모든 다른 레지스터
규칙:
long P(long x, long y)
{
long u = Q(y);
long v = Q(x);
return u + v;
}
long P(long x, long y)
// x in %rdi, y in %rsi
1 P:
2 pushq %rbp # %rbp 저장
3 pushq %rbx # %rbx 저장
4 subq $8, %rsp # 스택 프레임 정렬
5 movq %rdi, %rbp # x 저장
6 movq %rsi, %rdi # y를 첫 번째 인수로 이동
7 call Q # Q(y) 호출
8 movq %rax, %rbx # 결과 저장
9 movq %rbp, %rdi # x를 첫 번째 인수로 이동
10 call Q # Q(x) 호출
11 addq %rbx, %rax # 저장된 Q(y)를 Q(x)에 더하기
12 addq $8, %rsp # 스택의 마지막 부분 해제
13 popq %rbx # %rbx 복원
14 popq %rbp # %rbp 복원
15 ret
레지스터 저장 (2-3번째 줄):
값 보존:
레지스터 복원 (13-14번째 줄):
레지스터와 스택 사용에 대해 설명한 규칙들은 x86-64 프로시저가 자기 자신을 재귀적으로 호출할 수 있게 합니다.
long rfact(long n)
{
long result;
if (n <= 1)
result = 1;
else
result = n * rfact(n-1);
return result;
}
long rfact(long n)
// n in %rdi
1 rfact:
2 pushq %rbx # %rbx 저장
3 movq %rdi, %rbx # n을 callee-saved 레지스터에 저장
4 movl $1, %eax # 반환값 = 1로 설정
5 cmpq $1, %rdi # n:1 비교
6 jle .L35 # <=이면 done으로 이동
7 leaq -1(%rdi), %rdi # n-1 계산
8 call rfact # rfact(n-1) 호출
9 imulq %rbx, %rax # 결과에 n을 곱하기
10 .L35: done:
11 popq %rbx # %rbx 복원
12 ret # 반환
%rbx에 매개변수 n 저장 (기존 값은 스택에 저장)스택 원칙과 레지스터 저장 규칙으로 인해 재귀 호출 rfact(n-1)이 반환될 때(9번째 줄):
%rax에 저장됨%rbx에 저장됨재귀 함수 호출은 다른 함수 호출과 동일하게 진행됩니다.
스택 원칙이 다음을 제공:
rfact(4) 호출 시 스택 상태:
┌─────────────────┐ ← 높은 주소
│ main의 데이터 │
├─────────────────┤
│ rfact(4) frame │ ← n=4, %rbx 저장됨
├─────────────────┤
│ rfact(3) frame │ ← n=3, %rbx 저장됨
├─────────────────┤
│ rfact(2) frame │ ← n=2, %rbx 저장됨
├─────────────────┤
│ rfact(1) frame │ ← n=1, %rbx 저장됨
└─────────────────┘ ← %rsp (낮은 주소)
각 재귀 호출이 자체 스택 프레임을 가져 독립적인 n 값과 %rbx 저장값을 유지합니다.