打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
函数是如何被调用的?

C/C++语言中,函数是如何被调用的呢?本文就实际的例子,走进汇编代码来看下函数调用的过程。

首先看一个简单的代码例子:

void test(int i)

{

    int j = i;

}

 

void test1()

{

 

}

 

int test2()

{

    return 1;

}

 

void test3(int a,int b,int c)

{

}

 

void test4()

{

    int i,j;

}

 

void test5()

{

    int i,j,k,l;

}

 

int main()

{  

    int i =0;

    test1();

   

    test(10);

   

    test3(1,2,3);

 

    i=test2();

   

    test4();

   

    test5();

 

    return 0;

}

 

这段代码很简单,mian函数调用几个被测试的函数,分别是:

1.  没有参数

2.  有一个参数

3.  3个参数

4.  有返回值

5.  有两个临时变量

6.  有多个临时变量

 

VC7中,我们将断点设置到main函数入口的地方;然后F5运行程序。再按ALT+8反汇编,我们看到下面的代码:

Main函数变成这样了:

int main()

{  

00401120  push        ebp 

00401121  mov         ebp,esp

00401123  sub         esp,0CCh

00401129  push        ebx 

0040112A  push        esi 

0040112B  push        edi 

0040112C  lea         edi,[ebp-0CCh]

00401132  mov         ecx,33h

00401137  mov         eax,0CCCCCCCCh

0040113C  rep stos    dword ptr [edi]

    int i =0;

0040113E  mov         dword ptr [i],0 //直接将数据0放到指定地址中

    test1();

00401145  call        test1 (401030h)

   

    test(10);

0040114A  push        0Ah 

0040114C  call        test (401000h)

00401151  add         esp,4

   

    test3(1,2,3);

00401154  push        3   

00401156  push        2   

00401158  push        1   

0040115A  call        test3 (401090h)

0040115F  add         esp,0Ch

 

    i=test2();

00401162  call        test2 (401060h)

00401167  mov         dword ptr [i],eax

 

    test4();

0040116A  call        test4 (4010C0h)

   

    test5();

0040116F  call        test5 (4010F0h)

 

    return 0;

00401174  xor         eax,eax

}

00401176  pop         edi 

00401177  pop         esi 

00401178  pop         ebx 

00401179  add         esp,0CCh

0040117F  cmp         ebp,esp

00401181  call        _RTC_CheckEsp (4011E0h)

00401186  mov         esp,ebp

00401188  pop         ebp 

00401189  ret             

 

函数入口部分:

00401120  push        ebp  //保存ebp的值

00401121  mov         ebp,esp //将当前栈顶指针送到ebp

00401123  sub         esp,0CCh //将栈顶指针下移0XCC个字节,为临时变量留出空间

00401129  push        ebx  //保存ebx

0040112A  push        esi  //保存esi

0040112B  push        edi  //保存edi

0040112C  lea         edi,[ebp-0CCh] //edp-0CC地址送EAX

00401132  mov         ecx,33h //CC/4得到的

00401137  mov         eax,0CCCCCCCCh //初始化为0XCCCCCCCCH

0040113C  rep stos    dword ptr [edi]//复制

这写汇编是编译器为我们生成的函数入口部分,基本的含义是为临时变量分配空间,并且初始化临时变量。

这里需要说明几点:

1.  函数调用是通过堆栈来完成的。

2.  函数入口的地方必须为临时变量分配一定空间;实际上如果没有临时变量,也要留出C0个字节。

3.  堆栈栈顶指针随数据的进入逐渐减小。因此sub esp0CCh实际上是留出了CC个自己的堆栈空间。

我们看到实现将栈顶指针保存在ebp中,然后对该段空间设置初始值。而0XCCCCCCH是由堆栈的性质决定,可以看MSDN

如果开始的时候假设ESP等于0X12FEE0,那么在保存EBP之后,ESP变成0X12FEDC,那么后来EBP中的值就是这个值,在保存的空间(从0X12FE100X12FEDC)上将所有的内存都初始化为0XCC。而i被分配在0X12FED4处,也就是第一个预留的位置)。

 

 

call        test1 (401030h)

由于已经知道i的地址了,对i的赋值就很简单了。这里看调用第一个没有参数没有返回值的test1函数;仅仅一条语句,将test1的函数地址给call指令。

EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000 EDX = 00000001

ESI = 00000040 EDI = 0012FEDC EIP = 00401145 ESP = 0012FE04

EBP = 0012FEDC EFL = 00000202

上面是Call指令调用前各寄存器的值;下面是调用后的值:

EAX = CCCCCCCC EBX = 7FFD7000 ECX = 00000000 EDX = 00000001

ESI = 00000040 EDI = 0012FEDC EIP = 00401030 ESP = 0012FE00

EBP = 0012FEDC EFL = 00000202

主要变化在于EIPESP;前者是指令指针寄存器,而后者是堆栈指针寄存器。调用前指令的位置在00401145位置,而call指定将EIP改为test1的地址;同时将返回地址入栈;可以看到当前栈顶的值是0040114A,实际上是test1的下条指令。

因此我们说Call指定做了两件事情:

1.  EIP从当前值改为被调用函数的值。

2.  将返回地址,也就是当前地址的下条指令放入堆栈。

 

现在进入test1中看个究竟。

void test1()

{

00401030  push        ebp 

00401031  mov         ebp,esp

00401033  sub         esp,0C0h

00401039  push        ebx 

0040103A  push        esi 

0040103B  push        edi 

0040103C  lea         edi,[ebp-0C0h]

00401042  mov         ecx,30h

00401047  mov         eax,0CCCCCCCCh

0040104C  rep stos    dword ptr [edi]

 

}

0040104E  pop         edi 

0040104F  pop         esi 

00401050  pop         ebx 

00401051  mov         esp,ebp

00401053  pop         ebp 

00401054  ret            

上面的命令基本相同,主要区别在于test1内部没有临时变量,因此这里只保留了C0个自己的空间。

 

继续回到主程序:

    test(10);

0040114A  push        0Ah 

0040114C  call        test (401000h)

00401151  add         esp,4

由于test函数有一个参数,因此需要首先将参数压入堆栈中,然后执行与前面相似的操作。

这里有一点需要注意:函数返回之后需要将压入的参数弹出;可以使用pop命令,也可以使用add命令来执行。

 

对于test3的调用:

    test3(1,2,3);

00401154  push        3   

00401156  push        2   

00401158  push        1   

0040115A  call        test3 (401090h)

0040115F  add         esp,0Ch

 

由于它需要三个参数,因此都必须压入栈,返回的时候一次性弹出。

 

下面看如何调用带有返回值的参数:

    i=test2();

00401162  call        test2 (401060h)

00401167  mov         dword ptr [i],eax

其他的相同,但重要的一点是函数的返回值是通过eax寄存器来返回的。

 

其他几个函数的调用不同的是临时变量数目的不同,仅仅在初始化预留空间的时候不同,基本上是每增加一个变量多出12个字节的堆栈空间。

 

mian函数的返回值,有点特别:

    return 0;

00401174  xor         eax,eax

特别的不在于通过eax返回,而是自己和自己异或,大部分返回0的函数都这么做。

 

mian函数退出的时候有这段代码:

00401176  pop         edi 

00401177  pop         esi 

00401178  pop         ebx 

00401179  add         esp,0CCh

0040117F  cmp         ebp,esp

00401181  call        _RTC_CheckEsp (4011E0h)

00401186  mov         esp,ebp

00401188  pop         ebp 

00401189  ret             

前面几行是将寄存器的值恢复,而add esp0CCh是将保留的堆栈空间释放,同时比较ebp是否与esp相等,如果不相等就提示相应的错误,说明有内存泄露等。最后将ebp弹出然后返回。

 

从上面的分析我们可以看到编译器为我们做了很多事情,包括:堆栈空间分配和释放、寄存器状态保存、参数传递等。当然这些事情也可以完全由我们自己来完成,那么需要做的是使用关键字naked来声明函数。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
【代码真相】函数调用 堆栈 转载 - liangxiufei - 博客园
从汇编源码逐步分析函数调用过程
Ted's Blog
一个简单的C++程序反汇编解析
DLL中调用约定和名称修饰(一)
函数调用方式介绍
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服