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

[转载]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语言函数调用规定
stdcall 调用约定声明的语法为(以前文的那个函数为例): 调用约定声明的语法为(...C语言函数调用的底层机制... 6页 免费 C语言函数调用规定 56页 免费 C...
c语言函数调用详细过程
(gdb) disassemble main ---> 反汇编 main 函数 Dump of assembler code for...C语言函数调用的底层机制... 6页 免费 浅析c语言的函数调用 13页 免费 C...
浅析c语言的函数调用
浅析c 语言的函数调用 C 语言中的函数调用形式差别在形参的不同或是返回类型的...C语言函数调用的底层机制... 6页 免费 C语言函数的形参和实参运... 9页 免...
C语言函数习题及答案
以下关于函数的叙述不正确的是【 】。 A)C 程序是函数的集合,包括标准库函数和用户自定义函数 B)在 C 语言程序中,被调用的函数必须在 main 函数中定义 C)...
通过C语言调用函数的方式来实现线性表的基本操作
通过相关算 法的 C 语言实现,并考虑线性表在多项式运算中的应用。 (一)请通过 C 语言调用函数的方式来实现线性表的 基本操作: 1. 创建、初始化 线性表 2. ...
C语言函数调用三种方式 传值调用,引用调用和传地址调
C语言函数调用三种方式 传值调用,引用调用和传地址调_数学_自然科学_专业资料。...C语言函数调用规定 5页 免费 C语言函数调用的底层机制... 6页 免费 C...
C语言函数调用
2. C语言的函数兼有其它语言中的函数和过程两种功能,从这个角度看,又可把函数分为 有返回值函数和无返回值函数两种。 1) 有返回值函数:此类函数调用执行完...
汇编语言过程调用与C语言函数调用的异同
汇编过程调用以堆栈结构为核心包括定 义、调用机制应用、变量访问等关键行为。在 C 语言中则包括定义、 参数定义、数据传递等。 关键词:函数、过程、数据传递、...
汇编语言的过程调用与c语言的函数调用
汇编语言过程调用与c语言的函数调用_数学_自然科学_专业资料。汇编语言的过程调用与 c 语言的函数调用姓名:孙贵森 学号: 201212301118 汇编语言的过程调用,如果需要...
C语言 编程题函数调用
C语言 编程题函数调用_IT认证_资格考试/认证_教育专区。C语言 编程题函数调用编程题 #include <math.h> #include <stdio.h> #include <conio.h> double fun...
更多相关标签: