Dict.CN 在线词典, 英语学习, 在线翻译

学海苦作舟,书山勤为径

留下点回忆

常用链接

统计

积分与排名

Denoise

English study

Web技术

数据压缩

一些连接

最新评论

函数是如何被调用的?-探索代码背后的故事

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 esp 0CCh 实际上是留出了 CC 个自己的堆栈空间。

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

如果开始的时候假设 ESP 等于 0X12FEE0 ,那么在保存 EBP 之后, ESP 变成 0X12FEDC ,那么后来 EBP 中的值就是这个值,在保存的空间(从 0X12FE10 0X12FEDC )上将所有的内存都初始化为 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

主要变化在于 EIP ESP ;前者是指令指针寄存器,而后者是堆栈指针寄存器。调用前指令的位置在 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 esp 0CCh 是将保留的堆栈空间释放,同时比较 ebp 是否与 esp 相等,如果不相等就提示相应的错误,说明有内存泄露等。最后将 ebp 弹出然后返回。

 

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

posted on 2007-01-18 15:08 笨笨 阅读(2481) 评论(3)  编辑 收藏 引用 所属分类: 编码

评论

# re: 函数是如何被调用的?-探索代码背后的故事 2007-01-18 15:09 笨笨

终于又找回密码了,痛恨木马编写的人,痛恨病毒!同时感谢论坛斑竹的热心帮助!  回复  更多评论   

# re: 函数是如何被调用的?-探索代码背后的故事 2007-01-27 01:04 SonicLing

很多时候并不一定会用ebp来备份esp。包含ret的分支很少的函数用release编译之后,直接会在ret之前将esp加回到原来的值,ebp用来干其他的事。

xor eax,eax是因为该指令比mov eax,0无论是在指令长度还是在执行效率上都更优秀。  回复  更多评论   

# re: 函数是如何被调用的?-探索代码背后的故事 2007-01-29 08:45 笨笨

你说的很有道理,这里仅仅是将一段代码再VC中反汇编的到的。当然,这里的代码并非唯一的写法。
所以,谢谢你的补充
  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理