内存分区
如图:
![](
http://niuxiaoxiang.oss-cn-beijing.aliyuncs.com/HeapAndStack-Memory.png)
Text Segment:文本区也称代码区,通常指用来存放程序执行代码的一块内存区域,区域的大小在程序运行前就已确定,并且内存区域通常属于只读
Data Segment:数据段包含静态的和全局的变量,并且这些变量初始值非0,每个进程都有他自己的数据区。比如:static修饰,const修饰,全局变量等
Bss Segment:包含静态和全局的变量,但是这些变量的初值是0或者没有赋初值。执行之前默认都会赋值为0
代码示例
以下代码编译后都在Text Segment区域:
int value1; // bss segment
int value2 = 1; // data segment
static int value3; // bss segment
static int value4 = 12; // data segment
const int value5; // bss segment
const int value6 = 15; // data segment
void myFunction()
{
static int someLocalValue = 1; // data segment
static int someLocalV1; // bss segment
}
堆栈
栈区(Stack):
由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆
堆区(Heap):
用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收
栈:局部变量的值、参数的值、函数返回地址、指针的引用等;自动分配系统释放
堆:自己动态内存分配区域,mallocnewnew []alloc创建的等;自己分配自己释放
代码示例:
int myFunction()
{
char *pBuffer; // 栈上创建一个指针的引用
bool b = true; // 栈上创建
if ( b )
{
char buffer[100]; // 创建100字节在栈上
pBuffer = new char[100]; // 在堆上创建100字节
} // buffer在这自动释放,pBuffer没有释放
} // b释放,pBuffer没有释放,因为没有调用delete [],有内存泄露
void myMethod()
{
int i = 4; // 栈上
int y = 2; // 栈上
class1 cls1 = new class1(); // cls1指针在栈上,指针指向的内存在堆上分配
}
堆:
- 内存不连续,有一个链表记录空闲和已使用的内存块,堆上声明一个新的内存块时,同时需要在链表上标记为已使用。
- 当创建和释放很多小的内存块时,会导致很多不连续的空闲的小内存。这样当在堆区申请一个较大的内存块时,由于这些小内存块都不满足,导致没法使用,虽然各个小内存块之和能够满足,这样就产生了内存的碎片。
- 相邻的内存块释放后,可以合并形成更大的内存,供后面使用,减少内存碎片。
- 在应用程序启动时,系统会分配给堆一个有低地址到高地址的内存空间,一个程序通常只有一个堆区,在运行过程中如果内存不够时,堆区会动态向系统申请更多的内存。
- 堆中创建的内存区域需要自己释放,否则会有内存泄露,在程序退出时整个堆区会全部释放。
- 不同线程可以同时访问同一堆内存块,所以堆区中需要对不同线程共同访问同一块内存做处理
栈:
- 目的:调度程序中函数的有序执行(下面会讲)
- 系统从高地址向低地址分配的内存连续的区域,后进先出
- 每个线程都会有自己独立的栈,在线程创建时会创建栈并制定栈的大小。(不存在不同线程共同访问同一内存的情况)
- 回收:线程退出时栈回收,无须手动释放,有系统自动回收
- 创建速度快(与内存机制有关,就通过2个指针移动,下面会讲)
- 会有最大的分配限制,通常栈不会太大
- 如果你需要使用的栈过大时,会导致栈溢出(大部分的程序攻击都是栈溢出,下面会讲)
栈帧
栈中存放的就是与每个函数对应的栈帧。当函数调用发生时,新的栈帧被压入栈;当函数返回时,相应的栈帧从栈中弹出。典型的栈帧结构如图A-A所示。
栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个栈帧通常都有两个指针,其中一个称为栈低指针:ebp,另一个称为栈顶指针:esp。前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。因此,在函数中访问实参和局部变量时都是以栈帧指针为基址,再加上一个偏移。对照下图可知,实参的偏移为正,局部变量的偏移为负。
典型的栈帧结构和原理:
![](
http://niuxiaoxiang.oss-cn-beijing.aliyuncs.com/HeapAndStackWithStackFrame.png)
代码示例:
汇编代码:
myExample.c编译生成的汇编代码myExample.s(gcc -S myExample.c -o myExample.s)
1 .file "example1.c"
2 .version "01.01"
3 gcc2_compiled.:
4 .text
5 .align 4
6 .globl function
7 .type function,@function
8 function:
9 pushl %ebp
10 movl %esp,%ebp
11 subl $20,%esp
12 movl 8(%ebp),%eax
13 addl 12(%ebp),%eax
14 movl 16(%ebp),%edx
15 addl %eax,%edx
16 movl %edx,-20(%ebp)
17 movl -20(%ebp),%eax
18 jmp .L1
19 .align 4
20 .L1:
21 leave
22 ret
23 .Lfe1:
24 .size function,.Lfe1-function
25 .align 426 .globl main
27 .type main,@function
28 main:
29 pushl %ebp
30 movl %esp,%ebp
31 subl $4,%esp
32 pushl $3
33 pushl $2
34 pushl $1
35 call function
36 addl $12,%esp
37 movl %eax,%eax
38 movl %eax,-4(%ebp)
39 .L2:
40 leave
41 ret
42 .Lfe2:
43 .size main,.Lfe2-main
44 .ident "GCC: (GNU) 2.7.2.3”
注释:使用不同的Gcc版本或者base汇编结果可能会不一样,但基本原理和思路都一样,本文中所有示例程序的编译运行环境为gcc 2.7.2.3以及bash 1.14.7
汇编几个相关的指令:
压栈(push):栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。
出栈(pop):栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节。
调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。
离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针)。
返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。
创建栈区(sub , %esp):将栈顶指针%esp减去指定字节数(栈顶下移),即为被调函数局部变量开辟栈空间。为立即数且通常为16的整数倍(可能大于局部变量字节总数而稍显浪费,但gcc采用该规则保证数据的严格对齐以有效运用各种优化编译技术)
赋值(mov %esp, %ebp):将主调函数的栈顶指针%esp赋给被调函数帧基指针%ebp。此时,%ebp指向被调函数新栈帧的起始地址(栈底),亦即旧%ebp入栈后的栈顶