함수호출규약 (Calling Convention), cdecl, stdcall

Calling Convection


함수호출규약은 함수(subroutine, callee)가 어떻게 인자를 전달받고 결과값을 반환하는지에 대한 로우레벨에서의 규칙이다. 다양하게 구현된 함수호출규약들은 파라미터의 위치, 리턴 값이나 리턴 주소의 위치, 파라미터의 정리등의 방법에서 차이가 있다.
함수호출규약은 프로그래밍언어의 속도, 코드길이 등과 관련있지만 별차이 없다.

대표적인 함수호출규약으로는 cdecl, stdcall, fastcall이 있다. 이중에 우리는 cdecl과 stdcall에 대해서만 확인해 볼것이다.

cdecl, stdcall 모두 스택 상단에 인자를 쌓고 함수를 호출하는 방식으로 인자를 전달하고 리턴 값은 eax 레지스터를 이용해 받아온다. 즉 함수 호출까지는 cdecl, stdcall 모두 동일한 어셈블리어를 갖고 있다. 둘의 차이는 함수가 모두 실행되고 콜러로 돌아올때 발생한다. 인자를 전달하기 위해 할당한 스택 메모리를 정리 할때 cdecl은 콜리가 스택을 정리하고 stdcall은 콜리에서 콜러로 복귀한 후 콜러가 스택을 정리한다.

확인해보자


먼저 인자를 받아 합을 리턴해주는 간단한 cdecl 방식의 함수를 작성하고 컴파일 한다.
버퍼보호기능은 끄고 최적화는 모두 비활성화 시키고 컴파일 하였다. 컴파일러는 visual C++을 사용하였다.



push 2

push 1
call func

부분을 보면 스택의 상단에 2와 1을 쌓고 func함수를 호출하는 것을 알 수 있다. 2와 1은 func함수 안에서 인자로 사용된다. 인자를 push 함으로 스택은 8bytes만큼 자란다.
func 함수가 모두 실행되고 main함수로 돌아오자마자 add esp, 8 명령어를 만난다. esp에 8bytes를 더해 esp를 내린다. push 2, push 1로 8bytes만큼 잡은 인자를 정리하는 것이다.
func함수도 살펴보자.



cdecl 방식의 func 함수 안이다. func 함수를 호출 하기 이전에 main은 인자 전달을 위해 스택의 상단에 인자를 쌓았다. 그리고 func함수가 호출(RET생성)되고 프롤로그가 실행(SFP생성)되면서 RET, SFP가 인자 위로 쌓였을 것이다. 이제 func함수가 실행이 되는데 main함수에서 넘겨준 인자를 ebp+val 의 방식으로 사용을 한다. func함수가 실행된 후에는 몇개의 레지스터를 복원하고 스택프레임을 func진입당시 초기상태로 복원은 하지만 인자를 정리하는 부분은 찾아볼 수 없다.

func함수에서 main함수로 복귀한 후 main함수가 인자를 정리하는 것을 앞에서 확인했다.
cdecl은 콜러가 인자를 정리하는 것을 알 수 있다.

- cdecl 방식은 Caller에서 인자영역 스택을 정리한다
- 일반적으로 코딩으로 함수를 작성하면 cdecl 방식의 함수

cdecl 에서 인자를 할당받았던 스택을 어떻게 정리하는지 알아봤으니 stdcall도 같은 방식으로 확인해보자.

동일한 코드를 동일한 방법으로 func의 호출규약만 stdcall로 수정하고 컴파일 한다.
어셈블리어로 main함수부터 확인해보자.



cdecl방식과 func함수를 호출하기 이전까지는 같고 func함수가 끝나고 나서 add esp, 8 명령어가 없어졌다. 스택정리를 main에서 하지 않는다. func함수를 확인해보자.




stdcall 방식의 func 함수이다. cdecl방식과 다른게 없어 보이지만 자세히보면 retn에 오퍼랜드로 8이 들어가 있는것을 볼 수 있다. retn 하면서 스택의 인자 부분을 정리하는 코드이다.(ret 하면서 add esp, 8 이 된다.)

stdcall 방식에서는 콜리가 인자를 정리하는 것을 확인할 수 있다.

- stdcall 방식은 Callee가 인자영역 스택을 정리한다.
- 일반적으로 DLL안의 함수가 stdcall 방식의 함수

리눅스에서도 확인을 해보았는데 좀 이상하다.
Visual C++과 똑같이 코드를 작성하고 컴파일하였다.
gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -o cdecl cdecl.c



main함수에도 func함수에도 인자를 정리해주는 add esp, 8이 없다. 인자를 스택의 상단에 쌓아주는 push도 없다. 대신에 mov를 이용해 스택의 상단에 값을 넣어준다. 그리고 func함수는 비슷하게 돌아간다. 초반에 지역변수 메모리를 할당할때 아예 인자의 메모리까지 다 할당해버리고 사용을 한다. int형 변수 2개를 만들고 확인해보니 프롤로그 이후에 esp에 값을 뺄때 16bytes를 뺀다.(int형 변수 8bytes, func에 들어갈 인자 8bytes) 그래서 함수 호출, 복귀시 인자부분을 정리하지 않아도 된다. 인자가 많아져봐야 몇바이트 안될것같고 여러 함수가 호출되어도 mov로 계속 인자를 넣어주면 되니 좋을것같다.

stdcall도 확인해봐야겠다.
똑같은 방법으로 stdcall로만 바꾸고 컴파일 하였다.



main함수에서 sub esp, 0x8을 하고 func함수에서는 ret 0x8를 한다.
func함수에서 main함수로 복귀할때 ret 0x8로 스택을 줄이고 main함수에 돌아와서 sub esp, 0x8로 스택을 다시 키운다. 이렇게 되면 main함수 초기의 스택의 모양와 같아진다. 
리눅스에서 stdcall은 왜있는거지.

댓글 2개: