函数调用的底层机制

这是一篇介绍c语言中的函数调用是如何用实现的文章。写给那些对c语言各种行为的底层实现感兴趣人的入门级文章。如果你是c语言或者汇编、底层技术的老鸟或是对这个问题不感兴趣,那么这篇文章只会耽误您的时间,您大可不必阅读他。当然如果前辈们愿意为我指出不足,我将十分感谢您的指导,并对耽误您宝贵的时间致歉。好了,废话少说!要研究这个问题,让我们先打开vc++吧。最好是6.0的,:-p。(什么你没有vc++,倒!....赶快装一个!@#$,要快!) 首先,让我们在vc++里建立一个win32 console application项目,并建立主文件fun.c。并输入以下内容。

int fun(int a, int b) {
a = 0x4455;
b = 0x6677;
return a + b;
}
int main() {
fun(0x8899,0x1100);
return 0;
}
之后,最关键的是在项目设置里关闭优化功能。也就是把project->setting->c/c++->optimizations选为disabled。编译器的优化在分析底层实现时大多数情况不太受欢迎。按键盘上的f10键,进入单步调试模式(step over)。看到你的main函数左侧有个黄色的小箭头了吗?那个就是程序即将执行的语句。按alt + 8。打开反编译窗口,看到汇编语句了吗?是不是想这个样子
==> 00401078   push        1100h
0040107d push 8899h
00401082 call @ilt+5(fun) (0040100a)
00401087 add esp,8
看到两个push指令了吗?再看看后面的数字,不正是我们要传递的参数吗。奇怪阿?我们明明是先传递的0x8899怎么反倒先push 1100h呢?呵呵,这个现象就叫calling conversion。究竟是何方神圣,我在后面会详细的给你解释的。先别着急。随后的call指令的作用就是开始调用函数了。接下来关掉反汇编窗口,在源代码窗口按f11(step into)进入函数体。当看到那个黄色的小箭头指向函数名的时候再调出反汇编窗口(alt+8)。你会看到类似下面的代码:
1:    int fun(int a, int b) {
00401000 push ebp
00401001 mov ebp,esp

00401003 sub esp,40h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-40h]
0040100c mov ecx,10h
00401011 mov eax,0cccccccch
00401016 rep stos dword ptr [edi]
2: a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101f mov dword ptr [ebp+0ch],6677h
4: return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0ch]

5: }
0040102c pop edi
0040102d pop esi
0040102e pop ebx
0040102f mov esp,ebp
00401031 pop ebp
00401032 ret

vc++就是好,还在难懂的汇编语句前加入了c语言的源代码。不过同时也有不少我们不需要的代码。因此,你只需要关心红色的部分就可以了。奇怪阿?不是参数都用push传递了吗?怎么没看到被pop出来?问题其实是这样,当你调用call进入函数的时候call背着你做了一件事。call把它下一条语句的地址push进了堆栈。(旁人: 什么!这是为什么?)原因很简单,因为函数调用完了,要用ret返回。而ret怎么知道返回哪里呢?对了, ret指令pop了call指令push给他的地址(搞清楚这个关系哦),然后返回到了这个地址。call和ret配合的如此绝妙,一个push一个pop肯定不会让堆栈不平衡的(老外叫no stack unwinding)。现在明白了,如果你来个pop eax,那eax里面是什么?当然是ret要用的返回地址了。好啦,你要是pop eax就等于抢了ret要用的东西了。不论曾程序流程和道德标准上你做的都不对 :-p。可是怎么在函数体里使用参数呢?问题其实并不难,既然参数在堆栈里我们就可以使用esp(堆栈指针)来访问了。不过,我相信你也想到了。esp是个经常变化的值。一旦,函数里出现pop或push他就会变化。这样很不容易定位参数的于内存中的位置。因此,我们需要一个不会变化的东西作为访问参数的基准。看看函数体的开头部分:
00401000   push        ebp
00401001 mov ebp,esp
先用push ebp保存了原来ebp的值再把esp的值给ebp。原来ebp就是用来做基准的。也难怪他被称为ebp(base pointer)。很自然ret返回前的pop ebp就是恢复原来ebp的数值喽。当然一定要恢复,因为函数里也可以调用函数嘛。每个函数都用ebp,自然要保证使用完后完璧归赵了。现在当函数执行到 mov ebp, esp后堆栈应该变成这个样子了。
/-------------------/  higher address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
+-----------------+
| 函数返回地址 |
| 0x00401087 |
+-----------------+
| ebp |
/-------------------/ lower address <== stack pointer
& ebp all point to here, now
由于我们在vc++上使用的int类型是一个32位类型,ebp和函数返回值也是32位的。因此每个量要占去4个字节。另外还需要注意堆栈的扩展方向是高地址到低地址。有了这些指示。我们就可以分析出,第一个参数的地址是ebp + 08h,第二个参数就是ebp + 0ch。看看反汇编的代码:
2:       a = 0x4455;
00401018 mov dword ptr [ebp+8],4455h
3: b = 0x6677;
0040101f mov dword ptr [ebp+0ch],6677h
与我们的计算吻合。之后呢:
00401031   pop         ebp
00401032 ret
将ebp原来的数值完璧归赵,调用ret指令,ret指令pop出返回地址,之后返回到调用函数的call指令的下一条语句。ret之后,堆栈应该变成这个样子了
/-------------------/  higher address
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
/-------------------/ lower address <== stack pointer
哈哈,问题出现了,再函数返回后堆栈出现了不平衡的情况(stack unwinding)。怎么办呢?好办啊,直接 pop cx pop cx 把堆栈平衡过来就好了。幸好我们只有两个参数,要是有20个的话,那就要有20个pop cx。不说影响美观,程序效率也会很低。所以vc++使用了这个办法解决问题:
00401082   call        @ilt+5(fun) (0040100a)
00401087 add esp,8
看红色的语句,直接将esp的值加8,让堆栈变成
/-------------------/  higher address <== stack pointer
| 参数2: 0x1100h |
+-----------------+
| 参数1: 0x8899h |
/-------------------/ lower address
通过改变esp从根本上解决了stack unwinding。(push,pop指令本质上不就是通过改变esp来实现堆栈平衡的吗) 现在,明白了函数如何传递参数,如何调用,如何返回。下一个问题就是看看函数如何传递返回值了。相信你早就注意到了
4:       return a + b;
00401026 mov eax,dword ptr [ebp+8]
00401029 add eax,dword ptr [ebp+0ch]
可见,函数正式用eax寄存器来保存返回值的。如果你想使用函数的返回值,那么一定要在函数一返回就把eax寄存器的值读出来。至于为什么不用ebx,ecx...,这个虽然没有规定,但是习惯上大家都是用eax的。而且windows程序中也明确指出了,函数的返回值必须放入eax内。 ok,现在来解决什么是calling conversion这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡呢?所有上述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值