함수 호출 규약

함수 호출 규약은 함수의 호출 및 반환에 대한 약속입니다. 한 함수에서 다른 함수를 호출할 때, 프로그램의 실행 흐름은 다른 함수로 이동합니다. 그리고 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름을 이어나갑니다. 그러므로 함수를 호출할 때는 반환된 이후를 위해 **호출자(Caller)**의 상태(Stack frame) 및 반환 주소(Return Address)를 저장해야 합니다. 또한, 호출자는 **피호출자(Callee)**가 요구하는 인자를 전달해줘야 하며, 피호출자의 실행이 종료될 때는 반환 값을 전달받아야 합니다.

함수 호출 규약을 적용하는 것은 일반적으로 컴파일러의 몫입니다. 프로그래머가 고수준 언어로 코드를 작성하면, 컴파일러가 호출 규약에 맞게 코드를 컴파일합니다. 호출 규약은 여러 가지가 있는데 프로그래머가 코드에 명시하지 않는다면, 컴파일러는 지원하는 호출 규약 중에서 CPU의 아키텍처에 적합한 것을 선택합니다. 따라서 대개의 프로그래머는 함수 호출 규약을 알지 않아도 코드를 작성하는데 문제가 되지 않습니다.

그러나 만약, 컴파일러의 도움 없이 어셈블리 코드를 작성하려 하거나, 또는 어셈블리로 작성된 코드를 읽고자 한다면 함수 호출 규약을 알아야 할 필요가 있습니다. 시스템 해킹에서는 이 둘 모두 필수적인 기술이므로, 이번 강의에서는 함수의 호출 규약을 자세히 알아보도록 하겠습니다.

이 강의를 마치고 나면 cdecl, SYSV 호출 규약이 무엇인지, 그리고 다음 강의에서 배우는 스택 버퍼 오버플로우가 왜 위험한지 이해할 수 있을 것입니다.

함수 호출 규약의 종류

컴파일러는 지원하는 호출 규약 중, CPU 아키텍처에 적합한 것을 선택합니다. 예를 들어, x86(32bit) 아키텍처는 레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적으므로, 스택으로 인자를 전달하는 규약을 사용합니다. 반대로 x86-64 아키텍처에서는 레지스터가 많으므로 적은 수의 인자는 레지스터만 사용해서 인자를 전달하고, 인자가 너무 많을 때만 스택을 사용합니다.

CPU의 아키텍처가 같아도, 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있습니다. C언어를 컴파일할 때, 윈도우에서는 MSVC를, 리눅스에서는 gcc를 많이 사용합니다. 이 둘은 같은 아키텍처에 대해서도 다른 호출 규약을 적용합니다.. x86-64 아키텍처에서 MSVC는 MS x64 호출 규약을 적용하지만, gcc는 SYSTEM V 호출 규약을 적용합니다. 이 외에 같은 호출 규약을 컴파일러마다 다르게 구현하기도 합니다.

👉 아래에서 대표적인 몇 가지 호출 규약들을 살펴볼 수 있습니다.

이 강의에서는 리눅스 gcc에서 x86 바이너리를 컴파일할 때 사용하는 cdecl 그리고, x86-64 바이너리를 컴파일할 때 사용하는 SYSTEM V 호출 규약에 대해 살펴보겠습니다.

x86