C 储存及环境
C 存储及环境
C 程序一般是由下列几部分组成:
(1)正文段。这是由 CPU 执行的机器指令部分,它通常是可共享的,所以即使是频繁执行的程序(如文本编辑器、C 编译器和 shell 等)在存储器中也只有一个副本。另外,正文段通常是只读的,以防止程序由于意外而修改其指令。
(2)初始化数据段(简称数据段)。它包含了程序中被明确地赋初值的全局变量等。
(3)未初始化数据段(也称 bss 段,即“由符号开始的块”(block started by symbol))。函数外的未被初始化声明的变量就被存放在这里。程序开始前,内核将此段中的数据初始化为 0 或空指针。
(4)栈。自动变量及函数每次调用时所需保存的信息(如返回地址、调用者的环境信息等)都存放在栈中。注意,栈的增长方向一般是从高地址往低地址方向增长的。
(5)堆。主要用于动态存储分配,一般位于未初始化数据段和栈之间。
下图是这些段的一种典型存储空间布局。

从图中可知,未初始化数据段的内容并不存放在磁盘程序文件中,因为内核在程序开始运行前将它们都设置为 0。需要存放在磁盘程序文件中的只有正文段和初始化数据段。
实际上,编译好的可执行程序中还有若干其他类型的段,如包含符号表的段、包含调试信息的段以及包含动态共享库链接表的段等,不过它们并不装载到进程执行的程序映像中。
size 命令可报告正文段、数据段和 bss 段的长度(以字节为单位)。如:
其中第 4 列和第 5 列分别是以十进制和十六进制表示的前 3 段的总长度。
ISO 说明了 3 个用于存储空间动态分配的函数。
这 3 个函数返回的指针都是适当对齐的,可用于任何数据对象。其中 malloc 分配指定字节数的存储区,其中的初始值不确定;calloc 为指定数量指定长度的对象分配存储空间,其中的每一位都初始化为 0;realloc 可增加或减少以前分配区的长度。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定。realloc 的 newsize 参数是存储区的新长度,当 ptr 是空指针时,realloc 的功能就如同 malloc。free 函数可用来释放 ptr 指向的存储空间,以便以后再分配。
这些分配例程通常用 sbrk 系统调用实现,该系统调用可以扩充或缩小进程的堆。但是大多数 malloc 和 free 的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将它们保持在 malloc 池中而不返回给内核。一般所分配的存储空间都比所要求的要稍大一些,额外的空间会用来记录管理信息,比如分配块的长度、指向下一个分配块的指针等。因此在动态分配的缓冲区前或后进行写操作极有可能会改写另一块的管理记录信息。另外,释放一个已经释放了的块或者释放不是由 alloc 类函数分配的块等都有可能导致致命性的错误。而如果不释放不再使用的空间,则会使进程地址空间长度慢慢增加,直至不再有空闲空间,这被称为泄漏(leakage)。这会导致过度的换页开销,从而造成性能下降。
每个程序都接收到一张环境表,它是一个字符指针数组,其中每个指针都包含一个指向以 null 结束的 C 字符串的地址,全局变量“extern char **environ”则包含了该指针数组的地址,通常称 environ 为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。例如,一个以 5 个字符串组成的环境看起来大致如下图所示。

main 函数在大多数 UNIX 系统上的原型是这样的:
int main(int argc, char **argv[], char *envp[]);
其中第三个就是环境表地址。不过因为该参数比起 environ 也没有带来多少益处,因此 POSIX.1 也规定应使用 environ 而不使用第 3 个参数。通常用 getenv 和 putenv 等函数来访问和设置特定的环境变量,而不是直接使用 environ,但如果要查看整个环境,则必须使用 environ 指针。
其中:
* getenv 是用来获取某个环境变量的值。
* putenv 取形式为“name=value”的字符串,将其放到环境表中。如果 name 已经存在,则先删除其原来的定义。
* setenv 将 name 设置为 value。如果 name 已经存在,则根据参数 rewrite 的值为 0与否来决定是否覆盖原来的定义。
* unsetenv 删除 name 的定义,即使不存在也不算出错。
注意,putenv 和 setenv 的差别在于:putenv 可以自由地将传递给它的参数字符串直接放到环境中(但不应该将存放在栈中的字符串传给它);setenv 必须分配存储空间,以便依据其参数创建 name=value 字符串。
了解这些函数是如何修改环境表也是非常有益的。其中,环境表和环境字符串通常存放在进程存储空间的顶部(栈之上)。删除一个字符串时只需要在环境表中找到该指针,然后将所有后续指针都向环境表首部顺次移动一个位置。但是增加一个字符串或修改一个现有的字符串就困难得多。因为环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向扩展,同时也不能移动在它之下的各栈帧。两者结合使得该空间的长度不能再增加。
(1)当修改一个现有的 name 时:
a. 如果新 value 的长度不大于现有的长度,则只要复制新字符串到原字符串所在的空间即可。
b. 如果新 value 的长度大于原长度,则必须调用 malloc 为新字符串分配空间,然后将新字符串复制到该空间,接着使环境表中的针对 name 的指针指向新分配区。
(2)当增加一个新的 name 时就复杂多了。必须先调用 malloc 为 name=value 字符串分配空间,然后将该字符串复制到此空间中。
a. 如果这是第一次增加一个新的 name,则必须调用 malloc 为新的指针表分配空间。接着,将原来的环境表复制到新分配区,并将指向新 name=value 字符串的指针存放在该指针表的表尾,然后又将一个空指针存放在其后。最后使 environ 指向新指针表。如果原来的环境表位于栈顶之上,那么必须将此表移至堆中。但是此表中的大多数指针仍指向栈顶之上的各 name=value 字符串。
b. 如果这不是第一次增加一个新的 name,则说明之前已在堆中为环境表分配了空间,所以只要调用 relloc,以分配比原空间多存放一个指针的空间,然后将指向新 name=value 的字符串的指针存放在该表表尾,后面跟着一个空指针。
C 程序一般是由下列几部分组成:
(1)正文段。这是由 CPU 执行的机器指令部分,它通常是可共享的,所以即使是频繁执行的程序(如文本编辑器、C 编译器和 shell 等)在存储器中也只有一个副本。另外,正文段通常是只读的,以防止程序由于意外而修改其指令。
(2)初始化数据段(简称数据段)。它包含了程序中被明确地赋初值的全局变量等。
(3)未初始化数据段(也称 bss 段,即“由符号开始的块”(block started by symbol))。函数外的未被初始化声明的变量就被存放在这里。程序开始前,内核将此段中的数据初始化为 0 或空指针。
(4)栈。自动变量及函数每次调用时所需保存的信息(如返回地址、调用者的环境信息等)都存放在栈中。注意,栈的增长方向一般是从高地址往低地址方向增长的。
(5)堆。主要用于动态存储分配,一般位于未初始化数据段和栈之间。
下图是这些段的一种典型存储空间布局。
从图中可知,未初始化数据段的内容并不存放在磁盘程序文件中,因为内核在程序开始运行前将它们都设置为 0。需要存放在磁盘程序文件中的只有正文段和初始化数据段。
实际上,编译好的可执行程序中还有若干其他类型的段,如包含符号表的段、包含调试信息的段以及包含动态共享库链接表的段等,不过它们并不装载到进程执行的程序映像中。
size 命令可报告正文段、数据段和 bss 段的长度(以字节为单位)。如:
$ size /usr/bin/cc /bin/sh text data bss dec hex filename 346919 3576 6680 357175 57337 /usr/bin/cc 102134 1776 11272 115182 1c1ee /bin/sh
其中第 4 列和第 5 列分别是以十进制和十六进制表示的前 3 段的总长度。
ISO 说明了 3 个用于存储空间动态分配的函数。
#include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nobj, size_t size); void *realloc(void *ptr, size_t newsize); /* 返回值:若成功,都返回非空指针;否则,都返回 NULL */ void free(void *ptr);
这 3 个函数返回的指针都是适当对齐的,可用于任何数据对象。其中 malloc 分配指定字节数的存储区,其中的初始值不确定;calloc 为指定数量指定长度的对象分配存储空间,其中的每一位都初始化为 0;realloc 可增加或减少以前分配区的长度。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域内的初始值则不确定。realloc 的 newsize 参数是存储区的新长度,当 ptr 是空指针时,realloc 的功能就如同 malloc。free 函数可用来释放 ptr 指向的存储空间,以便以后再分配。
这些分配例程通常用 sbrk 系统调用实现,该系统调用可以扩充或缩小进程的堆。但是大多数 malloc 和 free 的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将它们保持在 malloc 池中而不返回给内核。一般所分配的存储空间都比所要求的要稍大一些,额外的空间会用来记录管理信息,比如分配块的长度、指向下一个分配块的指针等。因此在动态分配的缓冲区前或后进行写操作极有可能会改写另一块的管理记录信息。另外,释放一个已经释放了的块或者释放不是由 alloc 类函数分配的块等都有可能导致致命性的错误。而如果不释放不再使用的空间,则会使进程地址空间长度慢慢增加,直至不再有空闲空间,这被称为泄漏(leakage)。这会导致过度的换页开销,从而造成性能下降。
每个程序都接收到一张环境表,它是一个字符指针数组,其中每个指针都包含一个指向以 null 结束的 C 字符串的地址,全局变量“extern char **environ”则包含了该指针数组的地址,通常称 environ 为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。例如,一个以 5 个字符串组成的环境看起来大致如下图所示。
main 函数在大多数 UNIX 系统上的原型是这样的:
int main(int argc, char **argv[], char *envp[]);
其中第三个就是环境表地址。不过因为该参数比起 environ 也没有带来多少益处,因此 POSIX.1 也规定应使用 environ 而不使用第 3 个参数。通常用 getenv 和 putenv 等函数来访问和设置特定的环境变量,而不是直接使用 environ,但如果要查看整个环境,则必须使用 environ 指针。
#include <stdlib.h> char *getenv(const char *name); /* 返回值:指向与 name 关联的 value 的指针;未找到就返回 NULL */ int putenv(char *str); /* 返回值:若成功,返回 0;否则,返回非 0 */ int setenv(const char *name, const char *value, int rewrite); int unsetenv(const char *name); /* 两个函数返回值:若成功,返回 0;否则,返回 -1 */
其中:
* getenv 是用来获取某个环境变量的值。
* putenv 取形式为“name=value”的字符串,将其放到环境表中。如果 name 已经存在,则先删除其原来的定义。
* setenv 将 name 设置为 value。如果 name 已经存在,则根据参数 rewrite 的值为 0与否来决定是否覆盖原来的定义。
* unsetenv 删除 name 的定义,即使不存在也不算出错。
注意,putenv 和 setenv 的差别在于:putenv 可以自由地将传递给它的参数字符串直接放到环境中(但不应该将存放在栈中的字符串传给它);setenv 必须分配存储空间,以便依据其参数创建 name=value 字符串。
了解这些函数是如何修改环境表也是非常有益的。其中,环境表和环境字符串通常存放在进程存储空间的顶部(栈之上)。删除一个字符串时只需要在环境表中找到该指针,然后将所有后续指针都向环境表首部顺次移动一个位置。但是增加一个字符串或修改一个现有的字符串就困难得多。因为环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向扩展,同时也不能移动在它之下的各栈帧。两者结合使得该空间的长度不能再增加。
(1)当修改一个现有的 name 时:
a. 如果新 value 的长度不大于现有的长度,则只要复制新字符串到原字符串所在的空间即可。
b. 如果新 value 的长度大于原长度,则必须调用 malloc 为新字符串分配空间,然后将新字符串复制到该空间,接着使环境表中的针对 name 的指针指向新分配区。
(2)当增加一个新的 name 时就复杂多了。必须先调用 malloc 为 name=value 字符串分配空间,然后将该字符串复制到此空间中。
a. 如果这是第一次增加一个新的 name,则必须调用 malloc 为新的指针表分配空间。接着,将原来的环境表复制到新分配区,并将指向新 name=value 字符串的指针存放在该指针表的表尾,然后又将一个空指针存放在其后。最后使 environ 指向新指针表。如果原来的环境表位于栈顶之上,那么必须将此表移至堆中。但是此表中的大多数指针仍指向栈顶之上的各 name=value 字符串。
b. 如果这不是第一次增加一个新的 name,则说明之前已在堆中为环境表分配了空间,所以只要调用 relloc,以分配比原空间多存放一个指针的空间,然后将指向新 name=value 的字符串的指针存放在该表表尾,后面跟着一个空指针。