0%

함수 호출 규약 x86

이번 포스팅에서는 Intel사의 X86 processor의 함수 호출 규약에 대해 알아볼 것입니다.


함수 호출 규약

함수 호출 규약은 컴퓨터에서 함수를 호출하고, 함수를 구현하기 위해서 정한 표준 규약이라고 보면 됩니다. 이러한 함수 호출 규약을 통해서 컴파일러가 어떻게 서브루틴(호출된 함수)에 접근할지를 정하게 됩니다

함수 호출 규약은 어떻게 만드냐에 따라 다르겠지만, 기본적으로 함수에 전달되는 인자를 어떻게 다룰지, 함수의 리턴 값을 어떻게 다룰지, 함수의 스택과 스택프레임을 어떻게 관리할지를 정해야 합니다

이러한 함수 호출 규약은 C/C++ 에서 작성된 함수들이 어셈블리 언어로 변경될 때 사용됩니다. C 언어에서 32-bit x86 시스템의 주요 3가지 함수 호출 규약은 STDCALL, CDECL, FASTCALL가 있습니다.

함수 호출 규약을 알기 전에 아래의 몇가지 용어에 대해 알아야합니다. 해당 용어의 의미는 해당 사이트에서 Notes on Terminology 항목을 참고 하시면 됩니다.

  • Passing arguments
  • Right-to-Left and Left-to-Right, Return value
  • Cleaning the stack, Calling function(the caller)
  • Called function(the callee)
  • Name Decoration(Name mangling)
  • Entry sequence(the function prologue)
  • Exit sqeuence(the function epliogue), Call sequence

C 언어 함수 호출 규약

앞서 언급했던 C 언어에서 사용되는 함수 호출 규약 CDECL, STDCALL, FASTCALL이 무엇인지 알아 볼것입니다.


CDECL 호출 규약

CDECL은 C언어에서 일반적으로 사용되는 함수 호출 규약 입니다. 이는 컴파일러마다 다르기 때문에 확인을 해봐야합니다. 그리고 프로그래머가 명시적으로 어떤 함수 호출 규약을 사용할지 명시적으로 지정을 할 수 있습니다. 명시적으로 어떤 함수 호출 규약을 사용할지 명시하는 방법은 ISO-ANSI 표준이 아니기 때문에 컴파일러에 따라 사용하는 방법이 다를 수 있습니다.

CDECL 호출 규약에서는 기본적으로 파라미터들은 스택에 오른쪽에서 왼쪽으로 쌓여서 전달되고, 리턴 값은 eax 레지스터에 집어 넣게 되어 있습니다. 그리고 호출 함수(The calling function)에서 스택을 정리합니다. 마지막으로 가변 인자 함수(va_start(), va_arg()를 사용하는 함수) 사용이 가능합니다. 컴파일러 또는 링커에서 해당 함수에서 파라미터의 개수가 적절하게 사용되었는지 확인을 할 수 없기 때문에 이부분은 전적으로 프로그래머한테 달려 있습니다.

일반적으로 CDECL 함수 호출 규약을 이용해서 함수를 정의한다면, 아래처럼 정의 할수 있습니다.

1
2
3
4
_cdecl int foo(int a, int b)
{
return a + b;
}

해당 함수를 사용한다면 아래와 같이 사용할 수 있습니다.

1
x = foo(2,3);

해당 함수를 어셈블리어로 바꾸면 다음과 같습니다. 해당 부분은 어셈블러마다 다를 수 있기 때문에 참고 사항으로 보면 됩니다.

  • 함수 내부
1
2
3
4
5
6
7
8
_foo:
push ebp
mov ebp, esp
mov eax, [ebp+8]
mov edx, [ebp+12]
add eax, edx
pop ebp
ret
  • 함수 호출 전/후
1
2
3
4
push 3
push 2
call _foo
add esp, 8

이처럼 C 언어로 작성된 함수를 어셈블리어로 변환해서 보면, 위에서 언급한 특징들이 보입니다. CDECL의 특징을 요약하면 다음과 같습니다.

  • 파라미터는 오른쪽에서 왼쪽으로 스택에 쌓여서 전달된다.
  • 리턴값은 eax에 저장된다.
  • Calling function에서 스택을 정리한다.
  • 가변인자 함수를 정의해서 사용할 수 있다.

STDCALL 호출 규약

STDCALL 호출 규약은 WINAPI로서 알려져 있고, 일반적으로 Microsoft에서 Win32 API의 표준 호츌 규약으로 사용됩니다. STDCALL은 Microsoft에 의해 엄격하게 정의되므로 이를 구현 하는 모든 컴파일러는 동일한 방식으로 작동된다고 합니다.

STDCALL 또한 CDECL처럼 파라미터는 오른쪽에서 왼쪽순으로 스택에 쌓이고, 리턴값은 eax에 저장됩니다. CDECL과 달리 호출된 쪽(THe called function)에서 스택을 정리를 합니다. 호출된 쪽에서 스택을 정리 하기 때문에 가변 인자 함수를 정의해서 사용할 수 없습니다.

STDCALL에서 가변 인자 함수를 정의해서 사용할 수 없는 이유는 C언어로 작성된 소스코드를 어셈블리어로 변환해서 보면 그 이유를 확실하게 알 수 있습니다. 해당 부분은 STDCALL을 이용해서 C 언어로 함수를 정의하는 방법, 함수 호출 방법, 어셈블리어로 정의하는 방법을 통해 알아봅시다

일반적으로 C 언어를 통해서 함수를 작성하는 방법은 다음과 같습니다.

1
2
3
4
_stdcall int foo(int a, int b)
{
return a + b;
}

다음은 C 언어로 작성된 함수 호출 방법 입니다.

1
x = foo(2, 3);

C 언어로 작성된 함수를 어셈블리어로 변환시 다음과 같습니다.

  • 함수 내부
1
2
3
4
5
6
7
8
:_foo@8
push ebp
mov ebp, esp
mov eax, [ebp+8]
mov edx, [ebp+12]
add eax, edx
pop ebp
ret 8
  • 함수 호출 전/후
1
2
3
push 3
push 2
call _foo@8

드디어 어셈블리로 변환되니 왜 STDCALL에서는 가변 인자 함수를 정의할 수 없는지 알 수 있습니다. 함수 내부를 살펴보면, CDECL과 달리 STDCALL은 호출된 쪽에서 스택을 정리한다는 것을 알 수 있습니다. 여기서 스택을 정리하는 기법은 ret instruction을 이용해서 합니다. 해당 ret instruction 문서를 살펴보면, “ret imm16” 1mm16에 적힌 값만큼 스택을 정리한다는 것을 알 수 있습니다. 여기서는 8바이트만큼 스택을 정리하기 위해서 ret 8을 사용했습니다. 이 때문에 STDCALL 함수 호출 규약을 이용해서는 가변 인자 함수를 정의할 수 없다는 것입니다.

만약에 STDCALL 함수가 가변 인자 정의 함수를 정의할 수 있다면, 인자의 갯수에 따라서 함수 내부가 매번 바껴야 합니다. 예를 들어, 4바이트 크기의 인자가 1개면 ret 4, 4바이트 크기의 인자가 5개면 ret 20 으로 함수 내부가 바껴야 한다는 것입니다. 그렇다면, 파라미터의 길이의 차이 때문에 파라미터의 길이가 다른 것들이 몇개 있는지에 따라서 함수 내부를 여러개 작성해줘야 합니다. 이는 비효율적이며 컴파일러에서 지원을 하지 않습니다. 이러한 이유 때문에 STDCALL 함수 호출 규약을 이용해서는 가변 인자 함수를 정의를 할 수 없는 것입니다.

그리고 STDCALL 일명 WINAPI는 CDECL과 달리 네임 맹글링 방법에서 한 가지 다른점이 존재합니다. CDECL에서는 함수 이름 앞에 언더바()를 붙이지만, STDCALL에서는 함수 이름 앞에 언더버()와 스택에 전달된 파라미터의 수로 붙이게 되어있습니다. 해당 수는 바이트 단위로 작성하게 되어 있고, 32비트 시스템에서는 항상 4의 배수로 작성하도록 되어 있습니다.

이처럼 C 언어로 작성된 함수를 어셈블리어로 변환해서 보면, 위에서 언급한 특징들이 보입니다. STDCALL의 특징을 요약하면 다음과 같습니다.

  • 파라미터는 오른쪽에서 왼쪽으로 스택에 쌓여서 전달된다.
  • 리턴값은 eax에 저장된다.
  • Called function에서 스택을 정리한다.
  • 가변인자 함수를 정의해서 사용할 수 없다.

FASTCALL 호출 규약

FASTCALL 호출 규약은 모든 컴파일러에서 표준 규약이 아니기 때문에 조심해서 사용해야 합니다. 함수를 정의할 때, 일반적으로 몇개의 파라미터는 레지스터를 통해서 전달되고, 나머지는 STDCALL 호출 규약처럼 오른쪽에서 왼쪽순으로 스택을 이용해서 전달됩니다. 따라서 파라미터에 수에 따라서 호출된 쪽(The called function)에서 스택을 정리할 때도 있습니다. 파라미터가 레지스터를 이용해서 전달되기 때문에 속도가 빠르다라는 특징 이외에는 STDCALL의 특징과 동일합니다.

참고 자료

x86 Disassembly/Calling Conventions - wiki

x86 calling conventions - wiki

Calling convention - wiki

Calling Conventions - MSDN

Intel x86 Function-call Conventions - Assembly View