C语言的内存管理

1、内存管理的概述

当程序被加载到内存的时候,它在内存中会大致被组织成三个部分:代码区,静态存储区和动态存储区。代码区存放的是将要执行的程序的机器语言表示,包括组成程序的各种用户自定义函数和系统调用函数。关于静态存储区和动态存储区:The word static refers to things that happen at compile time and link time when the program is constructed—as opposed to load time or run time when the program is actually started; The term dynamic refers to things that take place when a program is loaded and executed. 就是说静态存储区和动态存储区的主要的区别在于空间分配的时间不同,静态存储区的空间实在程序编译和连接的时候分配的,而动态存储区分配的空间是在程序调入和执行的时候分配的。

静态存储区中主要存放全局的(global),静态的(static)数据。其可以分为两部分,一部分用来存放已经初始化的数据,另一部分用来存放没有初始化的数据(这部分数据会被默认初始化为0,这块区域称为BSS(Block Started by Symbol)段,是用来存放程序中未初始化的全局变量的一块内存区域。)。动态存储区分为两部分,即堆(heap)和栈(stack)。堆中主要存放在程序运行过程中调用calloc和malloc函数动态分配的内存空间,当在程序中调用calloc和malloc函数来为程序分配新的空间时,堆空间便会向上增长,如图1。栈用来存放局部变量,用来传递函数的参数,并用来记录函数的返回地址(当函数执行结束,根据改地址程序可以跳转到相应的指令出继续执行)。当一个新的函数被调用,会有一个新的栈frame被加入到占空间当中,这时栈向下增长,如图1。


通常来说,堆和栈在进程的虚拟地址空间的两端(从上面的图上也可以看出来)。当被访问的时候,栈地址空间就会增长,最大可以到一个由内核设定的值(可以通过setrlimit(RLIMIT_STACK,...)进行调整)。而堆空间的增长则是由于内存分配函数(calloc和malloc)触发了brk()和sbrk()系统调用,这样会使操作系统将更多的物理内存页面映射到该进程的虚拟地址空间。

可见,堆空间和栈空间的管理是由操作系统来实现的。通常,游戏等对性能要求较高的应用会有自己的内存管理方案。(比如,一次从堆中申请大块的空间,然后内部分配,这样就避免了依赖于操作系统的内存管理。)

2、栈

2.1 介绍

在计算机体系结构中,栈是一个后进先出的数据结构。在大多数现在计算机系统中,每一个线程多会有一个保留的内存区域作为栈来使用。当函数执行的时候,它会将一些状态信息放在栈顶;当函数退出的时候,它负责将数据从栈中移除。栈的一个重要用途就是用来保存函数调用的地址,来保证return语句能够返回到正确的位置。(当然,还有其他用途。)

因为栈中的数据时后进先出的,这种方式比较简单。因此,通常栈的内存分配要比堆的内存分配(也就是常说的动态内存分配)要快。另外一个特性是,当函数退出时,它在栈中占用的内存空间将会被自动释放。如果其中的数据是不需要的,这对于编程人员来说是非常方便的。当然,如果有些数据还需要被保留,那么这些数据一定要在函数退出之前被拷贝到其他地方。因此,基于栈的内存分配,适合于存放临时数据,或者是函数退出之后不再需要的数据。

一个栈是由一些栈frame组成的。这是一种和机器相关的保存着子程序状态信息的数据结构。每一个栈frame对应着一个还没有被return语句终止的子程序。例如,一个名为DrawSquare的程序调用了一个名为DrawLine的子程序,栈顶的格局便如下:


栈比较快的原因是,它的访问方式比较简单,这样比较容易从中分配内存,而堆有着复杂得多的内存分配和释放机制。另外一方面,栈中的每一个字节都会被经常重复使用,这样,在实现上,它们可能会被映射到处理器的cache,这也是栈计较快的原因。

2.2 tip

下面是关于栈的一些tip:

(1)在栈中创建的变量在超出作用域后便会被自动释放。

(2)栈中分配的变量要比堆中要快的多。

(3)栈是用一个真正的栈数据结构实现的,用来存储局部变量,函数的返回地址,并用来参数传递。

(4)如果超量使用会导致栈溢出(比如无限递归,超大的分配等)。

(5)栈中的数据不用指针就可以访问。

(6)如果在运行之前,你知道需要多大的空间来存放数据,而且这个空间不会导致溢出,那么这时候应该使用栈。

(7)通常,在程序开始的时候,一个栈空间的最大值就已经被确定了。

(8)在C语言中,可以是用alloca实现变量长度的内存分配,这会在栈上分配空间,而不会想alloc一样在堆上分配空间。尽管这样的内存空间在return之后也会被释放,但是用来做buffer还是很有用的。

2.3 栈溢出

栈相关的内存错误是最糟糕的。如果使用堆内存分配,在越界时,可能会触发一个segment fault(当然,也不是所有时候都会这样,比如恰好两个分配的空间时连续的。)。但是,由于在栈上创建的变量相互之间都是连续的,如果在写的时候越界会改变另一个变量的值。现在,我感觉只要是我的程序不按逻辑运行了,我就知道很可能是缓冲区溢出了。

下面是一些毁掉栈的例子:

(1)利用下面的方法,我们可以用掉比该线程可用的栈空间更要多的内存空间,从而导致栈溢出。

int main(){
   main();
}
或者是
int add(int n){
   return n+add(n+1);
}
(2)另外要注意的问题是缓冲区溢出。我们如果在写的时候越界(超出了一个变量的边界),将会覆盖掉一些关键的信息。比如下面的代码:

#include<string.h>
void foo(char *bar)
{
   char c[12];
   strcpy(c,bar);//no bounds checking
}
int main(int argc, char **argv)
{
   foo(argv[1]);
}
从上面可以看到,如果从命令行传递一个大于12个字节的参数,那么foo()就会覆盖掉局部的栈数据,保存的栈frame指针,甚至是至关重要的返回地址。当foo()return的时候,它会将返回地址弹出栈,并且跳转到这个地址,并从该地址继续执行程序。从上面的例子中,可以看出攻击者用一个指向栈缓冲区char c[12]的指针覆盖了返回地址,导致在存放返回地址的内存空间中放的是攻击者提供的数据,如下图3。在实际的栈缓冲区溢出中,A将会被其它和平台及函数相关的溢出代码代替。如果这个程序有特殊的权利(比如设置了SUID位,可以作为超级用户运行),那么攻击者将利用这一薄弱环节获得感染主机的超级用户权限。


(3)向已经释放的栈空间中写入数据

char * foo()
{
   char str[256];
   return str;
}
void bar()
{
   char* str=foo();
   strcpy(str,"Holy sweet Moses! I blew my stack!!");
}

3、堆

3.1 介绍

堆包含一个使用和空闲的块空间列表。新的内存空间分配(new 或者 malloc)通过在一个空闲的块上创建一个合适的内存块来实现。这需要更新堆上的块列表。这些关于堆上块的元信息也是存在堆上,往往是在每一块前面的一小块区域。

3.2 tip

  • 堆的大小在程序启动的时候就确定了,但是也可以随着空间需要而增长(分配器会向操作系统申请更多的内存空间。)
  • 堆上的变量必须被手动销毁,它们永远不会超出作用域。它们可以用delete,delete[]或者free来进行释放。
  • 堆上的变量要比栈上的变量空间分配要慢
  • 堆上的空间是由程序决定按需分配的。
  • 当执行很多分配和释放操作之后,会产生内存碎片
  • 当申请太大的空间时,可能会分配失败。
  • 如果在运行时不知道需要多大的空间,或者是需要分配很多的空间,这个时候应该使用堆分配。
  • 堆分配会产生内存泄露。

3.3 例子

下图是该程序中的内存结构及变化。

程序开始时和foo第一次调用时:


第二次调用foo后:


非常清晰。

int x;	/* static storage */

void main(){
	int y;	/* dynamic stack storage */
	char *str;/* dynamic stack storage */

	str=malloc(100);/* allocates 100 bytes of dynamic heap storage */

	y=foo(23);
	free(str);/* deallocates 100 bytes of dynamic heap storage */
}/* y and str deallocated as stack frame is poped */

int foo(int z){ /* z is dynamic stack storage */
	char ch[100];/* ch is dynamic stack storage */

	if (z==23)
		foo(7);

	return 3;/* z and ch are deallocated as stack frame is poped,
			 3 put on the top of the stack */
}

3.4 内存泄露

3.4.1 内存泄露介绍

当一个计算机程序占用了内存,但是使用完毕却没有将其释放给操作系统时,便出现了内存泄露。内存泄露只能通过程序员检查源代码查出。内存泄露会减少可用内存的数量,从而影响程序的性能。最坏的情况下,过多的空闲内存被占用,将导致设备或者系统的全部或部分停止正常工作。

内存泄露或许不是那么严重,甚至是可以通过常规手段探测到的。在现代操作系统中,被一个程序占用的正常的内存在该程序结束时会被释放。这意味着一个短时间运行的内存泄露可能不会被注意到,也很少会引起严重的问题。

通常,内存泄露是由于动态分配的内存become unreachable。这种常见的内存泄露的bug导致了一些探测unreachable内存的debug工具的流行。比如IBM Rational Purify,BoundsChecker,Valgrind,Insure++,memwatch等C/C++内存debug工具。“保守”的垃圾回收机制也被加到缺少这类原生feature的程序语言中。C/C++程序中也有负责这类功能的库。一个保守的收集器能够发现并释放大部分unreachable内存,但不是全部。

3.4.2 内存泄露的一个例子

下面的C函数通过丢失已分配内存空间的指针故意造成了内存泄露。由于程序循环会一直调用内存分配函数malloc(),而不保存申请到的内存空间的地址,所以当程序没有可用的内存空间的时候,malloc函数一定会失败(return NULL)。由于被分配的内存地址没有被保存下来,所以不可能释放之前分配的任何内存块。实际上,操作系统会推迟内存分配直到写数据到分配的内存中,因此,程序会在虚拟地址空间(每个进程有限制,或者是在IA-32上是2到4GB,在x86-64机器上会更多)用完时终止,而不会对系统的其它部分产生影响。

#include <stdlib.h>

int main(void)
{
	/*
	this is an infinite loop calling the malloc function which 
	allocates the memory but without saving the address of the 
	allocates place
	*/
	while (malloc(50));/* malloc will return NULL sooner or later, due to lack of memory. */
	return 0;/* free the allocated memory by operating system if self after program exits. */
}
下面的例子也有同样的问题,丢失了已分配空间的指针,导致内存泄露。

#include <stdlib.h>
void f(void)
{
	int* x=malloc(10*sizeof(int));
	x[10]=0;//problem 1:heap block overrun; problem 2:memory leak, x not freed.
}
int main(void)
{
	f();
	return 0;
}

终于完了,基本上是把原文翻了一遍。(废我一下午的说)

原文《Memory management in C: The heap and the stack》Leo Ferres, Department of Computer Science, Unverisidad de Concepcion, leo@udec.cl, October 7,2010

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值