1 minute read

어떻게 함수가 호출되고 인자가 전달될 것인지에 대해 살펴본다. - 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