함수 호출 규약은 함수의 호출 및 반환에 대한 약속이다. 한 함수에서 다른 함수를 호출할 떄, 프로그램의 실행 흐름은 다른 함수로 이동한다. 그리고 호출한 함수가 반환하면, 다시 원래 함수로 돌아와서 기존의 실행 흐름을 이어나간다. 그러믈호 함수를 호출할 때는 반환된 이후를 위해 호출자의 상태 및 반환 주소를 저장해야한다. 또한 호출자는 피호출자가 요구하는 인자를 전달해야하며 피호출자의 실행이 종료될 때는 반환값을 전달 받아야 한다.
함수 호출 규약을 적용하는 것은 일반적으로 컴파일러 몫이다. 프로그래머가 고수준 언어로 코드를 작성하면 컴파일러가 호출 규약에 맞게 코드를 컴파일한다. 호출 규약은 여러 가지가 있는데 프로그래서가 코드에 명시하지 않는다면, 컴파일러는 지원하는 호출 규약 중에 CPU의 아키텍처에 적합한 것을 선택합니다.
만약 컴파일러의 도움 없이 어셈블리 코드를 작성하려 하거나, 또는 어셈블리로 작성한 코드를 읽고자 한다면 함수 호출 규약을 알아야 할 필요가 있다.
컴파일러가 지원하는 호출 규약 중, CPU 아키텍처에 적합한 것을 선택한다. 예를 들어 x86(32bit) 아키텍처는 레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적으므로, 스택으로 인자를 전달하는 규약을 사용한다. 반대로 x86-64 아키텍처에서는 레지스터가 많으므로 적은수의 인자는 레지스터만 사용해서 인자를 전달하고 인자가 너무 많을 때만 스택을 사용한다. CPU의 아키텍처가 같아도 컴파일러가 다르면 호출 규약이 다를 수 있다. C언어를 컴파일 할때, 윈도우에서는 MSVC를, 리눅스에서는 GCC를 많이 사용한다.
우리는 리눅스 gcc에서 x86바이너리를 컴파일 할 때 사용하는 cdecl 그리고, x86-64 바이너리를 컴파일할 때 사용하는 SYSTEM V호출 규약에 대해 공부할것이다.
x86라키텍처는 레지스터 수가 적으므로, 스택을 통해 인자를 전달한다. 또한, 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있다. 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push한다.
리눅스 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어졌습니다. SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약등의 내용을 담고있습니다.
SYSV에서 정의한 함수 호출 규약의 특직
1. 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달한다. 더많은 인자를 사용해야 할 때는 스택을 추가로 이용한다.
2. Caller에서 인자 전달에 사용된 스택을 정리한다.
3. 함수의 반환값은 RAX로 전달한다.