함수 호출 규약

Wooki·2021년 8월 10일
0
post-thumbnail

함수 호출 규약?

Calling Convention, 해석하면 '함수 호출 규약' 이란 어떤 함수를 호출 할 때, 그 함수의 파라미터(인자)를 어떤 방식으로 전달하는가에 대한 하나의 약속이다.

함수를 호출 할 때, 프로세스에 정의되어있는 스택 메모리 공간을 이용해서 파라미터를 함수로 전달하게 되고, 이 스택 메모리 공간은 프로세스가 실행 될 때에 해당 PE헤더에 스택 메모리의 크기가 명시되어 있다.

스택에 저장되는 값들은 임시적인 값들이기 때문에 더이상 사용하지 않더라도 값을 지우거나 하지 않는다, 굳이 지우려고 하지 않더라도 스택에 다른 값을 입력할 때 저절로 덮어 쓰기 때문에 불필요한 CPU 자원 소모의 낭비를 막기 위해서 그대로 내버려 둔다.

만약 스택이 가득 찼다면?

ESP(스택 포인터)가 스택의 끝을 가리키고 있다면 더 이상 스택을 사용할 수 없다. 그렇기 때문에 함수를 사용한 후에 ESP(스택 포인터)의 위치를 함수 시작 전으로 돌려주면서 사용가능한 스택 메모리의 공간을 확보한다,
이 때 ESP를 어떻게 정리하는가 가 바로 이 포스트의 주제인 함수 호출 규약이다.

Caller 와 Callee

함수 호출 규약에 대해 알아보기 전에 Caller와 Callee에 대한 설명을 하고 넘어가자.

Caller는 호출자다 즉, 함수를 호출한 쪽이다.
Callee는 피호출자다 즉, 호출을 당한 함수를 말한다.

예를 들어보자,
main() 함수에서 add() 라는 함수를 호출했다면,
이 때, Caller는 main()함수가 될 것이고, Callee는 add()가 된다.

cdecl

cdecl 방식은 주로 C언어에서 사용되는 방식이다.
Caller(호출자)가 직접 스택을 정리하는 방식을 가지고 있다.

#include "stdio.h"

int add(int a, int b)
{
    return (a + b);
}

int main(int argc, char* argv[])
{
    return add(1, 2);
}

main에서 add함수를 호출하며 그 인자로 상수1과 2를 넘겨주는 간단한 코드다.

이때 Caller는 main Callee는 add가 된다.


디버거로 살펴보면 401010[main]함수에서 파라미터 1,2를 401000[add]로 넘겨주고, PUSH 2 PUSH 1
함수가 끝난 뒤 Caller였던 main함수에서 ADD ESP,8 코드를 통해서 스택포인터의 위치를 조정해준다.

이와 같이 Caller가 직접 스택을 정리하는 방식이 cdecl 방식이다.

cdecl의 장점으로, 가변 길이 파라미터를 전달할 수 있따는 장점이 있다.

stdcall

#include "stdio.h"

int _stdcall add(int a, int b)
{
    return (a + b);
}

int main(int argc, char* argv[])
{
    return add(1, 2);
}

이번 코드는 이전 코드와 흡사하지만 조금 다르다 add()를 잘 살펴보면
_stdcall이라는 옵션이 붙어있다.

stdcall 방식으로 컴파일하고 싶을 때는 _stdcall 키워드를 붙여주면 된다.

디버거로 살펴보니 cdecl 방식에서는 main()에서 ADD ESP,8을 이용해서 스택을 정리해줬지만, 이번에는 add()함수에서 RETN 8을 통해서 Callee가 직접 스택을 정리하는 모습을 볼 수 있다.
이처럼 Callee가 직접 스택을 정리하는 방식이 stdcall 방식이다.

RETN 8은 RETN + POP 8 Bytes의 의미와 같다.
(리턴 후 지정된 크기만큼 ESP 증가)

stdcall은 어떤 경우에 주로 사용될까?

stdcal방식의 장점은 호출되는 함수내에 스택을 정리하는 코드가 있기 때문에 함수를 호출할때마다 ADD ESP, * 명령을 치지 않아도 되서 전체적인 코드의 길이가 작아지고 Win32 API의 경우 C언어로 만들어진 라이브러리지만, stdcall 방식을 사용하고 있다.(호환성을 위해서)

profile
웹 개발자

0개의 댓글