C++函数调用栈空间结构探究&《软件工程师的自小弟我修养》纠错
杨力祥老师在C++课后给同学留了一道思考题,即探讨C++函数调用时其内存的结构究竟是什么样的。在参考《程序员的自我修养》的过程中,对于书上的描述有些疑惑,因此自己在VS2008的环境下,对程序1进行了反汇编,并随着单步调试的进行察看了内存的变化,发现书上给出的图和描述存在一些小错误。在此将实际的过程记录下来。
//程序1
#include <iostream>
using namespacestd;
int foo(inti){
int a= 1, b= 2;
b = a;
return i;
}
int main(){
int a =foo(0xABCDEF);
return 0;
}
首先要说明的是,程序运行时的实现是根据编译器的不同而不同的,因此需要强调,本文中讨论的所有过程都是在VS2008的编译环境下进行的,且考察的是debug版本的反汇编。
图1 运行时的栈结构
图1 是函数调用时栈的结构,下面来具体说一说汇编指令每一步都做了什么。
要事先说明的是EBP指针和ESP指针,前者在一个函数的运行过程中(没有调用其他函数时)是一个固定值,它指向的位置如图1所示;后者是栈顶指针。
序 |
操作方 |
对应汇编 |
内存操作 |
1 |
main 函数 |
push 0ABCDEFh |
函数参数压栈 ESP指向位置1 |
2 |
main 函数 |
call foo (11C1028h) |
在调用的时候,会自动将调用函数之后下一条要执行的指令地址(也就是所谓的返回地址)压栈 ESP指向位置2 |
3 |
foo 函数 |
push ebp mov ebp,esp |
将旧的EBP值压栈; ESP指向位置3 将当前ESP值赋给EBP,所以EBP也指向位置3 |
4 |
foo 函数 |
sub esp,0D8h |
在栈上给局部变量预留空间 ESP指向位置4 |
5 |
foo 函数 |
push ebx push esi push edi |
寄存器压栈保存 ESP指向位置5 |
6 |
foo 函数 |
lea edi,[ebp-0D8h] mov ecx,36h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] |
将位置4的地址值赋给EDI寄存器; 将位置4到位置3之间的内存全部赋值为0CCCCCCCCh(由低到高) 此时ESP指向位置5 |
7 |
foo 函数 |
mov dword ptr [a],1 mov dword ptr [b],2 |
通过指针访问,将局部变量区域中的a和b赋值; 注意:a和b并不是连续存放在栈中的,也没有将局部变量的空间占满。 ESP位置不变 |
8 |
foo 函数 |
mov eax,dword ptr [a] mov dword ptr [b],eax |
通过EAX寄存器来完成局部变量的赋值操作;依然是指针访问。 ESP位置不变 |
9 |
foo 函数 |
mov eax,dword ptr [i] |
8,9,10三步实现foo函数返回; 通过EAX寄存器来传递返回值 ESP依然指向位置5,即最后一个被压栈的寄存器 |
10 |
foo 函数 |
pop edi pop esi pop ebx |
将此前压栈的寄存器出栈 ESP指针指向位置4 |
11 |
foo 函数 |
mov esp,ebp pop ebp |
将EBP值赋给ESP指针,意味着释放掉了局部变量的内存空间 ESP指向位置3 将此前压栈的旧EBP值出栈,赋给EBP ESP指向位置2 |
12 |
foo 函数 |
ret |
函数返回,会根据此前压栈的返回地址跳转到下一条指令的位置 ESP指向位置1 |
13 |
main 函数 |
add esp,4 |
释放此前压栈的foo函数参数,至此,foo函数彻底结束了它的生命 ESP指向位置0 |
14 |
main 函数 |
mov dword ptr [a],eax |
通过EAX传递返回值,将返回值赋给变量a; main函数中有关调用foo函数的内容至此结束。 |
需要说明的是,在函数完成了现场保护之类的初始化工作之后,ESP会始终指向当前函数的栈空间顶,此时,若当前函数又调用了另一个函数,则会将此时的EBP视为旧EBP压栈,而与新调用的函数有关的内容则会从当前ESP所指向位置开始压栈。