【转】堆栈资料整理
每个线程一个栈,每个进程一个堆。
堆、栈及静态数据区详解
五大内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。栈里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)
【转】线程堆栈
一个由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"优化成一个地方。
}
堆和栈的理论知识
略,有时间再看。。
Call Stack(调用栈)是什么?
在计算机程序当中,一个 procedure(通常译作“过程”)吃进来一些参数,干一些事情,再吐出去一个返回值(或者什么也不吐)。我们熟悉的 function、method、handler 等等其实都是 procedure。当一个 procedure A 调用另一个 procedure B 的时候,计算机其实需要干好几件事。
一. 是转移控制——计算机要暂停 A 并开始执行 B,并让 B 在执行完之后还能回到 A 继续执行。
二. 是转移数据——A 要能够传递参数给 B,并且 B 也能返回值给 A。
三. 分配和释放内存——在 B 开始执行时为它的局部变量分配内存,并在 B 返回时释放这部分内存。
举个例子吧。假设我们有这样一段求阶乘的代码:
int fact(int n) {
    int result;
    if (n <= 1)
      result = 1;
    else
      result = n * fact(n - 1);
    return result;
}
当 main() 调用了 fact(n),fact(n) 又调用了 fact(n-1),fact(n-1) 即将调用 fact(n-2) 的时候,它的 call stack 差不多是这样:(具体情况大同小异,和编译器优化有关。)

其中每个 procedure 分配的内存区域叫做它的 stack frame(通常译作“栈帧”,类似于电影《盗梦空间》中的“梦境”)。这也就解释了为什么当我们分析递归函数调用的空间复杂度时,既需要考虑 recursion tree 的深度,也需要考虑每层所分配的局部变量的大小。
对于上述 fact() 函数,它的 recursion tree 的深度是 n,这就意味着总共有 n 个 stack frame。每个 stack frame 里面除了保存 return address 和一些寄存器的值之外,还需要保存参数 n 和局部变量 result,它们都是 O(1) 的。所以 fact() 总的空间复杂度是 O(n) 的。
希望同学们能够通过了解 call stack 进一步理解空间复杂度的计算,在面试的时候一通百通。
看上面的内容,好像明白了为什么递归需要考虑栈溢出了!!!
关于函数调用栈(call stack)的个人理解
call-stack:函数调用栈
stack:栈(一种线性数据结构,先入后出FILO)
stack Frame:栈帧,调用栈call-stack里面包含很多的栈帧。
heap:堆, 一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。堆是用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。
static:全局区, 全局变量和静态变量存放在此。
常量: 常量字符串放在此,程序结束后由系统释放。
代码: 存放函数体的二进制代码。
函数调用栈形象图解:

局部变量与全局变量。
函数中出现的变量可以分为局部变量和全局变量,在函数内部定义的变量(没有global语句)就是局部变量,只有在函数内部才能够使用它们。在函数外定义的变量就是全局变量。全局变量的作用是增加了函数间数据联系的渠道,全局变量在全部执行过程中都占用存储单元,如果在同一个源文件中,局部变量和全局变量同名,则在局部变量的作用范围内全局变量被屏蔽即它不起作用。(但一般的,为了便于编写程序并减少程序出错的概率,我们不推荐使用全局变量。)
静态局部变量,有时希望局部变量的值在函数调用结束后不消失而保持原值,即其占用的存储空间不释放,在下一次函数调用时,该变量已有值,即上次函数调用结束时的值,就应该指定该局部变量为"静态局部变量",用static声明。
静态局部变量属于静态存储类别,在静态存储区分配内存单元,在程序整个运行期间都不释放,动态局部变量属于动态存储类别,站动态存储区,函数调用结束即释放。静态局部变量的赋值是在编译阶段,即只赋值一次,在程序运行时它已有初值,以后每次调用函数不再重新赋值而是保留上次函数调用结束的值,而对动态局部变量不是在编译时期进行的,而是在函数调用时进行的,每调用一次函数就重新给一次赋值。
function call的整个过程:
简单地,我们可以认为:调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果。
这个过程就发生在编译阶段。在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
执行一条指令时,是根据PC中存放的指令地址,将指令由内存取到指令寄存器IR中。程序在执行时按顺序依次执行每一条语句,PC通过加1来指向下一条将要执行的程序语句。(但也有一些例外:(1)调用函数 (2)函数调用后的返回 (3)控制结构(if else while for等))

发生函数调用时,程序会跳转到被调函数的第一条语句,然后按顺序依次执行被调函数中的语句。函数调用后返回时,程序会返回到主调函数中调用函数的语句的后一条语句继续执行。换句话说,也就是“从哪里离开,就回到哪里”。

CPU执行程序时,并不知道整个程序的执行步骤是怎样的,完全是“走一步,看一步”。CPU都是根据PC中存放的指令地址找到要执行的语句。函数返回时,是“从哪里离开,就回到哪里”。但是当函数要从被调函数中返回时,PC怎么知道调用时是从哪里离开的呢?答案就是——将函数的“返回地址”保存起来。
函数调用的特点是:越早被调用的函数,越晚返回。比如fun1函数比fun2函数先调用,但是返回的时候fun1晚于fun2返回。这一特点正是"后进先出",所以我们采用栈来保存返回地址 。

如上图调用过程(1)发生时,需要压入保存返回地址A,栈的状态如图中(a)所示;调用过程(2)发生时,需要压入保存返回地址B,栈的状态如图中(b)所示;返回过程(3)发生时,需要弹出返回地址B,栈的状态如图中©所示;调用过程过程(4)发生时,需要压入保存返回地址C,栈的状态如图中(d)所示;返回过程(5)发生时,需要弹出返回地址C,栈的状态如图中(e)所示;返回过程(6)发生时,需要弹出返回地址A,此时栈被清空,图中未画出具体情况。
局部变量的调用是和栈的操作模式“后进先出”的形式是相同的。这就是为什么返回地址是压入栈里,同样的,局部变量也会压到相对应的栈里面。当函数执行时,这个函数的每一个局部变量就会在栈里有一个空间。在栈中存放此函数的局部变量和返回地址的这一块区域叫做此函数的栈帧(frame)。当此函数结束时,这一块栈帧就会被弹出。

总结
(1)一个函数调用过程就是将数据(包括参数和返回值)和控制信息(返回地址等)从一个函数传递到另一个函数。
(2)在执行被调函数的过程中,还要为被调函数的局部变量分配空间,在函数返回时释放这些空间。这些工作都是由栈来完成的。所传参数的地址可以简单的从FP算出来。
C语言的函数调用过程(栈帧的创建于销毁)
涉及到的寄存器
(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
(3)eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
(4)ebx 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
(5)ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
(6)edx 则总是被用来放整数除法产生的余数。
(7)esi/edi分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
涉及到的汇编
mov :数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的)
sub:减法指令
lea:取偏移地址
push:实现压入操作的指令是PUSH指令
pop:实现弹出操作的指令
call:用于保存当前指令的下一条指令并跳转到目标函数。
内存地址空间分配

总结
如果你学了微机原理,你会想到cpu中断处理过程,是的,函数调用过程和中断处理过程一模一样。
C语言函数调用栈(一)
寄存器分配
- 
x86处理器
不同架构的CPU,寄存器名称被添加不同前缀以指示寄存器的大小。例如x86架构用字母“e(extended)”作名称前缀,指示寄存器大小为32位;x86_64架构用字母“r”作名称前缀,指示各寄存器大小为64位。
- 
EIP寄存器: EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。
这个类似于PC寄存器。
 - 
ESP寄存器: ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶 。
 - 
EBP寄存器:EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。
 
 - 
 
寄存器使用约定
程序寄存器组是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值。
寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
当然,这些工作都由编译器在幕后进行。不过在编写汇编程序时应注意遵守上述惯例。
栈帧结构
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
。。。
堆栈操作
函数调用时的具体步骤如下:
- 主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行时栈中。该操作会改变程序的栈指针。
 
注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。
- 
主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。
 - 
若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。
 - 
被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。
 - 
被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。
 - 
一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。
 - 
恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。
 - 
被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。
 - 
主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。
 
步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。
                    
                
                
            
        
浙公网安备 33010602011771号