함수 인자 전달방식
- 매개변수(인자)를 전달하는 방법에는 두 가지 방법이 있다
- 레지스터에 넣기
- 스택에 넣기
.model flat
public _asm_main
.code
_asm_main:
call _add
ret
_add: ; _add 호출 시 인자를 넣어보자.
mov eax, 100
ret
end
- 레지스터에 인자 넣기
- 스택에 인자 넣기
레지스터에 인자 넣기
관례적으로 ecx edx, esi, edi 를 인자 레지스터로 많이 쓴다.
.model flat
public _asm_main
.code
_asm_main:
mov edx, 2
mov ecx, 2
call _add
ret
_add:
mov eax, edx ; eax = edx
add eax, ecx ; eax += ecx
ret
end
- 장점 : 빠르다
- 단점 : 인자의 개수에 제한이 있다
스택에 인자 넣기
push, pop을 이용해 구현하면 될까?
.model flat
public _asm_main
.code
_asm_main:
push 2 ; 2번째 인자
push 1 ; 1번째 인자
call _add
ret
_add:
; 단순히 pop을 쓸 수 없는게 call은 스택에 돌아올 메모리를 담는다.
; 좀 더 자세히 설명하면 함수를 call하면 스택에 'push 돌아올 주소'와 동일하다
; 단순히 pop을 해버려서 돌아올 주소를 pop해버릴경우 돌아갈 주소를 알 수 없게 된다.
; 따라서 절대 pop을 쓰면 안되는게 돌아갈 주소를 알수 없게 되어버린다
ret
end
그럼 어떻게하나??
- ESP를 써보자
- ESP(Extended Stack Pointer) : 가장 최근에 사용한 스택을 가리키는 레지스터
- ESP 기준 4바이트 떨어진 주소값으로 처리해야한다.
아래와 같이 구현하면 될까?
.model flat
public _asm_main
.code
_asm_main:
push 2
push 1
call _add
ret
_add:
mov eax, dword ptr[esp+4]
add eax, dword ptr[esp+8]
; 이렇게 구현하면 에러발생
; why?
; ret를 하며 문제가 발생한다
; ret는 `pop 돌아갈 주소`(스택의 바로 위 주소)
; `jmp 꺼낸주소`
; 이런식으로 동작하는데 조금더 자세한 설명을 위해 아래 참조
ret
end
_main 으로 돌아갈 주소
2
1
_asm_main 으로 돌아갈 주소 ( <- 현재 esp )
ret
호출 시
_main 으로 돌아갈 주소
2
1 ( <- 현재 esp )
_asm_main 으로 돌아갈 주소
스택에 데이터를 2개를 쌓았기에 _main 으로 돌아갈 주소
로 돌아가야하는데 데이터 1로 돌아가게 된다.
- 해결책) 함수 호출이 종료되면 인자 전달에 사용된 스택은 반드시 파괴되어야 한다.
.model flat
public _asm_main
.code
_asm_main:
push 2
push 1
call _add
add esp, 8 ; esp = esp + 8
; 이렇게만 해줘도 스택이 파괴된다.
ret
_add:
mov eax, dword ptr[esp+4]
add eax, dword ptr[esp+8]
ret
end
_main으로 돌아갈 주소 ( <- 현재 esp )
2
1
_asm_main 으로 돌아갈 주소
인자 전달과 스택 파괴
- 파괴하는 방식이 두 가지다.
- 호출자 파괴 -
add esp, 8
: esp를 조정- 코드가 길어진다(함수 호출시 마다 추가되어야 함)
- 피호출자 파괴 -
ret 8
: 리턴과 동시에 파괴- 코드가 짧아지며 피호출자에서 파괴하기에 에러(메모리 오접근)가 적다
- 호출자 파괴 -
.model flat
public _asm_main
.code
_asm_main:
push 2
push 1
call _add
add esp, 8 ; (호출자 파괴) 다른 방법으로 파괴할 수 있을까?
ret
_add:
mov eax, dword ptr[esp+4]
add eax, dword ptr[esp+8]
ret
end
_add:
mov eax, dword ptr[esp+4]
add eax, dword ptr[esp+8]
ret 8 ; 이렇게 파괴가능 (호출 당한 파괴)
end
Calling Convention : C언어는 인자를 어떻게 보낼까?
#include <stdio.h>
int add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = 0;
ret = add(1, 2); // 1, 2는 레지스터로 갈까 스택으로 갈까?
printf("result : %d\n", ret);
}
컴파일러 마다 다르겠지만
push 2
push 1 ; 스택에 넣고
call _add ; 함수호출
add esp, 8 ; 호출한 곳에서 파괴
인라인 어셈블로 제작해보자.
int main()
{
int ret = 0;
__asm
{
push 2
push 1
call add
add esp, 8
mov ret, eax
}
printf("result : %d\n", ret);
}
정리
/*
함수 호출 규약(calling convention)
* 호출 규약에 따라 달라지는 부분은 함수 이름 규칙, 인자 전달 방법, 스택 파괴 위치 이다.
__cdecl : 함수 이름 (_f) / 인자 전달 (스택) / 파괴 위치 (호출자)
__stdcall : 함수 이름 (_f@인자크기) / 인자 전달 (스택) / 파괴 위치 (피호출자)
__fastcall : 함수 이름 (@f@인자크기) / 인자 전달 (2까지 레지스터, 3부터 스택) / 파괴 위치 (3개 이상 피호출자)
* 단, 주의할 점은 가변인자로(...) 보낼경우 무조건 __cdecl로 호출규약이 고정됨
*/
void __cdecl f1(int a, int b) {}
void __stdcall f2(int a, int b) {}
void __fastcall f3(int a, int b) {}
void __fastcall f4(int a, int b, int c, int d) {}
int main() // __cdecl : 디폴트
{
f1(1, 2);
f2(1, 2);
f3(1, 2);
f4(1, 2, 3, 4);
}
; __cdecl
push 2
push 1
call _f1
add esp, 8
; __stdcall
push 2
push 1
call _f2@8
; __fastcall
mov edx, 2
mov ecx, 1
call @f@8
; __fastcall(인자 3개 이상)
push 4
push 3
mov edx, 2
mov ecx, 1
call @f4@16
- 차이점?
- 결국 차이점은 스택의 파괴 위치이다.
__cdecl
:- 호출자에서 스택정리
- 가변인자(…) 처리가능
__stdcall
:- 피호출자에서
ret
를 통해 스택정리 - 빠르다(스택을 별도의 명령으로 정리하지 않고
ret
를 통해 정리해서)- 왜? 자세한 사항은 나도 모름…
- 피호출자에서
- DLL이나 OS에서는
__stdcall
을 많이 사용하는데 프로그래밍 언어 사이의 표준이기에 그렇다.- 아마 빨라서가 아닐까?
void f1(int n, ...) {} // __cdecl
void __stdcall f2(int n, ...) {}
void __fastcall f3(int n, ...) {}
int main()
{
f1(1, 2); // add esp, 8
f1(1, 2, 3, 4); // add esp, 16
f2(1, 2); // 함수내에서 ret8을 해줘야 하는데 가변 매개변수는 ret 몇을 해줘야하는지 알 수 없음
// 컴파일러가 자동으로 __cdecl로 변경한다.
f3(1, 2); // 상동
}
어셈블리 언어에서 C함수 호출
#include <stdio.h>
int asm_main();
int main()
{
asm_main();
}
void __cdecl f1(int a, int b) { printf("f1 : %d, %d\n", a, b); }
void __stdcall f2(int a, int b) { printf("f2 : %d, %d\n", a, b); }
void __fastcall f3(int a, int b) { printf("f3 : %d, %d\n", a, b); }
void __fastcall f4(int a, int b, int c, int d)
{
printf("f4 : %d, %d, %d, %d\n", a, b, c, d);
}
f1, f2, f3, f4를 어셈블리에서 호출해보자.
.model flat
public _asm_main
; c에 있는 함수를 include한다고 생각
extern _f1: proc
extern _f2@8: proc
extern @f3@8: proc
extern @f4@16: proc
; printf, MessagBox 써보기용 include
extern _printf: proc
extern _MessageBoxA@16: proc
; printf, MessagBox 써보기용 string
.data
S1 DB "hello", 10, 0
; 참고) 10은 아스키 코드로 '\n'을 말하며 0은 string의 마지막 문자 null
.code
_asm_main:
; f1(1, 2) 호출
push 2
push 1
call _f1
add esp, 8
; f2(1,2)
push 2
push 1
call _f2@8
; f3(1,2)
mov edx, 2
mov ecx, 1
call @f3@8
; f4(1,2,3,4)
push 4
push 3
mov edx, 2
mov ecx, 1
call @f4@16
; 추가) printf 출력
push offset S1
call _printf
add esp, 4 ; __cdecl 이라서 호출자에서 스택정리
; MessageBoxA(0, "Hello", "Hello", MB_OK) 호출
push 0
push offset S1
push offset S1
call _MessageBoxA@16 ; __stdcall이라 피호출자에서 정리
ret
end