C++ 함수 호출 원리, 어셈블리 위주 설명 (고급 강의)

C++ 함수 호출 원리, 어셈블리 위주 설명 (고급 강의)


함수 호출 (function call)

기본적으로 클래스 멤버 함수의 호출에는 여러 가지 방법이 있습니다. 파스칼 타입의 C와 C- 형태로 호출하는 C++ 타입 등이 일반적인데, 가장 기본적인 호출 방법은 _stdcall을 사용하는 것입니다. 아래는 이에 대한 예입니다.


__ Cdecl: C, C++ 함수 호출 기본 형태


1
2
3
4
5
6
7
void Input( int &m,int &n);
00401068 lea eax,[ebp-8] ;
0040106B push eax;
0040106C lea ecx,[ebp-4];
0040106F push ecx;
00401070 call @ILT+5(Input) (0040100a);
00401075 add esp,8;
cs


위의 C++ 함수 호출 코드를 통해서 알 수 있는 건, Input() 함수를 호출하기 전에, ebp-8에 push하고, ebp-4에 push하고, 스택의 끝에서 함수 호출을 다음 마지막으로 esp+8을 복구합니다.

그래서, C언어 main() 함수에서 디폴트로 _cdecl를 호출하여 복원하는 것을 확인할 수 있습니다.


여기서, ebp-8과 ebp-4는 무엇일까요?


VC VIEW 디버그 창에 적히는 과정의 순서(Election)는, 선택된 레지스터 - 레지스터 변수 값 - 윈도우 메모리 디버깅 순입니다.


ebp-8의 값과 ebp-4의 값(또는, 직접 지정한 주소의 값)이 담긴 물리적인 저장 공간의 주소는 실제로, 변수 n(ebp-8)은 변수 m(ebp-4)이 push 할 인자의 주소를 나타냅니다(main C++ 함수의 호출은 오른쪽에서 왼쪽으로 이뤄집니다.


또한 변수가 인수를 참조하는 방식은, 물리적인 참조(actually passed by reference), 주소에 의한 참조(address of the variable(포인터 등))입니다.).


요약 : C나 C++에서 기본적인 스택 복원 함수인 _cdecl은 오른쪽에서 왼쪽으로 인자를 전달합니다.


C++ 함수 호출 원리 어셈블리[Function Call] 프로그래밍 기본 이론


WINAPI (actually PASCAL, CALLBACK, the _stdcall)


1
2
3
4
5
6
7
8
void WINAPI Input (int & m,int & n);
 
Look at the assembly code corresponding call:
00401068 lea eax,[ebp-8]
0040106B push eax 
0040106C lea ecx,[ebp-4]
0040106F push ecx 
00401070 call @ ILT +5 (Input)(0040100a)
cs


위의 Input 함수를 살펴보겠습니다.


이 함수를 호출하기 전에, ebp-8의 push, ebp-4의 push의 내용이, Input C++ 함수 자체의 어셈블리 코드로 생성되어 순차적으로 코드의 내용을 읽어 들이며 스택을 호출합니다.


요약 : Windows API의 프로시저에서 사용하는 방식입니다. 파라미터 전달은 __cdcel과 같이 오른쪽에서 왼쪽이지만 파라미터 해제는 프로시저가 복귀하기 전에 한다는 점이 __cdcel과 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
39:void WINAPI Input (int & m,int & n)
 
40:{
00401110 push ebp 
00401111 mov ebp, esp 
00401113 sub esp, 48h 
00401116 push ebx 
00401117 push esi 
00401118 push edi 
00401119 lea edi,[ebp-48h]
0040111C mov ecx, 12h 
00401121 mov eax, 0CCCCCCCCh 
00401126 rep stos dword ptr [edi]
 
41:int s, i;
 
42:
 
43:while(1)
00401128 mov eax, 1 
0040112D test eax, eax 
0040112F je Input +0 C1h (004011d1)
 
44:{
 
45printf ("\ nPlease input the first number m:");
00401135 push offset string "\ nPlease input the first number m"...(004260b8)
0040113A call printf (00401530)
0040113F add esp, 4 
 
46scanf ("% d"& m);
00401142 mov ecx, dword ptr [ebp +8]
00401145 push ecx 
00401146 push offset string "% d"(004260b4)
0040114B call scanf (004015f0)
00401150 add esp, 8 
 
47:
 
48:if(m = s)
004011B3 mov eax, dword ptr [ebp +8]
004011B6 mov ecx, dword ptr [eax]
004011B8 cmp ecx, dword ptr [ebp-4]
004011BB jl Input +0 AFh (004011bf)
 
57:break;
004011BD jmp Input +0 C1h (004011d1)
 
58:else
 
59printf ("m <n * (n +1) / 2, Please input again! \ N");
004011BF push offset string "m <n * (n +1) / 2, Please input agai"...(00426060)
004011C4 call printf (00401530)
004011C9 add esp, 4 
 
60:}
004011CC jmp Input +18 h (00401128)
 
61:
 
62:}
004011D1 pop edi 
004011D2 pop esi 
004011D3 pop ebx 
004011D4 add esp, 48h 
004011D7 cmp ebp, esp 
004011D9 call __ chkesp (004015b0)
004011DE mov esp, ebp 
004011E0 pop ebp 
004011E1 ret 8 
cs


32비트 기반의 C++에서 8개의 주소를 리턴(RET 8)함으로써 스택을 복원합니다(int 2개, 8바이트).


이것의 의미는, C++ 함수의 인자인 변수가 몇 번 pop 되는지 알 수 없으므로, push를 담당하는 main 호출 함수는 스택 포인터를 복원해야 할 필요가 있습니다.


C++ 고급 강의 어셈블리 함수 호출[Function Call] 프로그래밍 기본 이론


ebp-8과 ebp-4를 살펴보면, ebp-8의 주소를 저장하는 n의 값은 m의 값을 보유한 ebp-4입니다.


요약 : main 호출 함수는 스택의 push, pop-up에 대해서 복원할 책임이 있습니다. 함수 인자의 변수는 오른쪽에서 왼쪽으로 이뤄지며, 함수명 뒤에 원하는 바이트(10진수)를 @뒤에 붙여 함수의 기능을 수정할 수 있습니다.


1
2
3
4
5
6
7
8
void Input (int & m,int & n), is modified into: _Input @ 8.
 
Such as:
push edx 
push edi 
push eax 
push ebx 
call getdlgitemtexta
cs

마지막으로 또 하나의 예를 보면,


1
2
3
4
5
6
7
8
9
int sum(int a,int b )
{
    return a + b;
}
 
void main()
{
    sum( 12 );
}
cs


위의 코드를 각 __cecel과 __stdcall에서의 어셈블리 코드로 보면,


__cecel


1
2
3
4
5
6
7
8
9
10
11
12
13
main:
 push 2 ; 오른쪽 파라미터 2부터 전달되고
 push 1 ; 그 후에 1이 전달
 call sum
 add esp, 8 ; 프로시저 복귀 후 호출한 쪽에서 스택포인터 복귀
 
sum:
 push ebp
 mov ebp, esp ; ebp에 esp를 저장하는 이유가 중요한데 이 부분은 나중에 따로...
 mov eax, dword ptr [ebp + 0x8]
 add eax, dwrod ptr [ebp +
 pop ebp
 ret
cs


__stdcall


1
2
3
4
5
6
7
8
9
10
11
12
main:
 push 2 ; 역시 오른쪽 파라미터 2부터 전달되고
 push 1 ; 그 후에 1이 전달
 call sum
 
sum:
 push ebp
 mov ebp, esp
 mov eax, dword ptr [ebp + 0x8]
 add eax, dwrod ptr [ebp +
 pop ebp
 ret 8 ; 이 부분에서 프로시저가 복귀되기 전에 스택포인터 복귀
cs


__cecel과 __stdcall의 차이가 보입니다.


__cdcel은 스택 해제 코드가 프로시저를 호출한 쪽에서 생기기 때문에 프로시저를 많이 호출(C++ 함수 호출)하게 되면 그만큼 해제 코드가 생기기 때문에 코드의 크기가 늘어나게 되지만, __stdcall은 프로시저가 많이 호출되도 프로시저 내에 있는 해제 코드 하나로 해결이 되기 때문에 상대적으로 코드의 크기가 작습니다.


출처 - __stdcall表示什莫意思 [问题点数:10分,结帖人lyuzw]

C++ 함수 호출 원리, 어셈블리 위주 설명 (고급 강의)

이 글을 공유하기

댓글(0)

Designed by JB FACTORY