当调用者比如h调用某个函数f时,从编译器或者汇编语言角度来看,主要分以下几个步骤进行:
具体来说,从内存的角度看,函数h调用f时,Stack是按下面步骤发生变化的:
那么,到底是谁将“下一条指令地址”放入stack中的呢?当然是调用者h了。其实这个功能是一条汇编指令call实现的,而不是简单的用push/pop/mov指令实现的。CALL指令的执行可以视为做了以下工作:
想来个直观点的说明,最好还是通过一个小程序。昨天用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 into(VC6.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指令的意思是将ESP加8,其实就是将之前放入stack的两个实参从stack中移 除。函数调用也到此结束。
![]() |
函数调用过程详解
最新推荐文章于 2022-12-04 21:36:03 发布