您好,欢迎来到易榕旅网。
搜索
您的当前位置:首页C/C++中堆空间和栈空间的区别

C/C++中堆空间和栈空间的区别

来源:易榕旅网

非本人作也!因非常经典,所以收归旗下,与众人阅之!原作者不祥!


一直对堆栈空间的存储问题不是很理解,这个众人皆评论很经典,先转载过来慢慢看,原文排版太乱,有空再把原文排版修改一下。


一、预备知识—程序的内存分配

1.1 一个由c/C++编译的程序占用的内存分为以下几个部分:

1.2 例子程序

这是一个前辈写的,非常详细 :

//main.cpp 
int a = 0;      //全局初始化区 
char *p1;       //全局未初始化区 
main() 
{ 
int b;              //栈 
char s[] = "abc";   //栈 
char *p2;           //栈 
char *p3 = "123456";    //123456\0在常量区,p3在栈上。 
static int c =0;        //全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
//分配得来得10和20字节的区域就在堆区。 
strcpy(p1, "123456");  //123456\0放在常量区,编译器可能会将它与p1所指向的"123456"优化成一个地方。 
} 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

二、堆和栈的理论知识

2.1 申请方式

  • stack:

  由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 

 
 
  • 1
  • heap:

  •   需要程序员自己申请,并指明大小,在c中malloc函数 
      如p1 = (char *)malloc(10); 
    
      在C++中用new运算符 
      如p2 = (char *)malloc(10); 
      但是注意p1、p2本身是在栈中的。 
    
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2 申请后系统的响应

    • 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

    2.3申请大小的限制

    2.4申请效率的比较:

    • 栈:由系统自动分配,速度较快。但程序员是无法控制的。

    2.5堆和栈中的存储内容

    • 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

    2.6存取效率的比较

    char s1[] = "aaaaaaaaaaaaaaa"; 
    char *s2 = "bbbbbbbbbbbbbbbbb"; 
    //aaaaaaaaaaa是在运行时刻赋值的; 
    //而bbbbbbbbbbb是在编译时就确定的; 
    //但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
    //比如: 
    #include 
    void main() 
    { 
    char a = 1; 
    char c[] = "1234567890"; 
    char *p ="1234567890"; 
    a = c[1]; 
    a = p[1]; 
    return; 
    } 
    //对应的汇编代码 
    10: a = c[1]; 
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
    0040106A 88 4D FC mov byte ptr [ebp-4],cl 
    11: a = p[1]; 
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
    00401070 8A 42 01 mov al,byte ptr [edx+1] 
    00401073 88 45 FC mov byte ptr [ebp-4],al 
    //第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.7小结:

    堆和栈的区别可以用如下的比喻来看出:

    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。


    windows进程中的内存结构

    在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。

    接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。

    首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码:

    #include <stdio.h> 
    
    int g1=0, g2=0, g3=0; 
    
    int main() 
    { 
    static int s1=0, s2=0, s3=0; 
    int v1=0, v2=0, v3=0; 
    
    //打印出各个变量的内存地址 
    
    printf("0x%08x\n",&v1); //打印各本地变量的内存地址 
    printf("0x%08x\n",&v2); 
    printf("0x%08x\n\n",&v3); 
    printf("0x%08x\n",&g1); //打印各全局变量的内存地址 
    printf("0x%08x\n",&g2); 
    printf("0x%08x\n\n",&g3); 
    printf("0x%08x\n",&s1); //打印各静态变量的内存地址 
    printf("0x%08x\n",&s2); 
    printf("0x%08x\n\n",&s3); 
    return 0; 
    } 
    
    //编译后的执行结果是: 
    
    0x0012ff78 
    0x0012ff7c 
    0x0012ff80 
    
    0x004068d0 
    0x004068d4 
    0x004068d8 
    
    0x004068dc 
    0x004068e0 
    0x004068e4 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36


    ├———————┤低端内存区域 
    │ …… │ 
    ├———————┤ 
    │ 动态数据区 │ 
    ├———————┤ 
    │ …… │ 
    ├———————┤ 
    │ 代码区 │ 
    ├———————┤ 
    │ 静态数据区 │ 
    ├———————┤ 
    │ …… │ 
    ├———————┤高端内存区域 

    #include <stdio.h> 
    
    void __stdcall func(int param1,int param2,int param3) 
    { 
    int var1=param1; 
    int var2=param2; 
    int var3=param3; 
    printf("0x%08x\n",¶m1); //打印出各个变量的内存地址 
    printf("0x%08x\n",¶m2); 
    printf("0x%08x\n\n",¶m3); 
    printf("0x%08x\n",&var1); 
    printf("0x%08x\n",&var2); 
    printf("0x%08x\n\n",&var3); 
    return; 
    } 
    
    int main() 
    { 
    func(1,2,3); 
    return 0; 
    } 
    
    //编译后的执行结果是: 
    
    0x0012ff78 
    0x0012ff7c 
    0x0012ff80 
    
    0x0012ff68 
    0x0012ff6c 
    0x0012ff70 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    ;--------------func 函数的汇编代码------------------- 
    
    :00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间 
    :00401003 8B442410 mov eax, dword ptr [esp+10] 
    :00401007 8B4C2414 mov ecx, dword ptr [esp+14] 
    :0040100B 8B542418 mov edx, dword ptr [esp+18] 
    :0040100F 89442400 mov dword ptr [esp], eax 
    :00401013 8D442410 lea eax, dword ptr [esp+10] 
    :00401017 894C2404 mov dword ptr [esp+04], ecx 
    
    ……………………(省略若干代码) 
    
    :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间 
    :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间 
    ;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复 
    
    ;-------------------函数结束------------------------- 
    
    
    ;--------------主程序调用func函数的代码-------------- 
    
    :00401080 6A03 push 00000003 //压入参数param3 
    :00401082 6A02 push 00000002 //压入参数param2 
    :00401084 6A01 push 00000001 //压入参数param1 
    :00401086 E875FFFFFF call 00401000 //调用func函数 
    ;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C” 
    
    聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码: 
    
    #include <stdio.h> 
    #include <string.h> 
    
    void __stdcall func() 
    { 
    char lpBuff[8]="\0"; 
    strcat(lpBuff,"AAAAAAAAAAA"); 
    return; 
    } 
    
    int main() 
    { 
    func(); 
    return 0; 
    } 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44


    ├———————┤<—低端内存区域 
    │ …… │ 
    ├———————┤<—由exploit填入数据的开始 
    │ │ 
    │ buffer │<—填入无用的数据 
    │ │ 
    ├———————┤ 
    │ RET │<—指向shellcode,或NOP指令的范围 
    ├———————┤ 
    │ NOP │ 
    │ …… │<—填入的NOP指令,是RET可指向的范围 
    │ NOP │ 
    ├———————┤ 
    │ │ 
    │ shellcode │ 
    │ │ 
    ├———————┤<—由exploit填入数据的结束 
    │ …… │ 
    ├———————┤<—高端内存区域 


    windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码: 

    #include <stdio.h> 
    #include <iostream.h> 
    #include <windows.h> 
    
    void func() 
    { 
    char *buffer=new char[128]; 
    char bufflocal[128]; 
    static char buffstatic[128]; 
    printf("0x%08x\n",buffer); //打印堆中变量的内存地址 
    printf("0x%08x\n",bufflocal); //打印本地变量的内存地址 
    printf("0x%08x\n",buffstatic); //打印静态变量的内存地址 
    } 
    
    void main() 
    { 
    func(); 
    return; 
    } 
    
    程序执行结果为: 
    
    0x004107d0 
    0x0012ff04 
    0x004068c0 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数: 

    HeapAlloc  在堆中申请内存空间 
    HeapCreate  创建一个新的堆对象 
    HeapDestroy  销毁一个堆对象 
    HeapFree  释放申请的内存 
    HeapWalk  枚举堆对象的所有内存块 
    GetProcessHeap  取得进程的默认堆对象 
    GetProcessHeaps  取得进程所有的堆对象 
    LocalAlloc 
    GlobalAlloc 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间: 

    HANDLE hHeap=GetProcessHeap(); 
    char *buff=HeapAlloc(hHeap,0,8); 
     
     
    • 1
    • 2
    #pragma comment(linker,"/entry:main") //定义程序的入口 
    #include <windows.h> 
    
    _CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf 
    /*--------------------------------------------------------------------------- 
    写到这里,我们顺便来复习一下前面所讲的知识: 
    (*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。 
    由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是__stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。 
    ---------------------------------------------------------------------------*/ 
    void main() 
    { 
    HANDLE hHeap=GetProcessHeap(); 
    char *buff=HeapAlloc(hHeap,0,0x10); 
    char *buff2=HeapAlloc(hHeap,0,0x10); 
    HMODULE hMsvcrt=LoadLibrary("msvcrt.dll"); 
    printf=(void *)GetProcAddress(hMsvcrt,"printf"); 
    printf("0x%08x\n",hHeap); 
    printf("0x%08x\n",buff); 
    printf("0x%08x\n\n",buff2); 
    } 
    
    执行结果为: 
    
    0x00130000 
    0x00133100 
    0x00133118 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    #include <stdio.h> 
    
    int main() 
    { 
    int a; 
    char b; 
    int c; 
    printf("0x%08x\n",&a); 
    printf("0x%08x\n",&b); 
    printf("0x%08x\n",&c); 
    return 0; 
    } 
     
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这是用VC编译后的执行结果:

    0x0012ff7c 
    0x0012ff7b 
    0x0012ff80 
     
     
    • 1
    • 2
    • 3

    变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。

    这是用Dev-C++编译后的执行结果:

    0x0022ff7c 
    0x0022ff7b 
    0x0022ff74 
     
     
    • 1
    • 2
    • 3

    变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。

    这是用lcc编译后的执行结果:

    0x0012ff6c 
    0x0012ff6b 
    0x0012ff64 
     
     
    • 1
    • 2
    • 3

    变量在内存中的顺序:同上。

    三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。

    基础知识:

    参考:《Windows下的HEAP溢出及其利用》by: isno
    《windows核心编程》by: Jeffrey Richter

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- yrrd.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务