C语言函数调用的底层机制

上传人:206****923 文档编号:37217593 上传时间:2018-04-09 格式:DOC 页数:6 大小:40KB
返回 下载 相关 举报
C语言函数调用的底层机制_第1页
第1页 / 共6页
C语言函数调用的底层机制_第2页
第2页 / 共6页
C语言函数调用的底层机制_第3页
第3页 / 共6页
C语言函数调用的底层机制_第4页
第4页 / 共6页
C语言函数调用的底层机制_第5页
第5页 / 共6页
点击查看更多>>
资源描述

《C语言函数调用的底层机制》由会员分享,可在线阅读,更多相关《C语言函数调用的底层机制(6页珍藏版)》请在金锄头文库上搜索。

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

2、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 键,进入单步调试模式(StepOver)。看到你的 main 函数左侧有个黄色的小箭头了吗?那个就是程序即将执 行的语句。按 Alt +8。打开反编译窗口,看到汇编语句了吗?是不

3、是想这个样子 = 00401078 push 1100h0040107D push 8899h00401082 call ILT+5(fun) (0040100a)00401087 add esp,8 看 到两个 PUSH 指令了吗?再看看后面的数字,不正是我们要传递的参数吗。奇怪 阿?我们明明是先传递的 0x8899 怎么反倒先 push1100h 呢?呵呵,这个现象就叫 Callingconversion。究竟是何方神圣,我在后面会详细的给你解释的。先别着急。随 后的 Call 指令的作用就是开始调用函数了。接下来关掉反汇编窗口,在源代码窗口按 F11(StepInto)进入函数体。当看到

4、那个黄色的小箭头指向函数名的时候再调出反汇编窗 口(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 = 0x

5、4455; 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+0Ch5: 0040102C pop edi 0040102D pop esi 0040102E pop ebx 0040102F mov esp,ebp 00401031 pop ebp 00401032 ret VC+就是好,还在难懂的汇编语句前加

6、入了 C 语言的源代码。不过同时也有不 少我们不需要的代码。因此,你只需要关心红色的部分就可以了。 奇怪阿?不是参数都用 push 传递了吗?怎么没看到被 pop 出来?问题其实是这 样,当你调用 Call 进入函数的时候 Call 背着你做了一件事。call 把 它下一条语句的地址 push 进了堆栈。(旁人: 什么!这是为什么?)原因很简单,因为函数调用完了,要用 ret 返回。而 ret 怎么知道返回哪里呢?对了,ret 指令 pop 了 call 指令 push 给他的地址(搞清楚这个关系哦),然后返回到 了这个地址。call 和 ret 配合的如此绝妙,一个 PUSH 一个POP 肯

7、定不会让堆栈不平衡的(老外叫 no stack unwinding)。现在明白了,如 果你来个 popeax,那 eax 里面是什么?当然是 ret 要用的返回地址了。好啦,你要是 popeax 就等于抢了 ret 要用的东西了。不论曾程序流程和道德标准上你做的都不 对 :-P。 可是怎么在函数体里使用参数呢?问题其实并不难,既然参数在堆栈里我们就 可以使用 esp(堆栈指针)来访问了。不过,我相信你也想到了。esp 是个经常变 化的值。一旦,函数里出现 pop 或 push 他就会变化。这样很不容易定位参数的 于内存中的位置。因此,我们需要一个不会变化的东西作为访问参数的基准。 看 看函数体

8、的开头部分: 00401000 push ebp 00401001 mov ebp,esp 先 用 push ebp 保存了原来 ebp 的值再把 esp 的值给 ebp。原来 ebp 就是用来做基 准的。也难怪他被称为 ebp(BasePointer)。很自然 ret 返回前的 popebp 就是恢复原来 ebp 的数值喽。当然一定要恢复,因为函数里也可以调用函 数嘛。每个函数都用 ebp,自然要保证使用完后完璧归赵了。现在当函数执行到mov ebp, esp 后堆栈应该变成这个样子了。 /- Higher Address| 参数 2: 0x1100h | +-+| 参数 1: 0x8899

9、h |+-+| 函数返回地址 | | 0x00401087 |+-+| ebp | -/ Lower Address = stack pointer 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 之后,堆栈应该变成这个样子 了 /- Hi

10、gher Address| 参数 2: 0x1100h | +-+| 参数 1: 0x8899h | -/ Lower Address = stack pointer 哈 哈,问题出现了,再函数返回后堆栈出现了不平衡的情况(Stack Unwinding)。 怎么办呢?好办啊,直接 pop cx pop cx 把堆栈平衡过来就好了。幸好我们只有两个参数,要是有 20 个的话,那就要有 20 个 popcx。不说影响美观,程序效率也会很低。所以 VC+使用了这个办法解决问题: 00401082 call ILT+5(fun) (0040100a) 00401087 add esp,8 看红色的语

11、句,直接将 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 a

12、dd eax,dword ptr ebp+0Ch 可 见,函数正式用 eax 寄存器来保存返回值的。如果你想使用函数的返回值,那 么一定要在函数一返回就把 eax 寄存器的值读出来。至于为什么不用 ebx,ecx.,这个虽然没有规定,但是习惯上大家都是用 eax 的。而且 windows 程 序中也明确指出了,函数的返回值必须放入 eax 内。OK,现在来解决什么是 callingconversion 这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么 偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不 一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一

13、定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡 呢? 所有上述提议都是绝对可行的,而他们之间不同的组合就造就了函数不同的调 用方法。也就是你常看到或听到的 stdcall,pascal,fastcall,WINAPI,cdecl 等等。这些不同的处理函数调用方式就叫做 calling convention。 默认情况下 C 语言使用的是 cdecl 方式,也就是上面提到的。参数由右到左进 栈,调用函数者处理堆栈平衡。如果你在我们刚才的程序中 fun 函数前加入_stdcall,再来用上面的方法分析一下。 8: fun(0x8899,0x1100); 00401058 p

14、ush 1100h ; = 参数仍然是由右到左传递的 0040105D push 8899h 00401062 call fun (00401000) ;= 这里没有了 add esp, 08h1: int _stdcall 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 0

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号