시스템 프로그래밍 노트 5 - 함수와 스택 메모리
어떻게 함수가 호출되고 인자가 전달될 것인지에 대해 살펴본다. - x64 Calling Convention
1. 스택
기본적으로 서브루틴은 스택을 통해 관리된다. 스택은
- 메모리에 있다.
- 높은 주소에서 낮은 주소로 자란다.
- %rsp는 가장 낮은 스택 주소를 가리킨다.
- %rbp는 선택적으로 가장 높은 스택 프레임 주소로 사용된다.
대충 형태는 다음과 같다:
%rbp -> 0x7fffffffdd08
0x7fffffffdd00
0x7fffffffdcf8
...
%rsp -> 0x7fffffffdc08
%rsp가 내려갈수록 서브루틴 하나가 점유하는 메모리 공간이 늘어난다. 그래서 스택은 아래로 자란다라는 표현이 쓰이기도 한다.
%rbp가 선택적이라는 뜻은 x64 Calling Convention에서는 %rbp를 굳이 사용 안해도 서브루틴을 무사히 만들 수 있기 때문이다. 만약에 사용되었다면 가장 높은 스택 프레임 주소이다. 어떻게 사용될 수 있는지는 1.2 참조.
1.1. 관련된 인스트럭션
크게 네 가지가 있다.
- push
- pushq Src
%rsp
를 8 감소시키고%rsp
에 Src에 있는 값을 적는다.
- pop
- popq Dest
%rsp
에 적힌 값을 Dest에 적고%rsp
를 8 증가시킨다. 이 때, Dest는 레지스터이다.
- call
- call label
- 리턴 주소를 stack에 push하고 label로 jump한다.
- ret
- 스택에서 리턴 주소를 pop하고 그 주소로 이동한다.
push와 pop은 정반대의 인스트럭션이다. 그래서 어떤 레지스터를 메모리에 저장했다가 나중에 다시 불러오고 싶을 때 다음과 같이 사용하기도 한다.
1.2. 인자 전달
x64에서 인자는 레지스터를 통해 직접 전달된다.
- 처음 6개의 인자는 순서대로 %rdi, %rsi, %rdx, %rcx, %r8, %r9이다.
- 7번째 이후의 인자는 스택에 저장된다.
- 리턴 주소 위에 차곡차곡 쌓인다.
- %rbp에서 상대적인 값을 더해서 접근한다. 즉, 함수가 시작될 때 다음과 같은 코드를 사용하여 이전 %rbp값을 저장한 후 %rsp와 %rbp 값을 맞춘 다음 %rbp 위에 예전 %rbp 위에 return address 위에 있는 인자들에 접근한다.
- return 값은 %rax이다.
보통 %rbp를 사용하는 경우 함수는 다음과 같은 형태이다. 예전 %rbp를 저장한 후 %rbp를 그 함수의 스택 메모리를 할당하기 전의 %rsp 값으로 설정한다. 이를 통해 이 함수의 가장 높은 스택 프레임 주소가 %rbp로 무사히 설정된다.
push %rbp
mov %rsp, %rbp
...
pop %rbp
1.3. Saving Conventions
같은 레지스터를 다른 프레임의 함수에서 사용하면 충돌한다. 이를 방지하기 위해서 레지스터 별로 saving cocnvention 이 존재한다.
- Caller Saved
- 부르는 쪽이 저장한다.
- rax랑 argument registers rdi, rsi, rdx, rcx, r8, r9, 그리고 r10, r11까지이다.
- Callee Saved
- 부름당한 쪽이 값을 저장하고 return하기 전에 값을 불러온다.
- rbx랑 r12, r13, r14, 그리고 스택 포인터로 사용되는 rbp랑 rsp도 여기에 해당한다.
이런 Callee Saved 레지스터를 저장하고 리턴 전에 불러오는 패턴을 push, pop으로 해결할 수 있다.
push %rbx
...
pop %rbx
Leave a comment