C언어/잡학사전

[C언어] 함수 호출 규약 (Calling Convention, cdecl, stdcall, fastcall)

아무일도없었다 2022. 9. 14. 02:18
함수 호출 규약 ?

함수 호출 시 필요한 인자를 어떤 방법으로 전달할지(전달받을지), 함수 종료 시 데이터를 어떤 방식으로 전달할지(전달받을지), 스택을 어떤 방법으로 정리할지 정의해놓은 규칙을 의미한다.

 


 

C언어를 제법 사용한 개발자 중에서도 함수 호출 규약 (Calling Convention) 이라는 단어를 처음 보는 사람도 있을 것이다.

 

이를 정확하게 이해하려면 프로세스의 구조와 스택, 어셈블리 언어에 대한 이해가 필요하다.

 

하지만 위의 이해를 모두 설명하면 끝이 없으니, 심도 깊은 글은 나중에 시간이 된다면 정리해보는 거로 하고 이 글에서는 간략하게 설명해볼까 한다.

 


 

스택(Stack) ?

프로세스 실행 시 필요한 여러 데이터들은 메모리에 적재되는데 프로세스의 메모리 영역에는 스택이라는 부분이 있다.

 

해당 영역에는 함수의 인자, 지역 변수, 함수 호출 이후 복귀해야 할 주소 같은 값들이 저장된다.

 


 

 

함수 호출 규약에는 여러 규약이 있지만 이 글에서는 주요하게 사용하는 3가지 규약을 다루려고 한다.

 

1. cdecl

cdecl 은 주로 표준 C언어 API 에서 많이 사용하는 규약으로 함수를 호출하는 Caller 에서 스택(Stack)을 정리하는 방식이다. (C언어에서 함수 호출 규약을 명시하지 않으면 cdecl 로 동작한다.)

 

cdecl 호출 방식

 

위의 그림은 main 함수에서 func(1,2) 를 호출하는 상황을 예시로 들었고, 모든 주소는 임의의 값을 지정하였다.

 

실제 stack 에 저장되는 값은 파라미터뿐만 아니라 돌아올 주소 값과 사용 중인 지역변수도 있지만 생략하였다.

 

순서대로 살펴보면

 

  1. func 에 넘길 파라미터를 2 , 1 순서로 스택에 저장한다.
    (stack 구조상 저장 시 데이터 크기만큼 stack pointer 값 감소)

  2. func 함수를 호출(이동) 한다.
  3. func 에서는 stack pointer 를 사용하여 스택에 저장된 데이터를 사용한다. (파라미터)
  4. return 이후 main 함수로 복귀한다.
  5. return 이후 main 함수에서 stack pointer 를 func 의 파라미터로 줄어든 만큼 다시 증가시킨다. (스택 정리)

 

함수 호출 시 Caller(main)에서 stack pointer 의 제어를 모두 하고 있기 때문에 가변 인자를 사용하는 함수를 호출하는 경우  주로 사용된다. (가변 인자의 크기를 Caller(main) 에서만 알 수 있기 때문)

 

 


 

2. stdcall

stdcall 은 Win32 API에서 주로 사용하는 규약으로 호출을 당하는 Callee 에서 스택(Stack)을 정리하는 방식이다.

(C언어의 기본은 cdecl 방식이기 때문에 stdcall 을 사용하기 위해서는 함수 이름 앞에 _stdcall 키워드를 붙여야 한다.)

 

stdcall 호출 방식

 

순서를 보면 cdecl 과 모두 동일하지만 마지막 return 부분에서 stack pointer 를 정리하는 방식이 다르다.

 

cdecl 의 경우 stack pointer 를 Caller(main) 에서 정리했다면, stdcall 은 Callee(func) 에서 stack pointer 를 정리한다.

 

Caller 에서 정리하든 Callee 에서 정리하든 그림으로만 보면 별 차이가 없어 보이지만 이는 몇 가지 장점이 있다.

 

cdecl 대비 stdcall 의 장점

 

먼저 어셈블리에서는 (return) 과 (return & sp = sp + 0x8) 의 코드 모두 한 줄로 정의가 된다.

 

하지만 cdecl 방식에서는 return 코드 한 줄과  스택을 정리하는 코드가 각각 정의된다.

 

따라서 stdcall 방식은 cdecl 대비 어셈블리 코드 한 줄을 줄일 수 있다.
(※ 어셈블리 한 줄이라고 무시하면 안된다. 왜냐하면 함수 호출 마다 어셈블리 한줄이 줄어드는 것이고, 이는 CPU 명령 하나를 줄이는 것이기 때문에 실제 성능개선으로는 상당한 차이를 보이게 된다.)

 

 

두 번째로 Delphi, Visual Basic 과 같은 언어와의 호환성이 좋아진다.

 

Delphi 를 예시로 보면 cdecl, stdcall, pascal, fastcall, safecall 과 같은 함수 호출 규약이 있다.

 

그중에서도 stdcall 은 일반적으로 많이 사용하고, Windows 표준으로 사용되기 때문에 DLL(API) 호출 시 호환성이 뛰어나기 때문이다.

 

 


 

3. fastcall

fastcall 은 cdecl 과 매우 유사하지만 전달할 인자의 개수에 따라서 스택(stack)을 사용하지 않는다.

(C언어의 기본은 cdecl 방식이기 때문에 fastcall 을 사용하기 위해서는 함수 이름 앞에 _fastcall 키워드를 붙여야 한다.)

 

fastcall 호출 방식

 

파라미터가 2개 이하일 경우 ECX, EDX 라는 레지스터에 저장하는 방식으로 인자를 전달한다.
(레지스터는 memory 가 아닌 CPU와 같이 붙어있기 때문에 stack 에 비해 접근과 처리속도가 훨씬 빠르다.)

 

하지만 ECX(Extended Counter Register)와 EDX(Extended Data Register) 레지스터를 기존에 사용 중일 경우 백업을 하거나 추가적인 관리를 해야 하는 경우가 생길 수 있다.

 

이러한 예외 상황의 경우 추가적인 비용이 발생하기 때문에 이름과는 다르게 무조건적으로 빠르다고는 할 수 없다.

 

또한 파라미터 개수가 2개가 넘어갈 경우 cdecl 방식과 마찬가지로 3번째 파라미터부터 스택(stack)을 사용한다.

 

따라서 상황에 맞는 함수 호출 규약을 사용해야 한다.

 

 


 

마무리

대표적인 함수 호출 규약 3가지를 정리했지만 이 외에도 다른 방식의 규약이 있다.

 

하지만 직접 개발을 진행하면서 해당 호출 규약을 직접 명시하면서 사용할 일은 거의 없다고 봐도 된다.

 

컴파일러가 너무 똑똑해진 나머지 최적화를 알아서 척척 진행해주고 있기 때문에 개발자 입장에서는 함수 호출 규약은 신경을 크게 쓰지 않아도 되기 때문이다.

 

다만 C언어 API 가 정의되어있는 header 를 통해 보이는 _cdecl, _stdcall 과 같은 지시어를 보고 궁금점이 생긴 경우 이 글이 도움이 됐기를 바라본다.

 

또한 리버싱 또는 크랙과 같은 해킹을 목표로 삼은 경우는 위의 내용보다 더 깊은 내용을 이해해야 한다.

 

역시 알면 알수록 모르는 게 더 늘어나는 거 같다..

반응형