Random writings

on Free and Open Source Software

x86 호출 규약 (Calling Convention)

이 문서에는 x86_64 아키텍처에서 리눅스 환경 하에 사용되는 함수 호출 규약에 대해서 알아보도록 하겠습니다. 함수 호출 규약은 어셈블리 레벨에서 어떤 식으로 함수의 인자를 전달하고 리턴값을 반환하는지 등의 내용을 표준으로 정의한 것입니다.

참고로 여기서 설명하는 내용은 대부분의 경우에 적용되지만 (static 함수와 같이) 내부적으로만 사용되는 (특별한?) 경우 이를 따르지 않을 가능성도 있습니다.

우선 기본적으로 floating-point number가 아닌 경우 대부분의 인자는 범용 레지스터를 통해 전달합니다. 구체적으로는 순서대로 다음과 같은 레지스터를 이용합니다.

rdi, rsi, rdx, rcx, r8, r9

6개보다 많은 인자를 전달하는 경우는 스택에 차례로 push합니다. 이 때 인자의 역순으로 (right-to-left) 저장하기 때문에 가장 마지막 인자가 먼저 push 됩니다.

또한 대부분의 floating-point의 경우에는 8개의 SSE 레지스터를 이용합니다.

xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7

마찬가지로 그보다 많은 인자는 메모리(stack)를 이용합니다. 다만 80-bit 크기의 ‘long double’ 타입에 대해서는 메모리를 통해서 (16 바이트 크기) 전달하게 됩니다.

또한 구조체나 클래스와 같은 복합 타입인 경우 그 구성이나 크기에 따라 필드 별로 따로 레지스터로 전달되거나 내부적으로 메모리에 객체를 위한 공간을 할당한 뒤 포인터(reference) 형태로 전달할 수도 있습니다. 특히 non-trivial copy constructor 혹은 destructor가 있는 경우에는 크기에 상관없이 포인터 형태로 전달된다고 합니다.

또 한 가지 살펴볼 것은 rax 레지스터인데, 일반적으로는 인자 전달에 사용되지 않지만 어떤 함수가 가변 인자를 받는 경우에는 (printf 처럼), 인자 중에 floating-point number가 (정확히는 SSE/vector 레지스터가) 사용된 갯수를 전달하게 됩니다.

그럼 해당 함수에서는 rax 레지스터의 값을 확인한 후에 필요한 SSE 레지스터들을 스택에 저장하여 접근할 수 있도록 합니다.

시스템콜의 경우 거의 비슷하게 범용 레지스터를 이용해 최대 6개까지의 인자를 전달하지만, 4번째 인자의 경우 rcx 대신 r10 레지스터를 사용한다는 차이점이 있습니다.

리턴값의 경우에는 대부분 rax 레지스터를 통해 반환하지만 float 및 double 타입의 경우 xmm0 레지스터, long double 타입의 경우 st0 레지스터 (in x87 FPU)를 통해서 반환합니다.

만약 리턴값이 8바이트 이상이고 16바이트 이하인 경우에는 추가로 rdx 레지스터를 이용합니다.

가장 복잡한 경우 중의 하나는 리턴값이 16바이트 이상인 경우로 이 때는 인자에서처럼 메모리를 통해 전달하게 되는데, 함수 호출 시에 필요한 메모리를 미리 스택에 할당한 후에 rdi 레지스터를 통해 (마치 첫번째 인자인 것처럼) 포인터를 전달하고 여기에 필요한 값을 쓴 후에 rax 레지스터를 통해 반환합니다. 따라서 이후의 인자들은 하나씩 밀려서 전달됩니다.

마지막으로, 스택을 통해 인자를 전달하는 경우 stack pointer의 값은 항상 (최소) 16-byte 단위로 정렬되야 합니다. 특히 SSE(2) 레지스터를 사용하는 경우에는 스택이 정렬되지 않으면 프로그램이 오류를 일으킬 수 있으며 AVX 명령어를 통해 256-bit 크기의 벡터 레지스터를 쓰는 경우에는 32-byte 단위로 정렬해야 합니다. (AVX512는 64-byte 겠지만 확인하지 못했습니다).

참고