标签:默认 介绍 height 总结 src 代码 int let span
寄存器与栈的关系
这里必须提及寄存器与栈的概念,这里以c和汇编的程序为例。图片来源于博主Casualet
为什么用汇编去分析呢?因为汇编更为底层,能够深入操作系统的内容。这儿给出了C与汇编的对比,很明显有两个调用函数和一个main函数,首先对main函数的汇编进行分析,先是头三行:
pushl %ebp //压栈,压入一个栈顶元素并存到寄存器ebp中 movl %esp, %ebp //ebp=esp,栈顶指针赋给ebp subl $4, %esp //esp=esp-4,即esp向下移动4个单位,相当于开辟了1个局部int,同时默认了ebp为栈底
这三条用于保存栈的信息,ebp寄存器指向栈底,esp寄存器指向栈顶,栈底是高地址而栈底是低地址。执行完这三行后,栈就为main开辟了一个新空间,新空间从ebp开始到esp结束。开辟前与开辟后寄存器的位置关系如下图:
开辟前
开辟后
当main执行完后,我们需要消除它的栈,并返回原来的状态,如何返回呢?通过保存ebp=100这个信息。所以我们返回ebp=100,esp=88。这也就是为什么要做上面这三个步骤。然后继续,movl $8,(%esp) ,将数值8放在esp所指向的位置。
效果图
接下将调用函数f,这时候会将EIP寄存器压入栈(EIP用来存储CPU要读取指令的地址), eip指向的是call的下一条指令,,addl $1, %eax(eax是X86汇编语言上cpu通用的寄存器名称,是32位寄存器,用来暂时存储数值或地址),随后进入函数并执行头三行:
pushl %ebp movl %esp, %ebp subl $4, %esp
效果图
movl 8(%ebp) , %eax movl %eax , (%esp) call g
继续看调用函数f中的代码,第一行表示将ebp+8地址所在位置的值放入eax中,而由图解可知ebp+8的值实际上是8。这儿的8又正好是C语言里f(int x)的参数传递。所以,在32位X86的情况下,函数的参数传递是通过栈来实现的。我们在用call命令调用函数之前,先把需要的参数压入栈,然后再使用call命令将eip压栈,然后进入新的函数后,旧的ebp压栈,新的ebp指向的位置存了这个刚压栈的旧的ebp,所以我们可以通过新的ebp指向的位置,通过计算得到函数需要的参数值。接下来,movl %eax, (%esp),会把eax的值放入esp所指向的内存的位置,然后调用 g函数,,又可以压栈call指令的下一条指令的地址。
效果图
进行g函数,执行前两条指令,得到的结果如下:
第三条指令,movl 8(%ebp), %eax ,与之前的代码一致,将ebp+8位置的值存储在eax中。第四条指令,将eax+3,此时eax = 11。第五条指令,popl %ebp,将栈顶的那个数取出并存入到ebp寄存器中,ebp变成了72,因为这个时候esp执行的位置存放的值就是72,而这个值也是上一个函数中ebp的值。所以得到下图:
然后ret执行,会把leave的地址弹到eip中,就可以执行leave 指令了,得到的图是:
leave 指令类似一条宏指令, 等价于movl %ebp, %esp popl %ebp。由已知,ebp=72中存取的值是84,这又是上一个的旧ebp的值,所以继续leave,弹出,得到下图:
这一步后,又遇到了一次ret,开始执行addl $1,%eax,由于之前的eax=11,所以现在变成了12。然后又碰到了leave指令,弹出,达到清栈的目的。效果图如下:
于是栈恢复了状态。此时main中2还剩下一条ret指令,由于之前一开始我们没考虑过main的地址压栈,所这部分问题留给操作系统。
总结
一个函数的执行过程,会有自己的一段从ebp 到esp的栈空间。对于一个函数,ebp指向的位置的值是调用这个函数的上一个函数的栈空间的ebp的值, 这种机制使得leave指令可以清空一个函数的栈、达到调用之前的状态。由于在这个栈设置之前,有一个eip压栈的过程,所以leave 以后的ret正好对应了上一个函数的返回地址,也就是返回上一个函数时要执行的指令的地址,另外,由于对于一个函数的栈空间来说,ebp指向的位置存了上一个ebp的值, 再往上是上一个函数的返回地址,再往上是上一个函数压栈传参数的值,所以我们知道了自己的当前ebp,就可以通过栈的机制来获得参数。
还将介绍具体实例hanio towel(非尾递归),未完待续
标签:默认 介绍 height 总结 src 代码 int let span
原文地址:http://www.cnblogs.com/Bw98blogs/p/7594542.html