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版本的反汇编。

C++函数调用栈空间结构探究&《程序员的自我修养》纠错_第1张图片

图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所指向位置开始压栈。

 

你可能感兴趣的:(C++函数调用栈空间结构探究&《程序员的自我修养》纠错)