c 内存管理
第3章 内存管理及相关工具
在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。因此在程序设计中,有效地管理内存资源是程序员首先考虑的问题。
第1节主要介绍内存管理基本概念,重点介绍C程序中内存的分配,以及C语言编译后的可执行程序的存储结构和运行结构,同时还介绍了堆空间和栈空间的用途及区别。
第2节主要介绍C语言中内存分配及释放函数、函数的功能,以及如何调用这些函数申请/释放内存空间及其注意事项。
第3节主要介绍了在Linux下除GDB之外的内存调试及管理工具,包括glibc提供的内存管理函数、MemWatch内存错误检测工具和valgrind内存检测工具。重点介绍使用这三类工具进行内存检查的原理及相关示例。
3.1 内存管理基本概念
3.1.1 C程序内存分配
1.C程序结构
下面列出C语言可执行程序的基本情况(Linux 2.6环境/GCC4.0)。
[root@localhost Ctest]# ls test -l //test为一个可执行程序
-rwxr-xr-x 1 root root 4868 Mar 26 08:10 test
[root@localhost Ctest]# file test //此文件基本情况
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped
[root@localhost Ctest]# size test //此二进制可执行文件结构情况
//代码区静态数据/全局初始化数据区 未初始化数据区 十进制总和 十六进制总和 文件名
text data bss dec hex filename
906 284 4 1194 4aa test
可以看出,此可执行程序在存储时(没有调入到内存)分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。
(1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
(2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如,一个不在任何函数内的声明(全局数据):
int maxcount = 99;
使得变量maxcount根据其初始值被存储到初始化数据区中。
static mincount=100;
这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。
(3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:
long sum[1000];
将变量sum存储到未初始化数据区。
图3-1所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。
http://new.51cto.com/files/uploadimg/20081108/205745806.jpg
(1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。
代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。
(2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。
(3)未初始化数据区(BSS)。在运行时改变其值。
(4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。
(5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。
之所以分成这么多个区域,主要基于以下考虑:
一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。
临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。
全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。
堆区由用户自由分配,以便管理。
下面通过一段简单的代码来查看C程序执行时的内存分配情况。相关数据在运行时的位置如注释所述。
//main.cpp
int a = 0; //a在全局已初始化数据区
char *p1; //p1在BSS区(未初始化全局变量)
main()
{
int b; //b在栈区
char s[] = "abc"; //s为数组变量,存储在栈区,
//"abc"为字符串常量,存储在已初始化数据区
char *p1,p2; //p1、p2在栈区
char *p3 = "123456"; //123456\0在已初始化数据区,p3在栈区
static int c =0; //C为全局(静态)数据,存在于已初始化数据区
//另外,静态数据会自动初始化
p1 = (char *)malloc(10);//分配得来的10个字节的区域在堆区
p2 = (char *)malloc(20);//分配得来的20个字节的区域在堆区
free(p1);
free(p2);
}
2.内存分配方式
在C语言中,对象可以使用静态或动态的方式分配内存空间。
静态分配:编译器在处理程序源代码时分配。
动态分配:程序在执行时调用malloc库函数申请分配。
静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。
静态与动态内存分配的主要区别如下:
静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的变量,需要通过指针间接地对它进行操作。
静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数(C++中为new和delete运算符)来完成。
以下是采用静态分配方式的例子。
int a=100;
此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。
以下是采用动态分配方式的例子。
p1 = (char *)malloc(10*sizeof(int));//分配得来得10*4字节的区域在堆区
此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:
free(p1);
C程序一般分为
1.程序段(text): 程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.
2.文字常量区: 常量字符串就是放在这里的。
3.初始化过的数据(data): 在程序运行之初已经对变量进行初始化的.全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
4.未初始化过的数据(bss): 在程序运行初未对变量进行初始化的数据。
6.堆栈(stack): 存储局部,临时变量,在程序块开始时自动分配内存,结束时自动释放内存.存储函数的返回指针. 当函数被调用时,它们被压入栈;当函数返回时,它们就要被弹出堆栈。
7.堆(heap): 存储动态内存分配,需要程序员手工分配(c中malloc函数,c++中new函数),手工释放(free 和 delete 函数).
3和4称为静态存储区,6和7称为动态存储区。
windows进程中的内存结构
#include
#include
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456";// 123456\0在常量区,p3在栈上。
static int c =0;// 全局(静态)初始化区
static int uc,uc1,uc2;// 全局(静态)未初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456");// 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
printf("堆p1 \t\t\t0x%08x\n",p1);
printf("堆p2 \t\t\t0x%08x\n",p2);
printf("栈&p3 \t\t\t0x%08x\n",&p3);
printf("栈&p2 \t\t\t0x%08x\n",&p2);
printf("栈s \t\t\t0x%08x\n",s);
printf("栈&s[1] \t\t0x%08x\n",&s[1]);
printf("栈&b \t\t\t0x%08x\n",&b);
printf("main地址\t\t0x%08x\n",main);
printf("文本常量区\t\t0x%08x\n",p3);
printf("全局初始化区\t\t0x%08x\n",&a);
printf("(静态)初始化区\t0x%08x\n",&c);
printf("全局未初始化区\t\t0x%08x\n",&p1);
printf("(静态)未初始化区\t0x%08x\n",&uc);
printf("(静态)未初始化区\t0x%08x\n",&uc1);
printf("(静态)未初始化区\t0x%08x\n",&uc2);
}
[点击图片可在新窗口打开]
你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)” 是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。
├———————┤低端内存区域
│ …… │
├———————┤
│ 动态数据区 │
├———————┤
│ …… │
├———————┤
│ 代码区 │
├———————┤
│ 静态数据区 │
├———————┤
│ …… │
├———————┤高端内存区域
堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。
linux c程序存储空间布局
进程在内存中的影像.
我们假设现在有一个程序, 它的函数调用顺序如下.
main(...) ->; func_1(...) ->; func_2(...) ->; func_3(...)
即: 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3
当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.
(内存高址)
+--------------------------------------+
| ...... | ... 省略了一些我们不需要关心的区
+--------------------------------------+
| env strings (环境变量字串) | \
+--------------------------------------+ \
| argv strings (命令行字串) | \
+--------------------------------------+ \
| env pointers (环境变量指针) | SHELL的环境变量和命令行参数保存区
+--------------------------------------+ /
| argv pointers (命令行参数指针) | /
+--------------------------------------+ /
| argc (命令行参数个数) | /
+--------------------------------------+
| main 函数的栈帧 | \
+--------------------------------------+ \
| func_1 函数的栈帧 | \
+--------------------------------------+ \
| func_2 函数的栈帧 | \
+--------------------------------------+ \
| func_3 函数的栈帧 | Stack (栈)
+......................................+ /
| | /
...... /
| | /
+......................................+ /
| Heap (堆) | /
+--------------------------------------+
| Uninitialised (BSS) data | 非初始化数据(BSS)区
+--------------------------------------+
| Initialised data | 初始化数据区
+--------------------------------------+
| Text | 文本区
+--------------------------------------+
(内存低址)
这里需要说明的是:
i) 随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的. 随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地 被遗弃而向内存的高址方向回缩. 各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定.
ii) 进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分配给进程的内存数量的增加, Heap(堆)有可能向高址或低址延伸, 依赖于不 同CPU的实现. 但一般来说是向内存的高地址方向增长的. 堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk.
iii) 在BSS数据或者Stack(栈)的增长耗尽了系统分配给进程的自由内存的情况下, 进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行. (虽然和exploit没有关系, 但是知道一下还是有好处的)
iv) 函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈帧还是被调用函数栈帧, 则依赖于不同系统的实现),它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的数据, 其中包含了调用函数的下一条执行指令的地址.
v) 非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的. 初始化数据区用于存放可执行文件里的初始化数据. 这两个区统称为数据区.
vi) Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错. 文本区是被多个运行该可执行文件的进程所共享的. 文本区存放了程序的代码.
2) 函数的栈帧.
函数调用时所建立的栈帧包含了下面的信息:
i) 函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里, 取决于不同系统的实现.
&nb

浙公网安备 33010602011771号