C语言的函数调用过程

ahua0 2018-11-01

先上一段代码

#include<stdio.h>

int Add(int x, int y)

{

int z = 0;

z = x + y;

return z;

}

#include <stdio.h>

int main()

{

int a = 10;

int b = 20;

int c = Add(a, b);

return 0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

这个函数计算两个数的值。接下来我们通过反汇编,来看看函数被后的过程。

反汇编代码

#include <stdio.h>

int main()

{

01151720 push ebp

01151721 mov ebp,esp

01151723 sub esp,0E4h

01151729 push ebx

0115172A push esi

0115172B push edi

0115172C lea edi,[ebp-0E4h]

01151732 mov ecx,39h

01151737 mov eax,0CCCCCCCCh

0115173C rep stos dword ptr es:[edi]

int a = 10;

0115173E mov dword ptr [a],0Ah

int b = 20;

01151745 mov dword ptr [b],14h

int c = Add(a, b);

0115174C mov eax,dword ptr [b]

0115174F push eax

01151750 mov ecx,dword ptr [a]

01151753 push ecx

01151754 call _Add (01151104h)

01151759 add esp,8

0115175C mov dword ptr [c],eax

return 0;

0115175F xor eax,eax

}

01151761 pop edi

01151762 pop esi

01151763 pop ebx

01151764 add esp,0E4h

0115176A cmp ebp,esp

0115176C call __RTC_CheckEsp (01151122h)

01151771 mov esp,ebp

01151773 pop ebp

}

//Add()

#include<stdio.h>

int Add(int x, int y)

{

001016D0 push ebp

001016D1 mov ebp,esp

001016D3 sub esp,0CCh

001016D9 push ebx

001016DA push esi

001016DB push edi

001016DC lea edi,[ebp-0CCh]

001016E2 mov ecx,33h

001016E7 mov eax,0CCCCCCCCh

001016EC rep stos dword ptr es:[edi]

int z = 0;

001016EE mov dword ptr [z],0

z = x + y;

001016F5 mov eax,dword ptr [x]

001016F8 add eax,dword ptr [y]

001016FB mov dword ptr [z],eax

return z;

001016FE mov eax,dword ptr [z]

}

00101701 pop edi

00101702 pop esi

00101703 pop ebx

00101704 mov esp,ebp

00101706 pop ebp

00101707 ret

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

以上就是反汇编代码。现在我们一段一段的进行解读,去看看整个程序是怎么执行的。

main()函数的创建

01151720 push ebp

01151721 mov ebp,esp

01151723 sub esp,0E4h

01151729 push ebx

0115172A push esi

0115172B push edi

0115172C lea edi,[ebp-0E4h]

01151732 mov ecx,39h

01151737 mov eax,0CCCCCCCCh

0115173C rep stos dword ptr es:[edi]

//分割线.........................

int a = 10;

0115173E mov dword ptr [a],0Ah

int b = 20;

01151745 mov dword ptr [b],14h

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

这里要给读者朋友们简单说一下变量的含义。ebp 和 esp 是两个通用寄存器。它们可以存储 立即数 和 内存地址。esi是源变址寄存器。edi是目的变址寄存器。

它们都可用来储存地址。

在main()函数之前有一个函数CRTStartUp,ebp为这个函数的栈底,esp为这个函数的栈顶。

接下来通过前两句代码mov 赋值移动,它们的状态变成了如下:

第三句话中,sub是减法指令,这句话使得esp向后移动了0E4h个单位。

接下来的三句话,是将ebx,esi,edi压入了栈当中。

第七句话,lea 的意思是load effective address,也就是将[ebp - 0E4h]的地址取出来,赋值给了edi。

第八句话,将39h赋值移动到ecx。第九句话,将内容赋值移动给eax。

第十句话,rep stos 是重复拷贝数据。

注意:

ecx寄存器是用来保存复制粘贴的次数,ecx是计数器。

eax寄存器是用来传送信息的,eax是累加器,通常用来存储数据。

rep 是重复上述指令。

stos 是将eax的值赋值移动给 es:[edi]这个地址的存储单元。

现在其中的空白部分被cc cc cc cc充满了。这里cc cc就是经常遇到的“烫烫”。

分割线

接下来的操作很好理解,其目的是将 a和b创建,并且压入栈中。

Add()函数的调用过程

int c = Add(a, b);

0115174C mov eax,dword ptr [b]

0115174F push eax

01151750 mov ecx,dword ptr [a]

01151753 push ecx

01151754 call _Add (01151104h)

01151759 add esp,8

0115175C mov dword ptr [c],eax

1

2

3

4

5

6

7

8

前四句话,它将a和b找了个寄存器存起来,与此同时,以函数参数列表倒着的方式压入栈中。

方便起见,下面的图将只截取上半部分。

接下来使用了call指令,这个指令是先将 指令 移动到下一句话,然后再操纵call 后面的内容。在call处按F11我们将跳转到Add()函数。

Add()函数

#include<stdio.h>

int Add(int x, int y)

{

001016D0 push ebp

001016D1 mov ebp,esp

001016D3 sub esp,0CCh

001016D9 push ebx

001016DA push esi

001016DB push edi

001016DC lea edi,[ebp-0CCh]

001016E2 mov ecx,33h

001016E7 mov eax,0CCCCCCCCh

001016EC rep stos dword ptr es:[edi]

1

2

3

4

5

6

7

8

9

10

11

12

13

14

这一部分和main函数创建过程非常相似。所以用俩图来表示。

int z = 0;

001016EE mov dword ptr [z],0

z = x + y;

001016F5 mov eax,dword ptr [x]

001016F8 add eax,dword ptr [y]

001016FB mov dword ptr [z],eax

return z;

001016FE mov eax,dword ptr [z]

}

00101701 pop edi

00101702 pop esi

00101703 pop ebx

00101704 mov esp,ebp

00101706 pop ebp

00101707 ret

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

这段代码非常有意思,请读者朋友们一定要注意。

首先,我们创建了变量 ptr[z]。然后,将ptr[x]的值放在eax里边,将ptr[y]的值与eax相加后再放在eax里,将eax的值赋值移动到 ptr[z]中。最后,我们需要return z。这个地方编译器再次将ptr[z]的值赋值移动到了eax。这么做的原因是:ptr[x]、ptr[y]、ptr[z]将在函数完成后就会销毁,但是寄存器eax不会被销毁,所以在eax的值非常安全。

括号外的第四句话非常重要。它将销毁该函数的所有数据。

将ebp弹出后,该点只有一个指针esp。接下来执行ret,即 return 返回。返回到我们刚才call下面的地方。

main()函数的销毁

01151759 add esp,8

0115175C mov dword ptr [c],eax

return 0;

0115175F xor eax,eax

}

01151761 pop edi

01151762 pop esi

01151763 pop ebx

01151764 add esp,0E4h

0115176A cmp ebp,esp

0115176C call __RTC_CheckEsp (01151122h)

01151771 mov esp,ebp

01151773 pop ebp

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

现在我们跳回到了该段的第一句话,现在esp + 8向高地址进发了。其目的就是消去了形参实例化的a,b。第二句话,我们将eax里保存的数据赋值移动给了c。最后,xor异或,自己和自己异或 其值变为0。

之后,我们出栈了 edi,esi,ebx。现在esp指向了最开始我们红色那个箭头 ebp - 0E4h。

然后,由于add操作,我们继续向高地址进发,到了我们最开始的地方。

C语言的函数调用过程

C语言的函数调用过程

现在,我们到了cmp指令,cmp是比较指令。它的操作是拿第一个数减去第二个数,如果相减为ZF=0说明相等。但是cmp并不会真的减。到此位置main()函数就真的执行完毕,然后esp,ebp。回到了原来的位置。至于后面的函数调用,并不需要深究。至此我们就看到了在汇编上函数如何实现的

相关推荐