Chinaunix首页 | 论坛 | 博客
  • 博客访问: 445703
  • 博文数量: 111
  • 博客积分: 4290
  • 博客等级: 上校
  • 技术积分: 1301
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-24 14:22
个人简介

努力工作,建立一个幸福的家庭。

文章分类

全部博文(111)

文章存档

2015年(4)

2013年(9)

2012年(6)

2011年(17)

2010年(69)

2009年(6)

分类: WINDOWS

2011-03-15 22:24:46

函数调用机制例解

      昨天室友拿一个面试题为难我,问我C/C++函数调用是怎么一个流程。这问题实在简单,然而有一本什么面试宝典却说的前后不一,漏洞重重。室友尽信于书,非与我分个高低。单从机制本身来说,公说公有理,婆说婆有理,于是我就用了一个简单的实验才勉强说清楚。在此也顺便总结一下,从汇编的角度介绍一下函数调用过程。

 

      当调用者比如h调用某个函数f时,从编译器或者汇编语言角度来看,主要分以下几个步骤进行:

·                                 h将实参按照从右向左的顺序一个个压入stack中。

·                                 执行一个转移指令call f

·                                 f执行完函数体后,将返回值传入寄存器AX/EAX/RAX中。

·                                 f执行转移指令ret

·                                 h将实参从stack中一个一个弹出。

      由此可见,编译器是不会把下一条指令地址压入Stack中的。然而,当从f返回后,CPU是如何知道下一步应该执行什么指令呢?也就是说下一条指令的地址从哪来的呢?这当然还是从stack中获得。那么,这个地址是什么时候放到stack中的?还有,它什么时候从stack中出来的?这些工作是由谁来完成的?是调用者?还是被调用者?这就得先从内存的角度看一下Stack的变化。主是看esp/rsp寄存器的内空以及该地址对应的内存单元的内容。

      

      具体来说,从内存的角度看,函数h调用f时,Stack是按下面步骤发生变化的:

·                                  实参按照从右向左的顺序一个一个进入stack中。

·                                 函数调用指令之后的下一条指令地址进入stack中。

·                                 函数f中的局部变量加入到stack中。

·                                 函数f中的局部变量从Stack中弹出。

·                                 下一条指令地址stack中弱出,流入程序计数器寄存器IP中。

·                                 寄存器AX/EAX/RAX中的值流入到stackh的局部变量(或者全局变量等)中。

·                                 调用函数f时的实参从stack中弹出。

      然而,由于编译器的优化,用较新的编译器将程序翻译成汇编后,这部分逻辑变的比较难懂。如较新的GCC/G++编译器压stack和弹stack的操作都不是用pushpop指令实现的,而是一次性地将ESP/RSP增加一定的数值(分配好实参的空间),然后用MOV指令将参数放入Stack中的,这样速度比较快。老版本的编译器翻译成的汇编比较好懂。

 

      那么,到底是谁将下一条指令地址放入stack中的呢?当然是调用者h了。其实这个功能是一条汇编指令call实现的,而不是简单的用push/pop/mov指令实现的。CALL指令的执行可以视为做了以下工作:

·                                 下一条指令地址压入stack

·                                 改变IP的值为被调用的函数的地址。

      相应地,将下一条指令取出的操作是被调用者做的。其实这是RET指令的功能,而不是用PUSH/POP/MOV来实现的。从硬件角度来讲,RET指令也没有什么特别的,它仅仅就是与CALL指令对应的功能相反的指令,与CALL做的工作恰好相反,恢复了IP寄存器,使其指向调用者调用函数之后的下一条指令的地址

 

      想来个直观点的说明,最好还是通过一个小程序。昨天用GCC已经做过测试,由于版本比较新,它做的优化太多了,比如它尽量使用寄存器进行参数传递而非Stack,所以介绍起来比较麻烦。并且GCC使用的AT&T汇编格式比较难懂,还是用WINDOWS下都熟悉的MASM格式的汇编来列一下吧。同时为了清晰,少费点口舌,就用可视化的工具VC++6.0来介绍。

 

      首先,假设程序代码如下(很简单的):

int f(int a, int b)
{
    return a*b;
}

int main(int argc, char *argv[])
{
    int x = 0;
    x = f(5,6);
    ++x;
    return 0;
}

 

      编译完的汇编代码不再列出。对它调度跟踪一下,一切问题都有了着落。比如先把断点设在x=f(5,6)的地方,执行到该位置后,各个寄存器的值如“Resisters”窗口所示,此时的断点以及对应的汇编代码如下:

 

      接着执行,执行到call那条指令时,内存内容及相应寄存器的值如下面的图。Memory窗口显示了当前Stack从顶部开始的内存内容。最顶的是参数5(占4个字节,从低到高),然后是6,这两个是传给f的实参。此时,下一条指令地址也就是紧挨着的add指令的地址(0x401078)并没有放到stack中。由此可见,下一条指令地址是第一个进栈的这种说法是不对的。

 

 

      然后接着执行,采用step intoVC6.0中对应F11),也就是仅执行call这条指令,而不执行f中的任何指令。执行后如下图。可以看出,此时寄存器ESP的值变了,向下移动了4个字节,也就是stack中新插入了一个4字节的整数(其实它是一个内存地址)。这个新进来的地址是0x00401078,对应main函数中的add那条指令,也调用时的就是下一条指令地址。现在很清楚了,将下一条指令地址压stack”CALL指令的功能,是硬件干的,而不是软件。

 

 

      然后要说明的就是函数f返回过程了。当程序执行到RETURN语句时,对应的内存和寄存器状态如下。可以看出,RET指令执行之前,下一条指令地址还在stack中,同时EAX的值0x1E也就是30就是函数f的返回值。用EAX传递返回值是编译器的一个习惯,能不能说是标准我不太确定。反正GNU系列的编译器和微软的都是这么干的。

 

 

      最后,当f中的RET执行完后,会形成如下格局。与上图对比,会发现,stack顶的值跑到EIP里面去了。stack中仅剩下之前的两个实参!所以,下一条指令地址是第一个出栈的,而不是最后一个。接下来的add指令的意思是将ESP8,其实就是将之前放入stack的两个实参从stack中移除。函数调用也到此结束。

 

阅读(667) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~