원문: http://blogs.msdn.com/b/oldnewthing/archive/2004/01/14/58579.aspx
저자: 레이몬드 첸(Raymond Chen, The Old New Thing 블로그로 유명)
기록:
2007년 3월 - 최초 번역
2011년 2월 - 갱신
호출 규약 시리즈 마지막으로 AMD64 아키텍처(x86-64)를 살펴보자.
AMD64 아키텍처는 x86에서 쓰던 레지스터를 가져와서 64비트로 확장했다. (rax, rbx, etc.) 또한, 일반적인 용도로 사용할 레지스터 8개(R8 ~ R15)를 추가했다.
함수로 넘겨줄 파라미터 중 처음 4개는 rcx, rdx, r8, r9 레지스터를 통해 전달된다. 나머지 파라미터는 스택에 올라간다. 또한, 함수 내부에서 레지스터를 더럽히는 경우를 대비해서, 레지스터에 올라간 파라미터에 해당하는 공간도 스택에 예약된다. 이는 가변 인자를 받는 함수의 경우 중요하다.
64비트보다 작은 크기의 파라미터는 0으로 확장(zero-extended)하지 않는다. 따라서, 상위 비트는 쓰레기 값을 가지게 되고, 필요할 경우에 0으로 초기화하는 것을 잊지 마시라. 64비트보다 큰 파라미터는 주소로 전달된다.
리턴 값은 rax를 사용해서 전달한다. 리턴 값이 64비트보다 크다면, 리턴 값을 돌려줄 곳의 주소를 숨겨진(secret) 첫 번째 인자로 전달하고 그 곳에 리턴 값이 채워진다.
rax, rcx, rdx, r8, r9, r10, r11 레지스터를 제외한 모든 레지스터는 호출(call)을 하더라도 보존된다.
호출 받은 쪽(callee)은 스택을 정리하지 않는다. 스택을 정리하는 것은 호출 하는 쪽(caller)의 일이다.
스택은 16바이트 정렬을 맞추어야 한다. "call" 명령어가 8바이트로 된 리턴 주소[1]를 스택에 집어넣기 때문에, 모든 넌-리프(non-leaf)[2] 함수는 16바이트 정렬을 맞추기 위해서 16n+8의 형태로 조정이 일어날 것이다.
샘플을 보자:
void SomeFunction(int a, int b, int c, int d, int e); void CallThatFunction() { SomeFunction(1, 2, 3, 4, 5); SomeFunction(6, 7, 8, 9, 10); }
CallThatFunction 함수의 시작 지점(entry)에서, 스택은 아래와 같이 생겼다.
xxxxxxx0
xxxxxxx8
...rest of stack...
return address
<- RSP
리턴 주소의 존재로 인해, 스택은 정렬된 상태가 아니다. 따라서, CallThatFunction 함수는 다음과 같이 스택 프레임을 초기화 한다.
sub rsp, 0x28
스택 프레임의 크기는 16n+8이므로, 정렬된 스택은 아래와 같다.
자, 첫 번째 호출을 해보자.
mov dword ptr [rsp+0x20], 5 ; 파라미터 5 mov r9d, 4 ; 파라미터 4 mov r8d, 3 ; 파라미터 3 mov edx, 2 ; 파라미터 2 mov ecx, 1 ; 파라미터 1 call SomeFunction ; 어서 가자!
SomeFunction 함수가 리턴할 때, 스택은 정리되지 않고, 이전 상태 그대로 남는다. 다음 함수 호출을 해보자. 이미 할당된 공간이 있으니, 그대로 새 값을 채워 넣기만 하면 된다.
mov dword ptr [rsp+0x20], 10 ; 파라미터 5 mov r9d, 9 ; 파라미터 4 mov r8d, 8 ; 파라미터 3 mov edx, 7 ; 파라미터 2 mov ecx, 6 ; 파라미터 1 call SomeFunction ; 어서 가자!
CallThatFunction 함수는 이제 끝났으니 스택을 정리하고 리턴하자.
add rsp, 0x28 ret
amd64 코드에서는, 호출 하는 쪽(caller)이 파라미터가 저장될 공간을 확보해서 재사용하는 방식으로 패러다임이 변화해서, "push" 명령어가 거의 사용되지 않았다.
[1] (역주) 리턴 주소는 현재의 함수가 ret 명령으로 종료되었을 때, 돌아갈 곳의 주소.
[2] (역주) 넌-리프 함수는 콜 그래프(트리)의 루트나 끝(즉, 리프)이 아닌 함수 호출.