当前位置:首页 >> 信息与通信 >>

[转载]C语言函数调用的底层机制


这是一篇介绍 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 怎么反倒先 push1100h 呢?呵呵, 这个现象就 叫 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 0040100C mov 00401011 mov 00401016 rep stos 2: a = 0x4455; 00401018 mov 3: b = 0x6677; 0040101F mov 4: return a + b; 00401026 mov 00401029 add

edi,[ebp-40h] ecx,10h eax,0CCCCCCCCh dword ptr [edi] dword ptr [ebp+8],4455h dword ptr [ebp+0Ch],6677h eax,dword ptr [ebp+8] 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 这个历史遗留问题。如果认真思考 过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且 很快阿。参数的传递顺序不 一定要是由后到前的, 从前到后传递也不会出现任何问题啊?再有为什么一定要 等到函数返回了再处理堆栈平衡的问题呢, 能否在函数返回前就让堆栈平衡呢? 所有上述提议都是绝对可行的, 而他们之间不同的组合就造就了函数不同的调用 方法。也就是你常看到或听到的 stdcall,pascal, fastcall,WINAPI,cdecl 等等。这些不同的处理函数调用方式就叫做 calling convention。 默认情况下 C 语言使用的是 cdecl 方式, 也就是上面提到的。 参数由右到左进栈, 调用函数者处理堆栈平衡。如果你在我们刚才的程序中 fun 函数前加入 __stdcall,再来用上面的方法分析一下。 8: fun(0x8899,0x1100); 00401058 push 1100h ; <== 参数仍然是由右到左传递的 0040105D push 8899h 00401062 call fun (00401000) ;<== 这里没有了 add esp, 08h 1: 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 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 5: } 0040102C 0040102D 0040102E 0040102F 00401031 00401032

add pop pop pop mov pop ret

eax,dword ptr [ebp+0Ch] edi esi ebx esp,ebp ebp 8; <== ret 取出返回地址后, ; 给 esp 加上 8。看!堆栈平衡在函数内完成了。 ; ret 指令这个语法设计就是专门用来实现函数 ; 内完成堆栈平衡的

于 是得出结论,stdcall 是由右到左传递参数,被调用函数恢复堆栈的 calling convention. 其他几种 calling convention 的修饰关键词分别是__pascal,__fastcall, WINAPI(这个要包含 windows.h 才可以用)。现在,你可以用上面说的方法自己分 析一下他们各自的特点了。



相关文章:
C语言函数的定义和调用
C语言函数的定义和调用 - C 语言函数的定义和调用 点击: 发布日期:2007-4-19 14:15:00 进入论坛 本节介绍 C 程序的基本单元--函数。 函数中包含了程序的...
汇编语言过程调用与C语言函数调用的异同
汇编过程调用以堆栈结构为核心包括定 义、调用机制应用、变量访问等关键行为。在 C 语言中则包括定义、 参数定义、数据传递等。 关键词:函数、过程、数据传递、...
c语言函数调用详细过程
(gdb) disassemble main ---> 反汇编 main 函数 Dump of assembler code for...C语言函数调用的底层机制... 6页 免费 浅析c语言的函数调用 13页 免费 C...
C语言函数调用规定
stdcall 调用约定声明的语法为(以前文的那个函数为例): 调用约定声明的语法为(...C语言函数调用的底层机制... 6页 免费 C语言函数调用规定 56页 免费 C...
C语言函数的定义与使用
讲义十三 C 语言函数的定义与使用 一、函数的定义: 1、函数的定义: 返回类型 函数名(参数列表){ 函数体; } 如: 1)带返回值的定义 int sum1(int m){ ...
浅析c语言的函数调用
浅析c语言的函数调用 - 浅析 c 语言的函数调用 C 语言中的函数调用形式差别在形参的不同或是返回类型的不一 样,下面来谈一下这些函数。 第一种也是最简单的...
通过C语言调用函数的方式来实现线性表的基本操作
通过C语言调用函数的方式来实现线性表的基本操作 - (一)请通过C语言调用函数的方式来实现线性表的基本操作: 1. 创建、初始化 线性表 2. 事先给定e, 返回其...
C语言函数调用三种方式 传值调用,引用调用和传地址调
C语言函数调用三种方式 传值调用,引用调用和传地址调_数学_自然科学_专业资料。...C语言函数调用的底层机制... 6页 免费 C语言函数调用 23页 1下载券 C语言...
C语言-内部函数和外部函数
C语言-内部函数和外部函数 - 如果一个函数只能被本文件其他函数调用,它称为内部函数 在定义函数时,如果在函数首部的最左端冠以关键字extern,则表示此函数是...
C语言函数练习题及答案
C语言函数练习题及答案 - 1 【单选题】 在下列关于 C 函数定义的叙述中,正确的是? ? A、 函数可以嵌套定义,但不可以嵌套调用; ? ? B、 函数不可以嵌套...
更多相关标签: