函数调用机制2

http://blog.sina.com.cn/s/blog_476cd878010001y3.html

这是一篇介绍C语言中的函数调用是如何用实现的文章。写给那些对C语言各种行为的底层实现感兴趣人的入门级文章。如果你是C语言或者汇编、底层技术的老鸟或是对这个问题不感兴趣,那么这篇文章只会耽误您的时间,您大可不必阅读他。当然如果前辈们愿意为我指出不足,我将十分感谢您的指导,并对耽误您宝贵的时间致歉。好了,废话少说!要研究这个问题,让我们先打开VC++吧。最好是6.0的,:-P。(什么你没有VC++,倒!....赶快装一个[email protected]#$,要快!) 首先,让我们在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这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡呢?所有上述提议都是绝对可行的,而他们之间不同的组合就造就了函数不同的调用方法。也就是你常看到或听到的            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   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         8; <== ret 取出返回地址后,

;给esp加上 8。

;看!堆栈平衡在函数内完成了

;ret指令就是专门用来实现

; 函数内完成堆栈平衡的

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

时间: 2024-12-09 11:50:53

函数调用机制2的相关文章

C函数调用机制及栈帧指针

转载: http://bbs.csdn.net/topics/90317145 http://blog.chinaunix.net/uid-26817832-id-3347227.html 帧指针 和栈指针到底是什么,有什么联系吗 FP帧指针指向帧头 SP栈指针指向栈顶 大部分现代计算机系统使用栈来给进程传递参数并且存储局部变量.栈是一种在进程映象内存的高地址内的后进先出(LIFO)的缓冲区.当程序调用一个函数时 一个新的"栈帧"会被创建.这个栈帧包含着传递给函数的各种参数和一些动态的

(转载)你好,C++(25)函数调用和它背后的故事5.1.2 函数调用机制

你好,C++(25)函数调用和它背后的故事5.1.2 函数调用机制 5.1.2  函数调用机制 在前面的学习中,我们多次提到了“调用函数”的概念.所谓调用函数,就是将程序的执行控制权从调用者(某个函数)交给被调用的函数,同时通过参数向被调用的函数传递数据,然后程序进入被调用函数内部,执行函数定义中的代码获得结果数据,函数体代码执行完毕后再将控制权交回给调用者同时将结果数据通过返回值传递给调用者,作为整个函数调用表达式的值.简而言之,调用函数也就是执行函数中的代码,完成函数的功能. 在学习如何编写

函数调用机制

http://blog.csdn.net/jjiss318/article/details/7185802 在Linux内核程序boot/head.s执行完基本初始化操作之后,就会跳转去执行init/main.c程序.那么head.s程序是如何把执行控制转交给init/main.c程序的呢?即汇编程序是如何调用执行C语言程序的?这里我们首先描述一下C函数的调用机制.控制权传递方式,然后说明head.s程序跳转到C程序的方法. 函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移

通过反汇编C代码分析计算机函数调用机制和栈帧结构

备注:秋风 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 C程序代码(简单的函数调用和返回): 将C源码编译成汇编代码: 有效的汇编代码内容: 通过仔细分析汇编代码的工作过程中堆栈的变化情况,可以描绘出函数调用过程中使用的栈帧结构如下: 总结: 计算机在工作时使用程序栈来支持函数或过程调用,机器用栈传递参数.存储返回信息.保存寄存器值用于以后恢复,和存储本地变量.栈向低地址方向

Mono生命周期函数调用机制分析

1.生命周期函数的调用机制是反射,而并非继承. 我们打开Mono后发现其的确也没有生命周期函数的实现,向上翻父类中也没有,好,可以确定其就不是继承了. 下面验证下是否是反射,拿void start()函数来说,其原本是private,但如果你硬要改成public,其执行起来也什么影响(但你肯定不想外界去手动调用你的Start吧),甚至于你把它设置成private IEnumerator Start()都可以,因为是发射调用嘛,名字对了就可以.如下图             (针对Start使用I

jQuery 学习笔记(函数调用机制)

最近在学前端框架amazeui,之前用其中的CSS样式搭建了一个伪360网页,学会了点布局的东西,但是始终觉得有点无聊.所以这几天就开始研究jquery代码了. 对于我这样一个初学者来说,有很多东西都只能用懵逼来形容,比如我看到这么一段代码(复制自amazeui): var checkin = $myStart2.datepicker({ onRender: function(date, viewMode) { // 默认 days 视图,与当前日期比较 var viewDate = nowDa

虚函数调用机制

#include <iostream> class Animal { public: Animal(){}; virtual ~Animal(){}; virtual void Eat() { std::cout << "Animal Eat" << std::endl; } }; class Bird : public Animal { public: Bird(){}; void Eat() { std::cout << "

(转)c++多态实现的机制

原文地址:http://blog.csdn.net/zyq0335/article/details/7657465 1 什么是多态?多态性可以简单的概括为“1个接口,多种方法”,在程序运行的过程中才决定调用的机制程序实现上是这样,通过父类指针调用子类的函数,可以让父类指针有多种形态.2 实现机制举一个例子:#include <iostream.h>class animal{public:void sleep(){cout<<"animal sleep"<&

深入剖析C/C++函数的参数传递机制

2014-07-29 20:16 深入剖析C/C++函数的参数传递机制 C语言的函数入口参数,可以使用值传递和指针传递方式,C++又多了引用(reference)传递方式.引用传递方式在使用上类似于值传递,而其传递的性质又象是指针传递,这是C++初学者经常感到困惑的.为深入介绍这三种参数传递方式,我们先把话题扯远些: 1. C/C++函数调用机制及值传递: 在结构化程序设计方法中,先辈们告诉我们,采用“自顶向下,逐步细化”的方法将一个现实的复杂问题分成多个简单的问题来解决.而细化到了最底层,就是