堆栈_堆栈溢出 - CSDN
堆栈 订阅
在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。 展开全文
在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。
信息
外文名
Stack
定    义
一种数据项按序排列的数据结构
特    点
先进后出
中文名
堆栈
学    科
计算机
应    用
内存分配
堆栈简介
堆栈是一个特定的存储区或寄存器,它的一端是固定的,另一端是浮动的 [1]  。对这个存储区存入的数据,是一种特殊的数据结构。所有的数据存入或取出,只能在浮动的一端(称栈顶)进行,严格按照“先进后出”的原则存取,位于其中间的元素,必须在其栈上部(后进栈者)诸元素逐个移出后才能取出。在内存储器(随机存储器)中开辟一个区域作为堆栈,叫软件堆栈;用寄存器构成的堆栈,叫硬件堆栈。单片机应用中,堆栈是个特殊存储区,堆栈属于RAM空间的一部分,堆栈用于函数调用、中断切换时保存和恢复现场数据。堆栈中的物体具有一个特性:第一个放入堆栈中的物体总是被最后拿出来, 这个特性通常称为先进后出 (FILO—First-In/Last-Out)。 堆栈中定义了一些操作, 两个最重要的是PUSH和POP。 PUSH(入栈)操作:堆栈指针(SP)加1,然后在堆栈的顶部加入一 个元素。POP(出栈)操作相反,出栈则先将SP所指示的内部ram单元中内容送入直接地址寻址的单元中(目的位置),然后再将堆栈指针(SP)减1。这两种操作实现了数据项的插入和删除。
收起全文
精华内容
参与话题
  • 关于堆栈的讲解(我见过的最经典的)

    万次阅读 多人点赞 2016-11-16 17:31:36
    这是一篇转发的文章,我对他进行了格式化而已,原文出处不详。一、预备知识—程序的内存分配一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部...

    这是一篇转发的文章,我对他进行了格式化而已,原文出处不详。

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

    一个由c/C++编译的程序占用的内存分为以下几个部分
    1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
    4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
    5、程序代码区—存放函数体的二进制代码。

    二、例子程序

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

    //main.cpp
    int a = 0; //全局初始化区
    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放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
    }

    二、堆和栈的理论知识

    2.1申请方式

    stack:
    由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
    heap:
    需要程序员自己申请,并指明大小,在c中malloc函数
    p1 = (char *)malloc(10);
    在C++中用new运算符
    p2 = (char *)malloc(10);
    但是注意p1、p2本身是在栈中的。

    2.2 申请后系统的响应

    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
    会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    2.3 申请大小的限制

    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    2.4 申请效率的比较:

    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

    2.5 堆和栈中的存储内容

    栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

    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读取字符,显然慢了。

    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

    输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

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

    堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码:

    #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",param1); //打印出各个变量的内存地址
        printf("0x%08x\n",param2);
        printf("0x%08x\n\n",param3);
        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
    ├———————┤<—函数执行时的栈顶(ESP)、低端内存区域
    │ …… │
    ├———————┤
    │ var 1 │
    ├———————┤
    │ var 2 │
    ├———————┤
    │ var 3 │
    ├———————┤
    │ RET │
    ├———————┤<—“__cdecl”函数返回后的栈顶(ESP)
    │ parameter 1 │
    ├———————┤
    │ parameter 2 │
    ├———————┤
    │ parameter 3 │
    ├———————┤<—“__stdcall”函数返回后的栈顶(ESP)
    │ …… │
    ├———————┤<—栈底(基地址 EBP)、高端内存区域
    

    上图就是函数调用过程中堆栈的样子了。首先,三个参数以从右到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码:

    ;--------------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;
    }

    编译后执行一下回怎么样?哈,“”0x00414141”指令引用的”0x00000000”内存。该内存不能为”read”。”,“非法操作”喽!”41”就是”A”的16进制的ASCII码了,那明显就是strcat这句出的问题了。”lpBuff”的大小只有8字节,算进结尾的\0,那strcat最多只能写入7个”A”,但程序实际写入了11个”A”外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要这个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。

    ├———————┤<—低端内存区域
    │ …… │
    ├———————┤<—由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

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

    - HeapAlloc 在堆中申请内存空间
    - HeapCreate 创建一个新的堆对象
    - HeapDestroy 销毁一个堆对象
    - HeapFree 释放申请的内存
    - HeapWalk 枚举堆对象的所有内存块
    - GetProcessHeap 取得进程的默认堆对象
    - GetProcessHeaps 取得进程所有的堆对象
    - LocalAlloc
    - GlobalAlloc

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

    HANDLE hHeap=GetProcessHeap();
    char *buff=HeapAlloc(hHeap,0,8);

    其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧:

    #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

    hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。

    最后来说说内存中的数据对齐所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果:

    #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;
    }

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

    0x0012ff7c
    0x0012ff7b
    0x0012ff80

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

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

    0x0022ff7c
    0x0022ff7b
    0x0022ff74

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

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

    0x0012ff6c
    0x0012ff6b
    0x0012ff64

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

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

    基础知识:
    堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
    展开全文
  • 堆栈(其实就是栈)的概念: 看过很多关于堆和栈的帖子,但仍然一知半解。可能是智商不够用,o(╥﹏╥)o。写写自己的总结吧,怕以后忘了。 以下摘自维基百科。 堆栈(英语:stack)又称为栈或堆叠,是计算机科学中一...

    堆栈(其实就是栈)的概念:

    看过很多关于堆和栈的帖子,但仍然一知半解。可能是智商不够用,o(╥﹏╥)o。写写自己的总结吧,怕以后忘了。
    以下摘自维基百科。
    堆栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外堆栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。
    堆栈数据结构使用两种基本操作:推入(压栈,push)和弹出(弹栈,pop):
    推入:将数据放入堆栈的顶端(数组形式或串列形式),堆栈顶端top指针加一。
    弹出:将顶端数据数据输出(回传),堆栈顶端数据减一。

    堆的概念:

    (英语:Heap)是计算机科学中的一种特别的树状数据结构。若是满足以下特性,即可称为堆:“给定堆中任意节点 P 和 C,若 P 是 C 的父节点,那么 P 的值会小于等于(或大于等于) C 的值”。若父节点的值恒小于等于子节点的值,此堆称为最小堆(英语:min heap);反之,若父节点的值恒大于等于子节点的值,此堆称为最大堆(英语:max heap)。在堆中最顶端的那一个节点,称作根节点(英语:root node),根节点本身没有父节点(英语:parent node)。

    堆和栈在java中的区别

    在java中堆一般用来存储对象和数组,栈一般用来存储方法和基本类型(注:基本类型不包括string)变量。
    栈的存取速度比堆快。栈中的数据是可以共享的(解释:比如int a = 3; int b =3; 此时b的指针直接指向3的地址即可,不用重新开辟一块空间存储3的地址)

    展开全文
  • 堆栈的工作原理

    万次阅读 2016-01-25 14:16:16
    每一个使用c语言的都应该知道栈的重要性,我们能够使用C/C++语言写出诸多复杂的程序,很大功劳一部分有归于栈的实现,因为它可以帮助我们实现函数间的嵌套调用。 汇编程序的运行是不需要栈的,所以注定它函数...

    转自:http://blog.csdn.net/lee244868149/article/details/49493715

    每一个使用c语言的都应该知道栈的重要性,我们能够使用C/C++语言写出诸多复杂的程序,很大功劳一部分有归于栈的实现,因为它可以帮助我们实现函数间的嵌套调用。

    汇编程序的运行是不需要栈的,所以注定它函数的嵌套层数不会太多,一般是父函数调用子函数,然后在子函数就返回了,很少见到子函数还会调用孙子函数的情况。这是由它的语言特性决定的。因为每当汇编语言调用子函数时,就会将返回的PC地址保存在LR中, 如果子函数还要调用孙子函数,那么执行时也会将子函数的返回地址保存在LR中,这时如果要返回父函数,就需要将返回父PC的地址保存在另外一个寄存器中,比如R0中,这将占用另外一个寄存器。

    cpu的寄存器资源是很有限的,如果一个程序相对复杂,函数间有4、5层的嵌套调用,那将会占用至少4、5个寄存器资源,这是不现实的,也一般不会这样做,而且对于一种与硬件联系紧密的汇编语言来说,太复杂的逻辑关系或嵌套关系也不好实现。

    汇编语言是底层语言,它没有栈,它也不需要编写很复杂很庞大的程序,即使没有内存(SDRAM),它也能在cpu的片内内存运行以完成一些裸机硬件程序;但是C语言是高级语言,它能够编写复杂庞大的程序,所以它需要函数间的多层调用,它需要用到指针的灵活赋值等等,但是这些都有依赖于栈, 那么栈是怎么让C语言能够变得如此强大呢?


    一、栈的基本了解

    每次我们开机的时候,系统都会初始化好栈指针(SP),初始方法也很简单,在boot_load代码里我们可以看到:ldr sp, =4096   这样的语句,实际就是让SP指针指向这样的地址,但是注意,这个地址是内存中的地址,而不是cpu片内地址,内存资源相对cpu资源来说充裕多了,所以SP可以有很大的增长空间,这也是C语言可以写复杂程序的前提。

    我们知道栈在不同的系统中的增长方向是不一样的,但是栈的结构决定了它是一个先进后出的模型,所以和我们函数调用的过程是类似的,最先调用的函数总是最后返回,而最后调用的函数则是最最先返回,也就后调用先返回。

    栈的出栈方式决定函数的返回过程,栈的增长空间支持函数嵌套的复杂程度。


    二、栈的基本原理

    下面是收集的基于ARM平台的一个例子

    C语言进行函数调用的时候,常常会传递给被调用的函数一些参数,对于这些C语言级别的参数,被编译器翻译成汇编语言的时候,
    就要找个地方存放一下,并且让被调用的函数能够访问,否则就没发实现传递参数了。对于找个地方放一下,分两种情况。
        一种情况是,本身传递的参数就很少,就可以通过寄存器传送参数,因为在前面的保存现场的动作中,已经保存好了对应的寄存器的值,那么此时,这些寄存器就是空闲的,可以供我们使用的了,那就可以放参数,而参数少的情况下,就足够存放参数了,比如参数有2个,那么就用r0和r1存放即可。(关于参数1和参数2,具体哪个放在r0,哪个放在r1,就是和APCS中的“在函数调用之间传递/返回参数”相关了,APCS中会有详细的约定。感兴趣的自己去研究)
         

    但是如果参数太多,寄存器不够用,那么就得把多余的参数堆栈中了,即可以用堆栈来传递所有的或寄存器放不下的那些多余的参数。


    举例分析C语言函数调用是如何使用堆栈的
        对于上面的解释的堆栈的作用显得有些抽象,此处再用例子来简单说明一下,就容易明白了:
        用:
            arm-inux-objdump –d u-boot > dump_u-boot.txt
        
        可以得到dump_u-boot.txt文件。该文件就是中,包含了u-boot中的程序的可执行的汇编代码,
        其中我们可以看到C语言的函数的源代码,到底对应着那些汇编代码。
        
        下面贴出两个函数的汇编代码,
        一个是clock_init,
        另一个是与clock_init在同一C源文件中的,另外一个函数CopyCode2Ram:
        
            33d0091c <CopyCode2Ram>:
            33d0091c:  e92d4070   push   {r4, r5, r6, lr}
            33d00920:  e1a06000   mov r6, r0
            33d00924:  e1a05001   mov r5, r1
            33d00928:  e1a04002   mov r4, r2
            33d0092c:  ebffffef   bl  33d008f0 <bBootFrmNORFlash>
            ... ...
            33d00984:  ebffff14   bl  33d005dc <nand_read_ll>
            ... ...
            33d009a8:  e3a00000   mov r0, #0 ; 0x0
            33d009ac:  e8bd8070   pop {r4, r5, r6, pc}


            33d009b0 <clock_init>:
            33d009b0:  e3a02313   mov r2, #1275068416   ; 0x4c000000
            33d009b4:  e3a03005   mov r3, #5 ; 0x5
            33d009b8:  e5823014   str r3, [r2, #20]
            ... ...
            33d009f8:  e1a0f00e   mov pc, lr
        
        
        (1)clock_init部分的代码
            可以看到该函数第一行:
                33d009b0:  e3a02313   mov r2, #1275068416   ; 0x4c000000
            就没有我们所期望的push指令,没有去将一些寄存器的值放到堆栈中。这是因为,我们clock_init这部分的内容,
            所用到的r2,r3等等寄存器,和前面调用clock_init之前所用到的寄存器r0,没有冲突,所以此处可以不用push去保存这类寄存器的值,
            不过有个寄存器要注意,那就是r14,即lr,其是在前面调用clock_init的时候,用的是bl指令,所以会自动把跳转时候的pc的值赋值给lr,
            所以也不需要push指令去将PC的值保存到堆栈中。
            而clock_init的代码的最后一行:
                33d009f8: e1a0f00e mov pc, lr
            
            就是我们常见的mov pc, lr,把lr的值,即之前保存的函数调用时候的PC值,赋值给现在的PC,
            这样就实现了函数的正确的返回,即返回到了函数调用时候下一个指令的位置。
            这样CPU就可以继续执行原先函数内剩下那部分的代码了。
        
        (2)CopyCode2Ram部分的代码
            其第一行:
                33d0091c: e92d4070 push {r4, r5, r6, lr}
            
            就是我们所期望的,用push指令,保存了r4,r5,r以及lr。
            用push去保存r4,r5,r6,那是因为所谓的保存现场,以后后续函数返回时候再恢复现场,
            
            而用push去保存lr,那是因为此函数里面,还有其他函数调用:
            
                33d0092c:  ebffffef   bl  33d008f0 <bBootFrmNORFlash>
                ... ...
                33d00984:  ebffff14   bl  33d005dc <nand_read_ll>
                ... ...
            
            也用到了bl指令,会改变我们最开始进入clock_init时候的lr的值,所以我们要用push也暂时保存起来。
            而对应地,CopyCode2Ram的最后一行:
                33d009ac: e8bd8070 pop {r4, r5, r6, pc}
            就是把之前push的值,给pop出来,还给对应的寄存器,其中最后一个是将开始push的lr的值,pop出来给赋给PC,因为实现了函数的返回。
            另外,我们注意到,在CopyCode2Ram的倒数第二行是:
                33d009a8: e3a00000 mov r0, #0 ; 0x0
             
            
            是把0赋值给r0寄存器,这个就是我们所谓返回值的传递,是通过r0寄存器的。
            此处的返回值是0,也对应着C语言的源码中的“return 0”.
            
            
        对于使用哪个寄存器来传递返回值:
        当然你也可以用其他暂时空闲没有用到的寄存器来传递返回值,但是这些处理方式,本身是根据ARM的APCS的寄存器的使用的约定而设计的,
        最好不要随便改变使用方式,最好还是按照其约定的来处理,这样程序更加符合规范。



    下面是收集的x86平台的一个例子(个人觉得讲的很好)

        1)本文讨论的编译环境是 Visual C/C++,由于高级语言的堆栈工作机制大致相同,因此对其他编译环境或高级语言如C#也有意义。

        2)本文讨论的堆栈,是指程序为每个线程分配的默认堆栈,用以支持程序的运行,而不是指程序员为了实现算法而自己定义的堆栈。

        3)  本文讨论的平台为intel x86。

        4)本文的主要部分将尽量避免涉及到汇编的知识,在本文最后可选章节,给出前面章节的反编译代码和注释。

        5)结构化异常处理也是通过堆栈来实现的(当你使用try…catch语句时,使用的就是c++对windows结构化异常处理的扩展),但是关于结构化异常处理的主题太复杂了,本文将不会涉及到。

    document_thumb_thumb[4]从一些基本的知识和概念开始

        1) 程序的堆栈是由处理器直接支持的。在intel x86的系统中,堆栈在内存中是从高地址向低地址扩展(这和自定义的堆栈从低地址向高地址扩展不同),如下图所示:

    image

        因此,栈顶地址是不断减小的,越后入栈的数据,所处的地址也就越低。

        2) 在32位系统中,堆栈每个数据单元的大小为4字节。小于等于4字节的数据,比如字节、字、双字和布尔型,在堆栈中都是占4个字节的;大于4字节的数据在堆栈中占4字节整数倍的空间。

        3) 和堆栈的操作相关的两个寄存器是EBP寄存器和ESP寄存器的,本文中,你只需要把EBP和ESP理解成2个指针就可以了。ESP寄存器总是指向堆栈的栈顶,执行PUSH命令向堆栈压入数据时,ESP减4,然后把数据拷贝到ESP指向的地址;执行POP命令时,首先把ESP指向的数据拷贝到内存地址/寄存器中,然后ESP加4。EBP寄存器是用于访问堆栈中的数据的,它指向堆栈中间的某个位置(具体位置后文会具体讲解),函数的参数地址比EBP的值高,而函数的局部变量地址比EBP的值低,因此参数或局部变量总是通过EBP加减一定的偏移地址来访问的,比如,要访问函数的第一个参数为EBP+8。

        4) 堆栈中到底存储了什么数据? 包括了:函数的参数,函数的局部变量,寄存器的值(用以恢复寄存器),函数的返回地址以及用于结构化异常处理的数据(当函数中有try…catch语句时才有,本文不讨论)。这些数据是按照一定的顺序组织在一起的,我们称之为一个堆栈帧(Stack Frame)。一个堆栈帧对应一次函数的调用。在函数开始时,对应的堆栈帧已经完整地建立了(所有的局部变量在函数帧建立时就已经分配好空间了,而不是随着函数的执行而不断创建和销毁的);在函数退出时,整个函数帧将被销毁。

        5) 在文中,我们把函数的调用者称为caller(调用者),被调用的函数称为callee(被调用者)。之所以引入这个概念,是因为一个函数帧的建立和清理,有些工作是由Caller完成的,有些则是由Callee完成的。

    document_thumb_thumb4开始讨论堆栈是如何工作的

        我们来讨论堆栈的工作机制。堆栈是用来支持函数的调用和执行的,因此,我们下面将通过一组函数调用的例子来讲解,看下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    int foo1(int m, int n)
    {
        int p=m*n;
        return p;
    }
    int foo(int a, int b)
    {
        int c=a+1;
        int d=b+1;
        int e=foo1(c,d);
        return e;
    }
     
    int main()
    {
        int result=foo(3,4);
        return 0;
    }

        这段代码本身并没有实际的意义,我们只是用它来跟踪堆栈。下面的章节我们来跟踪堆栈的建立,堆栈的使用和堆栈的销毁。

    document_thumb_thumb4堆栈的建立

        我们从main函数执行的第一行代码,即int result=foo(3,4); 开始跟踪。这时main以及之前的函数对应的堆栈帧已经存在在堆栈中了,如下图所示:

    image

    图1

        参数入栈 

       当foo函数被调用,首先,caller(此时caller为main函数)把foo函数的两个参数:a=3,b=4压入堆栈。参数入栈的顺序是由函数的调用约定(Calling Convention)决定的,我们将在后面一个专门的章节来讲解调用约定。一般来说,参数都是从右往左入栈的,因此,b=4先压入堆栈,a=3后压入,如图:

    image

    图2

       返回地址入栈

        我们知道,当函数结束时,代码要返回到上一层函数继续执行,那么,函数如何知道该返回到哪个函数的什么位置执行呢?函数被调用时,会自动把下一条指令的地址压入堆栈,函数结束时,从堆栈读取这个地址,就可以跳转到该指令执行了。如果当前"call foo"指令的地址是0x00171482,由于call指令占5个字节,那么下一个指令的地址为0x00171487,0x00171487将被压入堆栈:

    image

    图3

        代码跳转到被调用函数执行

        返回地址入栈后,代码跳转到被调用函数foo中执行。到目前为止,堆栈帧的前一部分,是由caller构建的;而在此之后,堆栈帧的其他部分是由callee来构建。

       EBP指针入栈

        在foo函数中,首先将EBP寄存器的值压入堆栈。因为此时EBP寄存器的值还是用于main函数的,用来访问main函数的参数和局部变量的,因此需要将它暂存在堆栈中,在foo函数退出时恢复。同时,给EBP赋于新值。

        1)将EBP压入堆栈

        2)把ESP的值赋给EBP

    image

    图4

        这样一来,我们很容易发现当前EBP寄存器指向的堆栈地址就是EBP先前值的地址,你还会发现发现,EBP+4的地址就是函数返回值的地址,EBP+8就是函数的第一个参数的地址(第一个参数地址并不一定是EBP+8,后文中将讲到)。因此,通过EBP很容易查找函数是被谁调用的或者访问函数的参数(或局部变量)。 

        为局部变量分配地址

        接着,foo函数将为局部变量分配地址。程序并不是将局部变量一个个压入堆栈的,而是将ESP减去某个值,直接为所有的局部变量分配空间,比如在foo函数中有ESP=ESP-0x00E4,(根据烛秋兄在其他编译环境上的测试,也可能使用push命令分配地址,本质上并没有差别,特此说明)如图所示:

    image

    图5

         奇怪的是,在debug模式下,编译器为局部变量分配的空间远远大于实际所需,而且局部变量之间的地址不是连续的(据我观察,总是间隔8个字节)如下图所示:

     image

    图6

        我还不知道编译器为什么这么设计,或许是为了在堆栈中插入调试数据,不过这无碍我们今天的讨论。

    通用寄存器入栈

         最后,将函数中使用到的通用寄存器入栈,暂存起来,以便函数结束时恢复。在foo函数中用到的通用寄存器是EBX,ESI,EDI,将它们压入堆栈,如图所示:

    image

    图7

       至此,一个完整的堆栈帧建立起来了。

    document_thumb_thumb4堆栈特性分析

       上一节中,一个完整的堆栈帧已经建立起来,现在函数可以开始正式执行代码了。本节我们对堆栈的特性进行分析,有助于了解函数与堆栈帧的依赖关系。

       1)一个完整的堆栈帧建立起来后,在函数执行的整个生命周期中,它的结构和大小都是保持不变的;不论函数在什么时候被谁调用,它对应的堆栈帧的结构也是一定的。

       2)在A函数中调用B函数,对应的,是在A函数对应的堆栈帧“下方”建立B函数的堆栈帧。例如在foo函数中调用foo1函数,foo1函数的堆栈帧将在foo函数的堆栈帧下方建立。如下图所示:

    image

    图8 

      3)函数用EBP寄存器来访问参数和局部变量。我们知道,参数的地址总是比EBP的值高,而局部变量的地址总是比EBP的值低。而在特定的堆栈帧中,每个参数或局部变量相对于EBP的地址偏移总是固定的。因此函数对参数和局部变量的的访问是通过EBP加上某个偏移量来访问的。比如,在foo函数中,EBP+8为第一个参数的地址,EBP-8为第一个局部变量的地址。

       4)如果仔细思考,我们很容易发现EBP寄存器还有一个非常重要的特性,请看下图中:

    image

    图9

       我们发现,EBP寄存器总是指向先前的EBP,而先前的EBP又指向先前的先前的EBP,这样就在堆栈中形成了一个链表!这个特性有什么用呢,我们知道EBP+4地址存储了函数的返回地址,通过该地址我们可以知道当前函数的上一级函数(通过在符号文件中查找距该函数返回地址最近的函数地址,该函数即当前函数的上一级函数),以此类推,我们就可以知道当前线程整个的函数调用顺序。事实上,调试器正是这么做的,这也就是为什么调试时我们查看函数调用顺序时总是说“查看堆栈”了。

    document_thumb_thumb4返回值是如何传递的

        堆栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层函数,这是怎么做的呢?

        首先,caller和callee在这个问题上要有一个“约定”,由于caller是不知道callee内部是如何执行的,因此caller需要从callee的函数声明就可以知道应该从什么地方取得返回值。同样的,callee不能随便把返回值放在某个寄存器或者内存中而指望Caller能够正确地获得的,它应该根据函数的声明,按照“约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”:  
        1)首先,如果返回值等于4字节,函数将把返回值赋予EAX寄存器,通过EAX寄存器返回。例如返回值是字节、字、双字、布尔型、指针等类型,都通过EAX寄存器返回。

        2)如果返回值等于8字节,函数将把返回值赋予EAX和EDX寄存器,通过EAX和EDX寄存器返回,EDX存储高位4字节,EAX存储低位4字节。例如返回值类型为__int64或者8字节的结构体通过EAX和EDX返回。

        3)  如果返回值为double或float型,函数将把返回值赋予浮点寄存器,通过浮点寄存器返回。

        4)如果返回值是一个大于8字节的数据,将如何传递返回值呢?这是一个比较麻烦的问题,我们将详细讲解:

            我们修改foo函数的定义如下并将它的代码做适当的修改:

    1
    2
    3
    4
    MyStruct foo(int a, int b)
    {
    ...
    }
             MyStruct定义为:
    1
    2
    3
    4
    5
    6
    struct MyStruct
    {
        int value1;
        __int64 value2;
        bool value3;
    };

         这时,在调用foo函数时参数的入栈过程会有所不同,如下图所示:

    image

    图10

        caller会在压入最左边的参数后,再压入一个指针,我们姑且叫它ReturnValuePointer,ReturnValuePointer指向caller局部变量区的一块未命名的地址,这块地址将用来存储callee的返回值。函数返回时,callee把返回值拷贝到ReturnValuePointer指向的地址中,然后把ReturnValuePointer的地址赋予EAX寄存器。函数返回后,caller通过EAX寄存器找到ReturnValuePointer,然后通过ReturnValuePointer找到返回值,最后,caller把返回值拷贝到负责接收的局部变量上(如果接收返回值的话)。

        你或许会有这样的疑问,函数返回后,对应的堆栈帧已经被销毁,而ReturnValuePointer是在该堆栈帧中,不也应该被销毁了吗?对的,堆栈帧是被销毁了,但是程序不会自动清理其中的值,因此ReturnValuePointer中的值还是有效的。

    document_thumb_thumb4堆栈帧的销毁

        当函数将返回值赋予某些寄存器或者拷贝到堆栈的某个地方后,函数开始清理堆栈帧,准备退出。堆栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)

       1)如果有对象存储在堆栈帧中,对象的析构函数会被函数调用。

        2)从堆栈中弹出先前的通用寄存器的值,恢复通用寄存器。

        3)ESP加上某个值,回收局部变量的地址空间(加上的值和堆栈帧建立时分配给局部变量的地址大小相同)。

        4)从堆栈中弹出先前的EBP寄存器的值,恢复EBP寄存器。

        5)从堆栈中弹出函数的返回地址,准备跳转到函数的返回地址处继续执行。

        6)ESP加上某个值,回收所有的参数地址。

        前面1-5条都是由callee完成的。而第6条,参数地址的回收,是由caller或者callee完成是由函数使用的调用约定(calling convention )来决定的。下面的小节我们就来讲解函数的调用约定。

    document_thumb_thumb4函数的调用约定(calling convention)

        函数的调用约定(calling convention)指的是进入函数时,函数的参数是以什么顺序压入堆栈的,函数退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有2个办法可以指定函数使用的调用约定:

        1)在函数定义时加上修饰符来指定,如

    1
    2
    3
    4
    void __thiscall mymethod();
    {
        ...
    }
        2)在VS工程设置中为工程中定义的所有的函数指定默认的调用约定:在工程的主菜单打开Project|Project Property|Configuration Properties|C/C++|Advanced|Calling Convention,选择调用约定(注意:这种做法对类成员函数无效)。

        常用的调用约定有以下3种:

        1)__cdecl。这是VC编译器默认的调用约定。其规则是:参数从右向左压入堆栈,函数退出时由caller清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如printf方法。由于callee不知道caller到底将多少参数压入堆栈,因此callee就没有办法自己清理堆栈,所以只有函数退出之后,由caller清理堆栈,因为caller总是知道自己传入了多少参数。

        2)__stdcall。所有的Windows API都使用__stdcall。其规则是:参数从右向左压入堆栈,函数退出时由callee自己清理堆栈中的参数。由于参数是由callee自己清理的,所以__stdcall不支持可变数量的参数。

        3) __thiscall。类成员函数默认使用的调用约定。其规则是:参数从右向左压入堆栈,x86构架下this指针通过ECX寄存器传递,函数退出时由callee清理堆栈中的参数,x86构架下this指针通过ECX寄存器传递。同样不支持可变数量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么,将采用__cdecl或者__stdcall的规则来压栈和出栈,而this指针将作为函数的第一个参数最后压入堆栈,而不是使用ECX寄存器来传递了。

    document_thumb_thumb4反编译代码的跟踪(不熟悉汇编可跳过)

        以下代码为和foo函数对应的堆栈帧建立相关的代码的反编译代码,我将逐行给出注释,可对照前文中对堆栈的描述:

        main函数中 int result=foo(3,4); 的反汇编:

    1
    2
    3
    4
    5
    008A147E  push        4                     //b=4 压入堆栈  
    008A1480  push        3                     //a=3 压入堆栈,到达图2的状态
    008A1482  call        foo (8A10F5h)         //函数返回值入栈,转入foo中执行,到达图3的状态
    008A1487  add         esp,8                 //foo返回,由于采用__cdecl,由Caller清理参数
    008A148A  mov         dword ptr [result],eax //返回值保存在EAX中,把EAX赋予result变量

        下面是foo函数代码正式执行前和执行后的反汇编代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    008A13F0  push        ebp                  //把ebp压入堆栈
    008A13F1  mov         ebp,esp              //ebp指向先前的ebp,到达图4的状态
    008A13F3  sub         esp,0E4h             //为局部变量分配0E4字节的空间,到达图5的状态
    008A13F9  push        ebx                  //压入EBX
    008A13FA  push        esi                  //压入ESI
    008A13FB  push        edi                  //压入EDI,到达图7的状态
    008A13FC  lea         edi,[ebp-0E4h]       //以下4行把局部变量区初始化为每个字节都等于cch
    008A1402  mov         ecx,39h
    008A1407  mov         eax,0CCCCCCCCh
    008A140C  rep stos    dword ptr es:[edi]
    ......                                      //省略代码执行N行
    ......
    008A1436  pop         edi                   //恢复EDI 
    008A1437  pop         esi                   //恢复ESI
    008A1438  pop         ebx                   //恢复EBX
    008A1439  add         esp,0E4h              //回收局部变量地址空间
    008A143F  cmp         ebp,esp               //以下3行为Runtime Checking,检查ESP和EBP是否一致  
    008A1441  call        @ILT+330(__RTC_CheckEsp) (8A114Fh)
    008A1446  mov         esp,ebp
    008A1448  pop         ebp                   //恢复EBP
    008A1449  ret                               //弹出函数返回地址,跳转到函数返回地址执行                                            //(__cdecl调用约定,Callee未清理参数)

    document_thumb_thumb4[1]参考

    Debug Tutorial Part 2: The Stack

    Intel汇编语言程序设计(第四版) 第8章

    http://msdn.microsoft.com/zh-cn/library/46t77ak2(VS.80).aspx

    http://www.360doc.com/content/10/1126/23/3267996_72551321.shtml
    http://www.cnblogs.com/dwlsxj/p/Stack.html
    展开全文
  • 堆栈、堆、方法区介绍

    万次阅读 多人点赞 2017-03-10 10:37:06
    堆栈、堆、方法区介绍终于开始看java啦…不知道有没有很多人跟我一样想法,先把安卓看完了再去看java,因为安卓直接跟工资挂钩而java更多的是内功.直到前段时间我和我们这边后台大佬对接开发websocket噩梦就开始了,这...

    堆栈、堆、方法区介绍

    终于开始看java啦…不知道有没有很多人跟我一样想法,先把安卓看完了再去看java,因为安卓直接跟工资挂钩而java更多的是内功.直到前段时间我和我们这边后台大佬对接开发websocket噩梦就开始了,这个线程不安全,这个单例在某些情况下有问题,这里应该这样封装等等…..每天围绕在我脑边,本来就是第一次做websocket,在加上后台大佬java大法猛如虎,真是感觉自己啥也不会了.

    …不过现在是熬过来啦而且以后我可以说我做过基于websocket协议的im啦啦啦啦,容我嘚瑟一波哈哈哈哈( 这里需要感谢后台大佬刚哥带我把im搞了一遍:) )

    这里也是预告我后面会写websocket的blog总结的,还往大家捧场.扯远了,回到主题正是因为这次经历让我意识到java的重要,重要的事再说一遍我要开始学java了.后面会陆续更新这个系列.觉得写得好的不好的都给点个赞蛤,感谢!

    预备知识

    • java数据类型:

      1. 基础数据类型:boolean、byte、short、char、int、long、float、double

      2. 引用数据类型:类、接口、数组

    堆栈、堆、方法区

    JAVA的JVM的内存可分为3个区:堆(heap)、堆栈(stack)和方法区(method)

    • 堆区:

      1. 提供所有类实例和数组对象存储区域

      2. jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

    • 栈区:

      1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中

      2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

    • 方法区:

      1. 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

      2. 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

      3. 运行时常量池都分配在 Java 虚拟机的方法区之中

    栗子

    
    public void test(){
      A a = new A();
      int b = 1;
    }
    
    public class A{
    
    }

    这个栗子中

    • a是A对象的引用所以放在栈中,A是自定义对象所以放在堆中.

    • b是基础数据类型所以在栈中.

    小结

    这里用一个在别处看到的栗子总结再好不过

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

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

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

    第一次blog写这么短有点方.如果有错误的地方还望大佬们指出…小弟感激涕零

    展开全文
  • 关于堆栈

    千次阅读 2019-09-05 17:15:07
    写这篇博客的时候,我总会想到之前面试被怼的经历。 面试官最后总结出来一句话:你总是知其然不知其所以然!太浮于表面了 确实,了解一项知识之前首先应该明确: 1.它是什么?...堆栈到底是什么? ...
  • 堆栈介绍

    2019-02-22 14:39:34
    堆栈介绍 一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) — 一般由程序员...
  • 堆栈-操作系统(一)

    千次阅读 2019-04-01 18:32:44
    很早就想写一篇文章,关于操作系统中的堆栈。 说是堆栈,其实就是操作系统如何处理代码的,即如何为代码中的各种类型的变量、常量、方法体(函数体)等等分配内存空间。 了解这些内容,可以帮助我们做一些判断: ...
  • 【汇编】堆栈和画堆栈

    万次阅读 多人点赞 2018-08-09 13:53:09
    根据咱们学汇编的经验呀,汇编用的最多的是寄存器和内存之间的不断相互传值传地址,井然有序。  然而,你知道它们具体是怎么进行数据传递和交换的吗?    我们知道寄存器能够保存的数据量不多,所以需要存储...
  • 堆栈

    千次阅读 多人点赞 2016-03-06 16:51:04
    九曲迷宫,也不过是修在...堆栈一般分为”专用堆栈存储器“和“软件堆栈” 1、专用堆栈存储器:就是专门设计的硬件存储器 2、软件堆栈:程序员在内存中划一块出来,当做堆栈使用(8088、8086) 堆栈的结构 8086、
  • 浅谈C/C++堆栈指引——C/C++堆栈

    千次阅读 2015-07-07 17:07:36
    C/C++堆栈指引 Binhua Liu document_thumb_thumb前言 我们经常会讨论这样的问题:什么时候数据存储在飞鸽传书堆栈(Stack)中,什么时候数据存储在堆(Heap)中。我们知道,局部变量是存储在堆栈中的;debug时,查看...
  • JavaScript执行堆栈

    万次阅读 2019-05-10 08:01:59
    我们首先看JavaScript的函数底层工作原理 一个函数运行的信息被存储在它的执行上下文里。...与它关联的执行上下文被一个叫做执行上下文堆栈的特殊数据结构保存; 执行嵌套调用; 嵌套调用结束后...
  • 堆栈溢出

    千次阅读 2019-03-26 22:10:13
    堆栈:是一个在计算机科学中经常使用的抽象数据类型,堆栈是一块保存数据的连续内存。 一个名为堆栈指针(SP)的寄存器指向堆栈的顶部,堆栈的底部在一个固定的地址。 堆栈中的物体具有一个特性: 最后一个放入堆栈中的...
  • 浅谈STM32堆栈与uCOS堆栈,其实是两码事!~

    千次阅读 多人点赞 2018-02-28 16:56:26
    本博文分为3部分,分别为启动代码篇,C堆栈篇,和STM32堆栈和uCOS堆栈区别篇.***********************************************①启动代码篇***********************************************前年毕业时,去了意法半导体...
  • 数据结构的堆栈、内存中的堆栈

    千次阅读 2015-05-06 19:36:05
    内存中的堆栈和数据结构中的堆栈并非一个概念 操作系统的堆栈、数据结构的堆栈、内存的堆栈 内存的堆栈其实就是程序的堆栈,用java、c、c++举例 ==============华丽分割线==================== 数据结构中: ...
  • JAVA堆栈

    千次阅读 2019-04-30 11:25:19
    基本概念 1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在...
  • 堆栈平衡

    千次阅读 2017-11-21 16:17:38
    转自:http://www.cnblogs.com/this-543273659/archive/2012/03/01/2375652.html 先说明。原发者 iso9001  http://www.ghoffice.com/bbs/read.php?tid-35165.html 他提供的地址(当他是个指针好了:P) ...下边是我
  • #include #include #include #include int ShowEsp(int* arg1,int* arg2); /* 引言 各种面试宝典上都会说 又说栈在进程空间的高地址部分,向下扩展;...堆在进程空间的低地址部分,堆向上扩展 来验证一下是否正如所...
  • [Golang]堆栈解析

    千次阅读 2015-03-09 11:06:37
    本文为理解翻译,原文地址:http://www.goinggo.net/2015/01/stack-traces-in-go.html Introduction在Go语言中有一些调试... 本文将讨论堆栈跟踪信息以及如何在堆栈中识别函数所传递的参数。Functions先从这段代
  • 深入理解任务堆栈以及堆栈溢出

    千次阅读 2019-05-26 23:56:18
    在多任务操作系统中创建任务时,都需要指定该任务的堆栈大小,那么这个堆栈的作用时什么呢?什么情况下需要用到堆栈,以及大小不够时会产生什么异常呢? 1 任务状态 简单分为运行态,就绪态,阻塞态。 运行态:...
  • 堆栈(抽象数据类型) 有关在会计中使用术语LIFO,请参阅LIFO(会计)。对于力量训练中使用术语下推,请参阅下推(练习)。 有关其他用途,请参阅堆栈(消歧)。 使用推送和弹出操作简单表示堆栈运行时。 在...
1 2 3 4 5 ... 20
收藏数 482,031
精华内容 192,812
关键字:

堆栈