이번 절에서는 어셈블리의 여러 함수 간 상호작용과 함수 관리와 관련된 새로운 명령어를 소개한다.
| 매개변수 | 전달 방식 |
|---|---|
| 매개변수 1 | %rdi |
| 매개변수 2 | %rsi |
| 매개변수 3 | %rdx |
| 매개변수 4 | %rcx |
| 매개변수 5 | %r8 |
| 매개변수 6 | %r9 |
| 매개변수 7 이상 | 콜 스택 (Call Stack) |
첫 6개 함수 매개변수는 각 레지스터에 로드되며, 그 후 매개변수는 크기에 따라 32비트 데이터는 4바이트 오프셋, 64비트 데이터는 8바이트 오프셋은 콜 스택에 로드된다.
함수 매개변수가 콜 스택에 전달될 때, 스택은 메모리 정렬을 위해 8바이트 크기의 정해진 '슬롯(slot)' 단위로 공간을 할당합니다. 이 때문에 전달되는 데이터의 실제 크기와 상관없이 모든 매개변수는 이 8바이트 슬롯 하나를 차지하게 됩니다. 즉, 8바이트 크기의 long 타입은 슬롯을 완전히 채우고, 4바이트 int, 2바이트 short, 1바이트 char와 같이 작은 데이터들은 슬롯의 일부만 사용하며 나머지 빈 공간은 의미 없는 값(패딩, Padding)으로 채워집니다.
다음 코드 예시를 한 번 추적해보자.
#include <stdio.h>
int assign(void) {
int y = 40;
return y;
}
int adder(void) {
int a;
return a + 2;
}
int main(void) {
int x;
assign();
x = adder();
printf("x is: %d\n", x);
return 0;
}
이 코드를 다음 명령어로 컴파일하고, 어셈블리어로 변환해보자.
# 컴파일
gcc -o prog prog.c
# 어셈블리로 변환
objdump -d
변환된 어셈블리어
0000000000400526 <assign>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: c7 45 fc 28 00 00 00 movl $0x28,-0x4(%rbp)
400531: 8b 45 fc mov -0x4(%rbp),%eax
400534: 5d pop %rbp
400535: c3 retq
0000000000400536 <adder>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 45 fc mov -0x4(%rbp),%eax
40053d: 83 c0 02 add $0x2,%eax
400540: 5d pop %rbp
400541: c3 retq
0000000000400542 <main>:
400542: 55 push %rbp
400543: 48 89 e5 mov %rsp,%rbp
400546: 48 83 ec 10 sub $0x10,%rsp
40054a: e8 e3 ff ff ff callq 400526 <assign>
40054f: e8 d2 ff ff ff callq 400536 <adder>
400554: 89 45 fc mov %eax,-0x4(%rbp)
400557: 8b 45 fc mov -0x4(%rbp),%eax
40055a: 89 c6 mov %eax,%esi
40055c: bf 04 06 40 00 mov $0x400604,%edi
400561: b8 00 00 00 00 mov $0x0,%eax
400566: e8 95 fe ff ff callq 400400 <printf@plt>
40056b: b8 00 00 00 00 mov $0x0,%eax
400570: c9 leaveq
400571: c3 retq
각 함수는 프로그램에 선언된 이름에 해당하는 심볼릭 라벨로 시작한다. 함수 사벨의 주소는 해당 함수의 첫 번째 명령어이다.
아래 그림은 main 실행 직전의 스택을 보여준다.

%rbp에는 이전 함수의 스택 프레임 주소가 담겨있으며, %rsp는 스택 프레임의 시작주소인 0xd48이 담겨있다. %rip에는 명령어의 시작 주소인 0x542가 담겨있다.

첫 번째 명령은 %rbp의 현재 값을 스택에 넣는다. 스택은 낮은 주소 방향으로 커지므로 스택 포인터 %rsp는 0xd40으로 업데이트 된다. %rip는 다음 명령어 주소로 업데이트 된다.
push 의 역할
1. 스택 포인터(%rsp)를 8만큼 감소시켜 스택에 새로운 공간을 확보합니다. (이것이 '스택 영역 확장'에 해당합니다.)
2. 지정한 레지스터의 값(예: %rbp의 값)을 새로 확보된 공간에 복사합니다.

다음 mov 명령어는 %rbp의 값이 %rsp와 같아지도록 업데이트한다. 이제 %rbp는 main 함수의 스택 프레임의 시작 위치를 가리킨다. %rip는 순서상 다음 명령으로 이동한다.
0xd48에는 리턴주소가 담긴다.

sub 명령은 스택 포인터 주소에서 x10을 뺀다. 이것은 스택이 16바이트 커지게 만든다 -> 스택 한 칸 8바이트, 2칸 위로
%rsp는 0xd30을 가리키게 된다. %rip 값은 다음 명령 주소를 가리킨다.

callq 명령은 %rip의 값, 반환주소를 스택에 넣는다. 반환 주소는 함수의 실행이 끝나고, main으로 돌아왔을때 재개되는 프로그램 주소이다.
다음으로, callq 명령은 assign의 함수의 주소를 %rip에 옮긴다. 따라서 다음 실행은 assign이 실행된다.

assign 함수의 첫 두 명령은 main과 같다. %rbp에 저장된 main 함수의 스택 프레임을 스택에 저장한다.

다음 명령은 스택 프레임을 스택의 맨 위 값으로 업데이트 한다.

mov 명령어는 %0x28(40)을 스택 주소 -0x4(%rbp)에 넣는다. 이 주소는 프레임 포인터보다 4바이트 더 위에 있다. 여기서 %rsp 값은 바꾸지 않는다.

값 $0x28을 레지스터 %eax에 넣는다. 이 레지스터는 반환값을 가진다.

이 시점은 assign 함수의 실행이 거의 완료된다. 다음으로 pop %rbp 명령이 실행된다. 이 명령은 %rbp의 값을 이전 값, 0xd40으로 원복한다. 또한 %rsp 값을 0xd28로 수정한다.(스택 포인터를 8만큼 증가)

assign의 마지막 명령은 retq이다. 이전에 저장했던, 리턴주소를 스택에서 꺼내서 rip를 업데이트하여 다음 명령은 0x55f가 실행되도록한다.

main으로 돌아와서 addr 함수를 호출하면 새로운 반환주소 0x554를 스택의 이전 주소로 덮어쓴다. %rip는 addr의 첫 번째 명령(0x536)을 가리킨다.

addr 함수의 첫 번째 명령은 호출자의 프레임 포인터(main의 %rbp)를 스택에 저장한다.

%rsp의 값(0xd20)으로 %rbp를 업데이트 한다.
이 게시물의 내용과 이미지는 Suzanne J. Matthews, Tia Newhall, Kevin C. Webb의 Dive into Systems를 기반으로 하며, Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License에 따라 사용되었습니다.