[리버싱] 함수 호출 규약
함수 호출 규약(calling convention) 이란
함수를 호출 할 때 인자를 전달하는 방식과 함수 실행이 끝나고 스택을 정리하는 방식에 대한 약속이다.
하나의 Function(Caller) 에서 또 다른 Function(Callee)을 호출할 때 사용되는 일종의 protocol 인 것.
중요한 것은 caller 가 어떻게 parameters 를 callee 에 전달하는지, caller 또는 callee 가 stack 을 clean 하는지 등등은 debugging 시에 중요한 정보가 될 수 있다.
함수 호출 규약에는 크게 'cdecl', 'stdcall', 'fastcall' 이렇게 세가지 방식이 있다.
'cdecl' 방식
인자가 오른쪽에서 왼쪽으로 순서대로 스택으로 전달된다.
함수가 종료될 때 호출자가 피호출자의 스택 프레임을 정리한다.
'stdcall' 방식
인자가 오른쪽에서 왼쪽으로 순서대로 스택으로 전달된다.
함수가 종료될 때 피호출자 스스로 스택 프레임을 정리한다.
'fastcall' 방식
인자가 오른쪽에서 왼쪽으로 순서대로 레지스터를 사용해서 전달된다.
레지스터를 사용하기 때문에 별도로 스택을 정리할 필요가 없다.
예제
고급언어로 프로그래밍 했을 때 함수 caller() 가 서브루틴 callee를 1과 2 두개의 인자를 전달하면서 호출하는 로직이 'cdecl', 'stdcall', 'fastcall' 로 각각 어떻게 구현되는지 알아보자
<고급언어>
viod caller{
callee(1, 2);
return;
}
* 함수를 호출하는 자 : caller (호출자)
호출되는 자 : callee (피호출자)
<어셈블리어>
1. 'cdecl' 방식
[caller]
PUSH 2
PUSH 1
CALL callee
ADD ESP 8
인자는 오른쪽에 있는 2부터 callee() 함수로 전달된다.
스택에 있는 값을 사용할 때는 입력한 순서와 반대로 가장 나중에 입력한 값부터 사용할 수 있다. (stack은 LIFO이기 때문에)
따라서 callee() 함수에 인자는 2와 1 순서대로 전달된다.
callee() 함수가 종료되면,
caller() 함수에서 피호출자(callee)가 사용한 스택을 정리해준다.
4바이트 2개 인자를 스택에 넣었기 때문에 모두 8바이트를 정리해야한다.
따라서 'ADD ESP 8' 명령어를 통해 현재 스택의 위치를 8바이트 만큼 증가시켜준다.
2. 'stdcall'
[caller]
PUSH 2
PUSH 1
CALL callee
....
[callee]
PUSH EBP
MOV EBP, ESP
....
POP EBP
RETN 8
다음으로 stdcall 방식을 알아보면
인자는 오른쪽에 있는 2부터 callee() 함수로 전달된다.
스택에 넣을 때는 2부터 넣지만 사용할 때는 이와 반대로 1부터 사용할 수 있다.
callee 의
' PUSH EBP
MOV EBP, ESP
....
POP EBP '
부분은 일반적인 함수 prolog 영역으로 스택프레임을 생성하고 제거하는 부분이다.
마지막으로 callee() 함수가 종료되면서 caller() 함수로 반환할 때
사용한 스택 영역 8바이트를 스스로 정리한다.
3. 'fastcall'
[caller]
MOV EDX 2
MOV ECX 1
CALL callee
....
[callee]
PUSH EBP
MOV EBP, ESP
....
MOV DWORD PTR SS:[EBP-8], EDX
MOV DWORD PTR SS:[EBP-4], ECX
....
POP EBP
RETN
마지막으로 fastcall 방식이다.
MOV EDX 2
MOV ECX 1
-> 레지스터에 인자를 할당할 때는 왼쪽에 있는 1부터 순서대로 할당된다.
MOV DWORD PTR SS:[EBP-8], EDX
MOV DWORD PTR SS:[EBP-4], ECX
-> callee() 함수 안으로 들어가서 레지스터에 들어가있는 값을 사용할 때는
먼저 스택에 복사한 다음에, 연산할 때는 스택에 있는 값을 사용한다.
레지스터를 사용하기 때문에 별도로 스택을 정리할 필요가 없다.