主要讨论C语言怎样组织正在运行的程序的数据结构的细节。
我们知道知道在UNIX操作系统中,一个C语言文件经过预处理(cpp),编译(cc1),汇编(as)和链接(ld)后可以得到可执行文件a.out。我们可以用size命令(或nm、dump)检查可执行文件,它会告诉你这个文件中的三个段(文本段.text,数据段.data,.bss段)的大小。文本段包含程序的指令。由于程序的文本内容和大小都不会变化,链接器把指令直接从文件拷贝到内存中,不再处理。数据段包含经过初始化的全局和静态变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段之后。当这个内存区进入程序的地址空间后全部清零,通常是memset全部置0。通常把数据段和BSS段统一称为数据区。上面这些内容可以通过size命令很直观的查看与验证。除了上面提到的程序指令、全局和静态变量,我们还有局部变量等数据需要存储。这就引出了堆栈段(stack)和堆空间(heap)。下面将详细讨论数据区、堆栈段和堆空间。
1.数据区存放着全局变量与静态变量,包括初始化和未初始化。其中未初始化的在进入程序地址空间的时候统一清零。这些数据的生命周期都是主程序运行时间。所谓静态变量,是指由static修饰的变量,可以是全局变量,也可以是局部变量。当修饰子函数的局部变量的时候,如果有初始化的话只在第一次进入子函数的时候执行初始化语句,并且在子函数退出的时候依然有效。关于static的另一个作用便是对链接器不可见,这个在以后再说。
1 |
|
2.堆栈段有三个主要的用途:一是为函数内部声明的局部变量(也称“自动变量”)提供存储空间;二是在进行函数调用时,堆栈存储与此相关的一些维护性信息。这些信息被称为堆栈结构(stack frame),或者过程活动记录(precedure activation recored),其中包括函数调用地址、任何不适合装入寄存器的参数以及一些寄存器的保存;三是用作暂时存储区。有时程序需要一些临时存储,比如算术表达式计算。值得注意的是堆栈的入栈是由高地址到低地址,并且局部变量的生命周期在子函数退出的时候也释放了。下面看一个程序
1 |
|
由于C语言中对数组越界并不会作检查,所以上述程序是可以编译通过并执行的。
volatile是为了防止编译器对代码进行优化,我们来看下foo子函数的堆栈段的情况,由高到低,压入i,a,b[1],b[0],则i==2时,返回的实际上是a。
上面提出局部变量的生命周期,如果一个函数返回了一个局部变量会怎么样呢?
1 |
|
两个函数是不一样的,第一个函数返回int型变量,第二个函数返回给一个指针,指向int类型。第一个函数是没有错的,return的时候return的是局部变量a的一个拷贝。而第二个函数返回了局部变量的地址,这是很不安全的。由于函数foo()退出后,局部变量a的地址不受保护,可能会被用作他用,如上面的程序先调用foo2(),再调用foo1()时,把之前的y指向的地址的内容修改了,所以上述程序输出为100, 100。
是不是感觉不过瘾,那再来一个
1 |
|
如果根据第二个例子,我们可以很容易推出结果是:100。但是结果对吗?跑一下就知道了,gcc -O2 test test.c。这里加入了优化选项,从程序中可以看出foo()函数其实什么都没有做,经过编译器可能直接路过foo1(),执行foo2(),所以结果是个随机数。验证也很简单,在foo1()中对a进行一次相关操作就好了。好了,这个有点涉及程序优化了,以后再说。
3.堆(heap)空间用于动态内存的分配,用于malloc相关函数。上面提到堆栈段,注意堆栈段就是栈,是向下生长的,而堆是向上生长的,正好形成一个闭区间。当我们调用malloc等分配内存的时候,要进行检查,因为如果分配失败的话,返回NULL指针,访问的话就会出现段错误了。还有一点要注意是内存泄漏,不用的时候需要显示free()释放。有些语言是加入了垃圾回收机制的,如LISP。对了,还有一点需要提防的就是野指针。野指针在我们进行free之后,指针的指向还是没有变的,所以需要手动置为NULL。
上面主要对C语言运行时的几个段进行一些讨论,当然更深入的学习还是要靠大家自己去实验。
参考:《C专家编程》,《深入理解计算机系统》