6.1 栈帧的形成和关闭
栈在内存中是一块特殊的储存空间,它的储存原则是“先进后出”,汇编过程通常使用PUSH指令与POP指令对栈空间数据压入和数据弹出操作。栈的结构示意图如下:
栈的结构示意图栈结构在内存中占用一段连续的存储空间,通过esp与ebp这两个栈指针寄存器来保存当前栈的起始地址与结束地址(又称栈顶与栈底)。在栈结构中,每4个字节的栈空间保存一个数据,像这样的存储空间称为栈帧。
栈帧是如何形成的呢?当栈顶指针esp小于栈底指针ebp时就形成栈帧。通常,vc++中,栈帧中可以寻址的数据有局部变量、函数返回地址、函数参数等。
不同的两次函数调用,所形成的栈帧也不相同。当由一个函数进入到另一个函数中时,就会针对调用的函数开辟出其所需的栈空间,形成此函数的栈帧。当这个函数结束调用时,需要清除掉它所使用的栈空间,关闭栈帧,我们把这一过程称为栈平衡。
栈指针保存与平衡检查上面代码中,进入函数后,先保存原来的ebp,然后调整ebp的位置到esp, 接下来通过“sub esp, 40h” 这句指令打开了0x40字节大小的栈空间,这是留给局部变量使用的。如果编译选项为Debug,则为了调试方便将局部变量初始化为0CCCCCCCCCh.
由于进入函数前打开了一定大小的栈空间,在函数调用结束后需要将这些栈空间释放,因此需要还原环境POP与"add esp,40h", 以降低栈顶这样的指令。将栈顶指针esp、栈底指针ebp还原后,当使用Debug编译时还要进行平衡检测,以确保栈帧被正确关闭。
6.2 各种调用方式的考察
vc++下调用约定有3种:
_cdecl: C\C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用。
_stdcall: 被调方平衡栈,不定参数的函数无法使用。
_fastcall: 寄存器方式传参,被调方平衡栈,不定参数的函数无法使用。
6.3 使用ebp或esp寻址
高级语言的变量访问,转换成汇编后,就变成了对dbp或esp的加减法操作(寄存器相对间接寻址)来获取变量在内存中的数据:
由此可见,局部变量是通过栈空间来保存的。根据这两个变量以ebp方式可以看出,在内存中,局部变量是以连续排列的方式存储在栈内的。
由于局部变量使用栈空间进行存储,因此进入函数后的第一件事就是开辟函数中局部变量所需的栈空间大小。这时函数中的局部变量就有了各自的内存空间。在函数结尾处执行释放栈空间的操作。因此局部变量是有生命周期的,它的生命周期在进入函数体的时候开始,在函数执行结束的时候结束。
6.4 函数的参数
假设当前esp为 0x0012FF10 ,传递参数为:
函数参数通过栈结构进行传递,其值参顺序为从右向左依次入栈。
因为函数传参数是通过栈方式传递的,使用push指令将数据压入到栈中,而push指令将操作数复制到栈顶,所以这时压入栈中的数据和原数据在两个不同的地址处。因此对参数的修改和原数据没有任何关系。
6.5 函数返回值
函数调用结束后,ret指令执行后为什么可以返回到函数调用昝的下一条指令呢?call指令被执行后,该指令同时还会做另一件事,那就是将下一条指令所在的地址压入栈中。
函数调用前esp与栈中的信息call指令的下一句指令所在地址为 0x0040DB39, 当前esp保存的地址为0x0012FF2C. 当执行call指令时,再次进入函数实现 中观察esp与栈数据的变化,发现esp被减4,并且其对应地址中的数据被修改
执行call指令后esp与栈中内存数据的信息执行call指令后,由于有压栈操作,esp被减4,修改为 0x0012FF28,并且该地址中保存的信息为0x0040DB39, 对比call前,该地址即为函数的返回地址。当函数执行到ret指令时,当前esp已经被平衡,此时将再次指向0x0012FF28.函数退出前,会执行ret指令,这个指令取得esp所指向的4字节内容作为函数的返回地址值更新eip,程序的流程回到返回地址处。
前面分析了call和ret指令的细节,介绍了栈结构中函数的运行机制。那么函数的返回值是如何得到的呢?VC中使用寄存器eax来保存返回值 ,由于32位的eax寄存器只能保存4字节数据,因此大于4字节的数据将使用其他方法保存。通常,eax作为返回值 ,只有基本数据类型与sizeof(type)小于等于4的自定义类型。
网友评论