0. 들어가며
- 함수 호출 규약 : 함수의 호출 및 반환에 대한 약속
- 함수 호출 시 프로그램의 실행 흐름이 다른 함수로 이동
- 이 때, 기존 함수로 돌아오기위해 호출 방식에 대한 약속을 규정
1. 함수 호출 규약
- 함수를 호출할 때는
- 반환된 이후를 위한 호출자의 StackFrame 및 반환 주소 저장
- 피호출자가 요구하는 인자 전달
- 피호출자의 실행 종료 시 반환 값 전달
- 함수 호출 규약을 적용하는 것은 일반적으로 컴파일러
2. 함수 호출 규약의 종류
- CPU 아키텍처와 컴파일러 종류에 따라 함수 호출 규약도 달라짐
- x86(32비트) 아키텍처
- 레지스터를 통해 피호출자의 인자를 전달하기에는 레지스터의 수가 적어 스택을 이용하는 함수 호출 규약 사용
- cdecl
- stdcall
- fastcall
- x86-64
- 레지스터가 많으므로 적은 수의 인자는 레지스터를, 인자가 많을 경우는 스택을 사용
- SYSV
- MSCV
3. x86호출 규약: cdecl
- cdecl 호출 규약은 c/c++ 함수에서 기본적으로 사용되는 호출 규약
- x86 아키텍처는 레지스터의 수가 적어 스택을 통해 인자 전달
- 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 것이 cdecl 특징
- 스택을 통해 인자 전달 시, 마지막 인자부터 스택에 PUSH
int sum(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
sum(5, 4);
return 0;
}
- 위 C코드를 빌드한 후 main함수 확인
00401068 |. 6A 04 PUSH 4
0040106A |. 6A 05 PUSH 5
0040106C |. E8 94FFFFFF CALL Consolas.00401005
00401071 |. 83C4 08 ADD ESP,8
- sum의 마지막 인자인 4부터 stack에 PUSH
- sum 함수 호출 이후 ADD ESP, 8을 통해 스택에 올라간 인자를 호출자가 정리
- cdecl은 이처럼 호출자가 피호출자의 인자를 정리
4. x86호출 규약: stdcall
- stdcall 방식은 Win32 API에서 사용
- 피호출자가 스택을 정리
- Win32 API에서는 가변 인수 함수가 없어 매개변수의 개수가 고정적 → 피호출자가 스택을 정리하는 것이 효율적
int __stdcall sum(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
sum(5, 4);
return 0;
}
- cdecl의 코드와 동일한 동작을 하는 stdcall 코드
00401068 |. 6A 04 PUSH 4
0040106A |. 6A 05 PUSH 5
0040106C |. E8 9EFFFFFF CALL Consolas.0040100F
- 해당 코드의 main문 일부
- cdecl과 동일하게 stack에 인자를 PUSH
- 하지만 stack을 호출자인 main에서 정리하는 것이 아닌 sum함수에서 정리
00401020 >/> 55 PUSH EBP
00401021 |. 8BEC MOV EBP,ESP
..
00401038 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
0040103B |. 0345 0C ADD EAX,DWORD PTR SS:[EBP+C]
..
00401041 |. 8BE5 MOV ESP,EBP
00401043 |. 5D POP EBP
00401044 |. C2 0800 RETN 8
- sum함수의 일부
- 함수의 마지막 부분의 RETN 8을 통해 함수 내 스택을 정리
- 반환을 한 후 ESP를 8만큼 증가시킴
5. x86호출 규약: fastcall
- 스택이 아닌 가까운 레지스터를 사용하여 호출 속도가 빠름
- 호출된 함수 내에서 사용된 스택은 정리
- 인자 전달을 위해 스택을 사용하지 않고 레지스터를 이용하므로 따로 정리를 하지 않음
int __fastcall sum(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
sum(5, 4);
return 0;
}
- cdecl, stdcall과 같은 기능을 하는 fastcall 코드
00401068 |. BA 04000000 MOV EDX,4
0040106D |. B9 05000000 MOV ECX,5
00401072 |. E8 98FFFFFF CALL Consolas.0040100F
- main문의 일부
- EDX와 ECX 레지스터를 사용하여 함수의 인자를 전달
- cdecl, stdcall과 동일하게 마지막 인자부터 전달
00401020 >|> 55 PUSH EBP
00401021 |. 8BEC MOV EBP,ESP
..
0040103A |. 8955 F8 MOV DWORD PTR SS:[EBP-8],EDX
0040103D |. 894D FC MOV DWORD PTR SS:[EBP-4],ECX
00401040 |. 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4]
00401043 |. 0345 F8 ADD EAX,DWORD PTR SS:[EBP-8]
..
00401049 |. 8BE5 MOV ESP,EBP
0040104B |. 5D POP EBP
0040104C |. C3 RETN
- EDX의 레지스터의 값을 EBP-8에 넣고
- ECX의 레지스터의 값을 EBP-4에 넣음
- EAX 레지스터를 통해 두 전달 받은 인자를 더하는 연산
- 레지스터를 이용하여 호출 속도가 빠름
5. x86-64호출 규약: SYSV
- 64비트 리눅스에서 사용하는 함수 호출 규약
- SYSV에서 정의한 함수 호출 규약
- 6개의 인자를 RDI, RSI, RDX, RCX, R8, R9에 순서대로 저장하여 전달, 더 많은 인자는 스택을 추가로 이용
- 호출자가 인자 전달에 사용된 스택을 정리
- 함수의 반환 값은 RAX로 전달
#define ull unsigned long long
ull callee(ull a1, int a2, int a3, int a4, int a5, int a6, int a7) {
ull ret = a1 + a2 + a3 + a4 + a5 + a6 + a7;
return ret;
}
void caller() { callee(123456789123456789, 2, 3, 4, 5, 6, 7); }
int main() { caller(); }
- 스택 사용을 확인하기 위해 7개의 인자 함수를 사용
- 함수 호출 직전의 레지스터와 스택의 상태
- RDI, RSI, RDX, RCX, R8, R9과 RSP에 순서대로 인자가 저장되어있음
- 32비트와 다르게 인자 순서대로 저장
- 함수 종료 시 반환값은 RAX을 통해 전달하며
- SYSV의 스택 정리는 호출자가 정리
6. 마치며
- 위에서 소개한 함수 호출 규약 이외에도 다양한 아키텍처와 컴파일러에 의해 다양한 함수 호출 규약 존재
- 64비트 Window의 경우 MSCV의 함수 호출 규약으로 레지스터 RCX, RDX, R8, R9을 사용하여 호출하며 호출자가 스택을 정리
- 어셈블리어 분석을 하면서 함수 호출 규약을 볼 경우가 많으므로 기억해놓기
'Security > Reversing' 카테고리의 다른 글
리버싱 / 악성코드 분석을 위한 Windows 10 Defender 비활성화 (0) | 2023.07.11 |
---|---|
PE File Format Advance (1) | 2023.07.08 |
스택프레임 (0) | 2023.07.05 |
PE파일 (1) | 2023.07.03 |
어셈블리어 (0) | 2023.07.02 |