新书推荐:9.5堆栈图解析生命周期

本节必须掌握的知识点:

   掌握局部变量、全局变量存放在哪

   熟练画堆栈图

   掌握每个函数从哪开始被调用的,从哪结束的

开始看本节前,请读者思考如下几问题:

  1. 局部变量存放在哪里?
  2. 全局变量存放在哪里?
  3. 编译器是怎么辨别哪个是全局变量,哪个是局部变量?
  4. 每个函数都有生命周期吗?
  5. 函数内嵌入函数调用是怎么运作的?

爱思考、爱动手的读者在学习前面的章节中应该已经知道了以上五个问题的答案。本节将再次剖析这些问题,强化对上述问题的理解。

9.5.1 局部变量存放在哪里?

分三种情况讨论,第一种情况:main函数内只有一个局部变量并没有调用函数的例子。

实验七十九:main函数内的局部变量

在VS中新建项目9-5-1.c:

/*

   main函数内的局部变量

*/

#include <stdio.h>

#include <stdlib.h>

int main(void) {

    int i = 0;      //局部变量

    return 0;

}

●代码解析:

  在main函数里定义了一个int类型的局部变量i,我们知道在函数内定义的变量我们统称为局部变量,main函数内定义的变量也不例外。

局部变量存放在栈中,只有系统调用变量所在函数时,系统会动态在函数栈内分配临时空间给局部变量。

      这是最简洁的程序了,我们看它的反汇编,来看它是存放在哪里的。

●Debug版本反汇编代码:

       int main(void) {

008516F0  push        ebp 

008516F1  mov         ebp,esp  ;建立堆栈框架

008516F3  sub         esp,0CCh ;分配0CCH个字节的栈局部变量内空间

008516F9  push        ebx 

008516FA  push        esi     保护寄存器入栈

008516FB  push        edi 

008516FC  lea         edi,[ebp-0CCh] 

00851702  mov         ecx,33h 

00851707  mov         eax,0CCCCCCCCh       堆栈空间初始化0xcc

0085170C  rep stos    dword ptr es:[edi] 

0085170E  mov         ecx,offset _68E6568B_9-5-1@c (085B003h) 

00851713  call        @__CheckForDebuggerJustMyCode@4 (0851203h)   堆栈校验

    int i = 0;      //局部变量

00851718  mov         dword ptr [i],0  ;栈内局部变量i初始化为0

查看局部变量i的地址和值:

在VS监视窗口输入&i,监视窗口显示如下:

名称

类型

&i

0x00f3fae0 {0}

int *

打开寄存器窗口,查看ebp寄存器的值为0x 012FF9B8,局部变量i的地址为0x012FF9B0,因此局部变量i=[ebp-8]。

【注】这是VS中Debug版本的反汇编代码,与其他版本或DtDebug调试器中的栈内地址可能会稍有差异,例如i=[ebp-4]。

在内存窗口地址栏输入0x012FF9B0:

图9-20 局部变量i

    return 0;

0085171F  xor         eax,eax  ;返回值0

}

00851721  pop         edi 

00851722  pop         esi     恢复栈内寄存器

00851723  pop         ebx 

00851724  add         esp,0CCh  ;释放栈内局部变量空间

0085172A  cmp         ebp,esp 

0085172C  call        __RTC_CheckEsp (085120Dh)  堆栈校验

00851731  mov         esp,ebp 

00851733  pop         ebp     释放堆栈框架

00851734  ret 

      

       ●堆栈图:

      

                         图9-21 main函数栈

【注】反汇编代码中的add esp,0cch语句和mov esp,ebp语句等价。请读者参考上述堆栈图,根据自己的编译器或调试器,绘制自己本机的堆栈图。

实验八十:main函数调用函数的情况

在VS中新建项目9-5-2.c:

/*

   main函数调用函数的情况

*/

#include <stdio.h>

#include <stdlib.h>

int funtion()

{

    int i = 2;

    return i;

}

int main(void) {

    int a = funtion();

    printf("a = %d\n", a);

    system("pause");

    return 0;

}

●代码解析:

1、int a = funtion();//在main函数内定义了局部变量a,调用funtion函数,funtion函数返回值赋值给变量a;

2、进入到funtion函数,funtion函数里定义了局部变量i,并赋值;

3、return i;返回值是i,i的值为2,所以funtion函数返回值是2;

4、int a = 2;执行printf函数输出 a = 2;

●反汇编代码:

int main(void) {

01221880  push        ebp 

01221881  mov         ebp,esp  ;建立main函数堆栈框架

01221883  sub         esp,0CCh ;分配局部变量空间

01221889  push        ebx 

0122188A  push        esi  ;保护寄存器入栈

0122188B  push        edi 

0122188C  lea         edi,[ebp-0CCh] 

01221892  mov         ecx,33h              初始化堆栈空间

01221897  mov         eax,0CCCCCCCCh 

0122189C  rep stos    dword ptr es:[edi] 

0122189E  mov         ecx,offset _6AA0E8D2_9-5-2@c (0122C003h) 

012218A3  call        @__CheckForDebuggerJustMyCode@4 (0122121Ch) ;堆栈校验

    int a = funtion();

012218A8  call        _funtion (012211C2h)  ;函数调用

012218AD  mov         dword ptr [a],eax     ;函数返回值保存到局部变量a

    printf("a = %d\n", a);

012218B0  mov         eax,dword ptr [a] 

012218B3  push        eax 

012218B4  push        offset string "a = %d\n" (01227B30h) 

012218B9  call        _printf (0122104Bh)  ;输出变量a

012218BE  add         esp,8 

    system("pause");

012218C1  mov         esi,esp 

012218C3  push        offset string "pause" (01227B3Ch) 

012218C8  call        dword ptr [__imp__system (0122B168h)] 

012218CE  add         esp,4 

012218D1  cmp         esi,esp 

012218D3  call        __RTC_CheckEsp (01221226h) 

    return 0;

012218D8  xor         eax,eax 

}

012218DA  pop         edi 

012218DB  pop         esi  ;保护寄存器出栈

012218DC  pop         ebx 

012218DD  add         esp,0CCh  ;释放sub分配的局部变量空间

}

012218E3  cmp         ebp,esp 

012218E5  call        __RTC_CheckEsp (01221226h)  ;堆栈校验

012218EA  mov         esp,ebp  ;释放局部变量空间

012218EC  pop         ebp  ;释放ebp

012218ED  ret  ;函数返回

int funtion()

{

01221820  push        ebp 

01221821  mov         ebp,esp  ;建立funtion函数堆栈框架

01221823  sub         esp,0CCh ;分配局部变量空间

01221829  push        ebx 

0122182A  push        esi  ;保护寄存器

0122182B  push        edi 

0122182C  lea         edi,[ebp-0CCh] 

01221832  mov         ecx,33h 

01221837  mov         eax,0CCCCCCCCh       ;初始化堆栈空间

0122183C  rep stos    dword ptr es:[edi] 

0122183E  mov         ecx,offset _6AA0E8D2_9-5-2@c (0122C003h) 

01221843  call        @__CheckForDebuggerJustMyCode@4 (0122121Ch)  ;堆栈校验

    int i = 2;

01221848  mov         dword ptr [i],2  ;局部变量i赋值

    return i;

0122184F  mov         eax,dword ptr [i] ;eax保存返回值

}

01221852  pop         edi 

01221853  pop         esi  ;保护寄存器出栈

01221854  pop         ebx 

01221855  add         esp,0CCh  ;释放sub指令分配堆栈空间

0122185B  cmp         ebp,esp 

0122185D  call        __RTC_CheckEsp (01221226h)  ;堆栈校验

01221862  mov         esp,ebp 

01221864  pop         ebp  ;释放堆栈框架

01221865  ret  ;函数返回

●堆栈图:

图9-22 main函数+funtion函数堆栈图

 

总结

每个函数都有他自己的生命周期,在实验八十中,main函数有自己的生命周期,funtion函数也有自己的生命周期。main函数的生命周期是程序结束,funtion函数的生命周期是执行到return i;结束funtion函数。

局部变量是存放在栈中的,局部变量的生命周期是所在函数调用结束,随着系统动态分配的空间也会释放掉。

实验八十一:main函数调用两个函数

在VS中新建项目9-5-3.c:

/*

   main函数调用两个函数

*/

#include <stdio.h>

#include <stdlib.h>

int funtion2()

{

    int j = 6;

    return j;

}

int funtion()

{

    int i = 2;

    return i;

}

int main(void) {

    int a = funtion();

    int b = funtion2();

    printf("a = %d\nb = %d\n", a, b);

    system("pause");

    return 0;

}

●代码解析:

1.int a = funtion();//在main函数内定义了局部变量a,调用funtion函数,funtion函数返回值赋值给变量a;

2.进入到funtion函数,funtion函数里定义了局部变量i,并赋值;

3.return i;返回值是i,i的值为2,所以funtion函数返回值是2;

4.int b = funtion2();//在main函数内定义了局部变量b,调用funtion2函数,funtion2函数返回值赋值给变量b;

5.进入到funtion2函数,funtion2函数里定义了局部变量j,并赋值;

6.return j;返回值是j,j的值为6,所以funtion2函数返回值是6;

7.执行printf函数,输出

a = 2

b = 6;

●反汇编代码:

int main(void) {

00B418F0  push        ebp 

00B418F1  mov         ebp,esp 

00B418F3  sub         esp,0D8h 

00B418F9  push        ebx 

00B418FA  push        esi 

00B418FB  push        edi 

00B418FC  lea         edi,[ebp-0D8h] 

00B41902  mov         ecx,36h 

00B41907  mov         eax,0CCCCCCCCh 

00B4190C  rep stos    dword ptr es:[edi] 

00B4190E  mov         ecx,offset _6B6282E5_9-5-3@c (0B4C003h) 

00B41913  call        @__CheckForDebuggerJustMyCode@4 (0B41221h) 

    int a = funtion();

00B41918  call        _funtion (0B411C2h)  ;函数调用

00B4191D  mov         dword ptr [a],eax 

    int b = funtion2();

00B41920  call        _funtion2 (0B411F9h) ;函数调用

00B41925  mov         dword ptr [b],eax 

    printf("a = %d\nb = %d\n", a, b);

00B41928  mov         eax,dword ptr [b] 

00B4192B  push        eax 

00B4192C  mov         ecx,dword ptr [a] 

00B4192F  push        ecx 

00B41930  push        offset string "a = %d\nb = %d\n" (0B47B30h) 

00B41935  call        _printf (0B4104Bh) 

00B4193A  add         esp,0Ch 

    system("pause");

00B4193D  mov         esi,esp 

00B4193F  push        offset string "pause" (0B47B44h) 

00B41944  call        dword ptr [__imp__system (0B4B168h)] 

00B4194A  add         esp,4 

00B4194D  cmp         esi,esp 

00B4194F  call        __RTC_CheckEsp (0B4122Bh) 

    return 0;

00B41954  xor         eax,eax 

}

00B41956  pop         edi 

}

00B41957  pop         esi 

00B41958  pop         ebx 

00B41959  add         esp,0D8h 

00B4195F  cmp         ebp,esp 

00B41961  call        __RTC_CheckEsp (0B4122Bh) 

00B41966  mov         esp,ebp 

00B41968  pop         ebp 

00B41969  ret 

int funtion()

{

00B41830  push        ebp 

00B41831  mov         ebp,esp 

00B41833  sub         esp,0CCh 

00B41839  push        ebx 

00B4183A  push        esi 

00B4183B  push        edi 

00B4183C  lea         edi,[ebp-0CCh] 

00B41842  mov         ecx,33h 

00B41847  mov         eax,0CCCCCCCCh 

00B4184C  rep stos    dword ptr es:[edi] 

00B4184E  mov         ecx,offset _6B6282E5_9-5-3@c (0B4C003h) 

00B41853  call        @__CheckForDebuggerJustMyCode@4 (0B41221h) 

    int i = 2;

00B41858  mov         dword ptr [i],2  ;局部变量i=2

    return i;

00B4185F  mov         eax,dword ptr [i] 

}

00B41862  pop         edi 

00B41863  pop         esi 

00B41864  pop         ebx 

00B41865  add         esp,0CCh 

00B4186B  cmp         ebp,esp 

00B4186D  call        __RTC_CheckEsp (0B4122Bh) 

00B41872  mov         esp,ebp 

00B41874  pop         ebp 

00B41875  ret 

int funtion2()

{

00B41890  push        ebp 

00B41891  mov         ebp,esp 

00B41893  sub         esp,0CCh 

00B41899  push        ebx 

00B4189A  push        esi 

00B4189B  push        edi 

00B4189C  lea         edi,[ebp-0CCh] 

00B418A2  mov         ecx,33h 

00B418A7  mov         eax,0CCCCCCCCh 

00B418AC  rep stos    dword ptr es:[edi] 

00B418AE  mov         ecx,offset _6B6282E5_9-5-3@c (0B4C003h) 

00B418B3  call        @__CheckForDebuggerJustMyCode@4 (0B41221h) 

    int j = 6;

00B418B8  mov         dword ptr [j],6  ;局部变量j=6

    return j;

00B418BF  mov         eax,dword ptr [j] 

}

00B418C2  pop         edi 

00B418C3  pop         esi 

00B418C4  pop         ebx 

00B418C5  add         esp,0CCh 

00B418CB  cmp         ebp,esp 

00B418CD  call        __RTC_CheckEsp (0B4122Bh) 

00B418D2  mov         esp,ebp 

00B418D4  pop         ebp 

00B418D5  ret 

●堆栈图:

                        图9-23 main函数

                        图9-24 funtion函数

                        图9-25 funtion2函数

        

细心的读者可能会发现一个有意思的事情,funtion函数栈被释放后,紧接着这段堆栈空间就被分配给了funtion2函数。当然这并不代表这一定会发生这样的事情,也许是一个偶然事件。

 

总结

不管多么复杂的程序,只要我们慢慢画堆栈图,都会剥开云雾,都会明白它们之间干了什么,谁调用了谁,怎么调用的。每个函数都有生命周期,每个变量也有自己的生命周期,局部变量的生命周期,通过画堆栈图,我们可以很明确的看出每个变量的生命周期。

9.5.2 全局变量存放在哪里?

全局变量的作用域是从全局变量定义的位置到本源文件结束都有效。

我们先看一下全局变量在反汇编中是怎么体现的,如实验八十二示例示例代码9-5-4.c。

实验八十二:全局变量的作用域

在VS中新建项目9-5-4.c:

/*

   全局变量的作用域

*/

#include <stdio.h>

#include <stdlib.h>

int i = 2;

int main(void) {

    int j = i;

    return 0;

}

       VS中的反汇编代码:

/*

   全局变量的作用域

*/

#include <stdio.h>

#include <stdlib.h>

int i = 2;

int main(void) {

010416F0  push        ebp 

010416F1  mov         ebp,esp 

010416F3  sub         esp,0CCh 

010416F9  push        ebx 

010416FA  push        esi 

010416FB  push        edi 

010416FC  lea         edi,[ebp-0CCh] 

01041702  mov         ecx,33h 

01041707  mov         eax,0CCCCCCCCh 

0104170C  rep stos    dword ptr es:[edi] 

0104170E  mov         ecx,offset _6E2D9460_9-5-4@c (0104B003h) 

01041713  call        @__CheckForDebuggerJustMyCode@4 (01041203h) 

    int j = i;

01041718  mov         eax,dword ptr [i (01049000h);全局变量i

0104171D  mov         dword ptr [j],eax  ;局部变量j

    return 0;

01041720  xor         eax,eax 

}

01041722  pop         edi 

01041723  pop         esi 

01041724  pop         ebx 

01041725  add         esp,0CCh 

0104172B  cmp         ebp,esp 

0104172D  call        __RTC_CheckEsp (0104120Dh) 

01041732  mov         esp,ebp 

01041734  pop         ebp 

01041735  ret 

01041718  mov         eax,dword ptr [i (01049000h);全局变量i

这一行中(01049000h)正是全局变量i的存放地址

全局变量编译的时候就已经确定了内存地址和宽度,变量名就是内存地址的别名。如果不重新编译(也就是不重新构建程序),全局变量的内存地址将不会改变。

 

总结

全局变量保存在内存的全局区中,占用静态的存储单元。说到静态的存储单元,这里还要提一下全局变量分为:全局变量和静态全局变量。静态全局变量只是在int i = 2;前加static关键字。

书写形式:static int i =2;

全局变量与静态全局变量有什么区别?

全局变量作用范围:从全局变量定义的位置到本源文件结束都有效,如果想在别的文件中访问可以加上extern声明,书写形式:extend int i = 2;。

静态全局变量作用范围:只在定义它的文件中可用,而文件之外是不可以被看见的。静态全局变量就是用来解决重名问题的,使用静态全局变量就是告诉编译器这个变量只在当前文件使用,在别的文件中就不可以使用。如果静态全局变量定义在函数内,则它的作用域会被限制在该函数内。

对于一个完整的程序:内存分布有如下几个区、栈区、堆区、全局区、常量区、代码区。 

图9-26 C语言内存分区

图9-26中有几个内存区域没有介绍到,以后的章节中会补充说明。接下来我们使用示例来说明C语言的内存区域。

实验八十三:C语言的内存区域

在VS中新建项目9-5-5.c:

/*

   C语言的内存区域

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

//全局区

int g_n1 = 1;//全局初始化区

char g_c2;//全局未初始化区

void funtion()

{

    int a = 1;//funtion函数栈区

}

int main(void) {

    int nNum = 1;//main函数栈区

    char cStr2[] = "123";//main函数栈区

    char *cStr1 = "hello";//cStr1在main函数栈区,hello\0在常量区

    static int nNum1 = 0;//全局初始化区

    char *pCStr = (char *)malloc(10);//分配10字节区域在堆区

    strcpy_s(pCStr,10, "666");//666放在常量区

    printf("程序代码区的地址\n");

    printf("funtion=%08p\n", funtion);

    printf("文字常量区 常量的地址\n");

    printf("&cStr1=%08p\n", &cStr1);

    printf("&pCStr=%08p\n", &pCStr);

    printf("全局区变量的地址\n");

    printf("&g_n1=%08p\n", &g_n1);

    printf("&g_c2=%08p\n", &g_c2);

    printf("&nNum1=%08p\n", &nNum1);

    printf("栈区 变量的地址\n");

    printf("&nNum=%08p\n", &nNum);

    printf("&cStr2=%08p\n", &cStr2);

    printf("堆区 空间的地址\n");

    printf("pCStr=%08p\n", pCStr);

    free(pCStr);//释放

    system("pause");

    return 0;

}

●输出结果:

程序代码区的地址

funtion=003B11C2

文字常量区 常量的地址

&cStr1=00EFFE0C

&pCStr=00EFFE00

全局区变量的地址

&g_n1=003BA000

&g_c2=003BA145

&nNum1=003BA140

栈区 变量的地址

&nNum=00EFFE24

&cStr2=00EFFE18

堆区 空间的地址

pCStr=03394A90

请按任意键继续. . .     

9.5.3 全局变量引发的事故

       在C语言中要求程序员尽量遵循最低权限原则,能使用局部变量就不要使用全局变量,不希望被修改的地址变量,形参就需要添加const修饰词。这些是善意的提醒。而在汇编语言中没有这样的要求,这需要程序员自己对代码的安全性负责。

因为全局变量的作用域是整个文件,在源文件的任意位置修改了全局变量的值,都会影响到其他地方对该全局变量的使用。对于更为复杂的多模块、多线程的程序则更要加倍小心。

【注】多线程的程序我们暂且不涉及,本书只是针对初学者编写,有兴趣的读者可以参考编程达人系列教程《Windows API每日一练》这本书。

接下来我们举例说明。

实验八十四:全局变量引发的事故

在VS中新建项目9-5-6.c:

/*

   全局变量引发的事故

*/

#include <stdio.h>

#include <stdlib.h>

#include <windows.h>

int flag = 11111111;

int main(void)

{

    if (flag == 11111111)

    {

        while (1)

        {

            Sleep(50);

            printf("flag=%d\n\n", flag);           

        }

    }

    else if (flag == 22222222)

    {

        while (1)

        {

            Sleep(50);

            printf("flag=%d\n\n", flag);

        }

    }

    else if (flag == 33333333)

    {

        while (1)

        {

            Sleep(50);

            printf("flag=%d\n\n", flag);

        }

    }

    else

    {

        printf("flag=%d\n\n", flag);

    }

    system("pause");

    return 0;

}

输出结果:

       flag=11111111

flag=11111111

flag=11111111

…    

      程序中,if/else语句块分别设置了3个while语句死循环结构,由flag全局变量的条件语句控制,全局变量编译的时候就已经确定了内存地址和宽度,并且存在于整个程序的生命周期。

程序运行期间,无法改变全局变量flag的值,我们将借助一款内存修改工具CE(编程达人网站资料下载CheatEngine)在程序运行时,通过直接修改内存的方式改变flag全局变量的值实现条件语句的跳转。     

第一步:运行程序MyProjectOne.exe,打开CE,点击选择进程按钮,如图9-27所示。

第二步:选择打开的进程MyProjectOne.exe,点击open,如图9-28所示。

第三步:搜索框内输入搜索精确值“11111111”,点击首次搜索,左侧栏显示搜索值所在的内存地址,鼠标左键双机该地址,底栏描述信息栏显示地址“000DA000”处的值为“11111111”,如图9-29所示。

第四步:鼠标双机描述栏搜索值“11111111”,弹出的对话框中将其修改为“22222222”,如图9-30所示。

此时观察,程序运行的控制台窗口,如图9-31所示。 

                        图9-31 变化的控制台窗口

图9-27 CE内存修改器  

图9-28 打开进程MyProjectOne.exe

图9-29 搜索精确值的内存地址

  图9-30 修改内存地址存储的值 

                

我们发现运行结果发生了改变,这就是传说中的“基址”,只要找到了“基址”,我们就可以暴力施加一些小特技。

以上就是所为的全局变量引发的小事故,感兴趣的同学可以试着CE工具找一下类似于小游戏植物大战僵尸的“基址”,增强一下对学习编程的兴趣。

9.5.4 函数的参数内存分布情况

看下面这段代码,我们通过切换到反汇编,调出寄存器窗口,然后一步步画堆栈图的形式,让大家对函数的参数内存分布情况一目了然。

实验八十五:函数的参数内存分布情况

在VS中新建项目9-5-7.c:

/*

   函数的参数内存分布情况

*/

#include <stdio.h>

#include <stdlib.h>

int fnAdd(int x, int y)

{

    return x + y;

}

int main(void)

{

    int i = fnAdd(1, 2);

    return 0;

}

       VS Debug版反汇编代码:

int main(void)

{

00F71760  push        ebp 

00F71761  mov         ebp,esp  ;建立堆栈框架

00F71763  sub         esp,0CCh ;分配局部变量空间

00F71769  push        ebx 

00F7176A  push        esi     ;保护寄存器入栈

00F7176B  push        edi 

00F7176C  lea         edi,[ebp-0CCh] 

00F71772  mov         ecx,33h 

00F71777  mov         eax,0CCCCCCCCh       初始化堆栈

00F7177C  rep stos    dword ptr es:[edi] 

00F7177E  mov         ecx,offset _6C6B2A39_9-5-7@c (0F7C003h) 

00F71783  call        @__CheckForDebuggerJustMyCode@4 (0F71208h)  ;堆栈校验

    int i = fnAdd(1, 2);

00F71788  push        2  ;参数2入栈

00F7178A  push        1  ;参数1入栈

00F7178C  call        _fnAdd (0F711C2h)  ;函数调用

00F71791  add         esp,8  ;堆栈平衡

00F71794  mov         dword ptr [i],eax  ;函数返回值存入局部变量i

    return 0;

00F71797  xor         eax,eax 

}

00F71799  pop         edi 

00F7179A  pop         esi  ;恢复保护寄存器

00F7179B  pop         ebx 

00F7179C  add         esp,0CCh  ;释放局部变量堆栈空间

00F717A2  cmp         ebp,esp 

00F717A4  call        __RTC_CheckEsp (0F71212h)  ;堆栈校验

00F717A9  mov         esp,ebp  ;释放ebp

00F717AB  pop         ebp  ;释放堆栈框架

00F717AC  ret  ;函数调用返回

      

int fnAdd(int x, int y)

{

00F71700  push        ebp 

00F71701  mov         ebp,esp   ;建立堆栈框架

00F71703  sub         esp,0C0h  ;分配局部变量空间

00F71709  push        ebx 

00F7170A  push        esi  ;保护寄存器入栈

00F7170B  push        edi 

00F7170C  lea         edi,[ebp-0C0h] 

00F71712  mov         ecx,30h 

00F71717  mov         eax,0CCCCCCCCh       初始化堆栈

00F7171C  rep stos    dword ptr es:[edi]   

00F7171E  mov         ecx,offset _6C6B2A39_9-5-7@c (0F7C003h) 

00F71723  call        @__CheckForDebuggerJustMyCode@4 (0F71208h)   ;堆栈校验

    return x + y;

00F71728  mov         eax,dword ptr [x]  ;取变量x的值

00F7172B  add         eax,dword ptr [y]  ;x+y

}

00F7172E  pop         edi 

00F7172F  pop         esi  ;恢复保护寄存器

00F71730  pop         ebx 

00F71731  add         esp,0C0h   ;释放局部变量堆栈空间

00F71737  cmp         ebp,esp 

00F71739  call        __RTC_CheckEsp (0F71212h)  ;堆栈校验

00F7173E  mov         esp,ebp    ;释放ebp

00F71740  pop         ebp   ;释放堆栈框架

    00F71741  ret    ;函数调用返回

      

       堆栈图:

                        图9-32 fnAdd函数堆栈图

最后执行完都回到最初开始的地方,如果没有回到最初开始地方,说明堆栈没平衡,程序出现Bug了。

重点:[ebp+8]是压入的第一个参数:1,[ebp+0xC]是压入的第二个参数:2,……而[ebp+4]是函数返回地址。

从[ebp+8]开始是参数存储的地方。[ebp+4]是函数返回地址。这里不要嫌弃笔者重复叙述,重要的事情说三遍。是为了能够让读者朋友加深印象。如果你没有画堆栈图,你会后悔的。

练习

1、写出下面程序运行结果,并画出堆栈图。

#include <stdio.h>

#include <windows.h>

int fnSub(int x,int y)

{

   return x-y;

}

int fnAdd(int x,int y)

{

return x+y;

fnSub();

}

int main(void)

{

int i =fnAdd(1,2);

      return 0;

}

2、写出下面程序运行结果,并画出堆栈图。

#include <stdio.h>

#include <windows.h>

int i = 2;

int fnAdd(int x,int y)

{       

return x+y;

}

int main(void)

{

int j =fnAdd(1,2);

i = j;

printf(“%d\n”,i); 

return 0;

}

3、思考题1:会出现什么结果,写出程序输出结果,并画出堆栈图。

int add(int a)

{

       return add(a-1);

}

void main() 

{      

       int i = add(5);

       return; 

 }

4、思考题2:下面程序有问题吗?为什么?请写出理由。(提示数组下标是否越界?数组下标越界是否可用?自己可以切换到反汇编查看)。

#include <stdio.h>

#include <windows.h>

void fnStr()

{

    printf(“Hello World”);

    getchar();

}

void fnArr()

{

int arr[5]={1,2,3,4,5};

arr[6] = (int)fnStr;

}

int main(void)

{

   fnArr();

   return 0;

}

本文摘自编程达人系列教材《汇编的角度——C语言》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值