一个进程在执行的时候,它所占用的内存的虚拟地址空间一般被分割成好几个区域,通常叫做“段(segment)”,所下所示:
- 文本段(只读端):编译后的机器码存在的区域。一般这个区域是只读的
- 包含了进程运行的程序机器语言指令。
- 文本段具有只读属性,以防止进程通过错误指针意外修改自身指令。
- 因为多个进程可以同时运行同一程序,所以又将文本段设置为可共享。这样,一份程序代码可以映射到所有这些进程的虚拟地址空间中
- 数据段:程序使用的静态变量和全局变量
- 初始化数据段:
- 包含显式初始化的全局变量和静态变量。
- 也叫做用户初始化数据段(user-initialized data segment)
- 当程序加载到内存时,从可执行文件中读取这些变量的值。
- 未初始化数据段(bss段):
- 段包含了未进行显式初始化的全局变量和静态变量。
- 程序启动之前,系统将本段内所有内存初始化为 0。
- 出于历史原因,此段常被称为 BSS 段,这源于老版本的汇编语言助记符“block started by symbol”。
- 它也叫做零初始化数据段(zero-initialized data segment)
- 为什么要将将经过初始化的全局变量和静态变量与未经初始化的全局变量和静态变量分开存放?
- 其主要原因在于程序在磁盘上存储时,没有必要为未经初始化的变量分配存储空间。
- 相反,可执行文件只需记录未初始化数据段的位置及所需大小,直到运行时再由程序加载器来分配这一空间。
- 初始化数据段:
- 栈:随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间
- 栈(stack)是一个动态增长和收缩的段,由栈帧(stack frames)组成
- 系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量、实参和返回值以及调用链接的信息
- 堆:可在运行时(为变量)动态进行内存分配的一块区域。堆顶端称作program break。
size(1)命令可显示二进制可执行文件的文本段、初始化数据段、非初始化数据段(bss)的段大小:
下面程序展示了不同类型的 C 语言变量,并以注释说明每种变量分属于哪个段。这些说明正确的前提是假定使用了非优化的编译器,且在应用程序二进制接口(ABI)中,是通过栈来传递所有参数的。实际上,优化编译器会将频繁使用的变量分配在寄存器中。
应用程序二进制接口(ABI)是一套规则,规定了二进制可执行文件在运行时应该如何与某些服务(比如内核或函数库所提供的服务)交换信息。ABI特别规定了使用哪些寄存器和栈地址来交换信息以及所交换值的含义,一旦针对某个特定ABI进行了编译,其二进制可执行文件应该能在ABI相同的任何系统上运行。。与之相反,标准化的 API(如 SUSv3)仅能通过编译源代码来保证应用程序的可移植性
#include <stdio.h>
#include <stdlib.h>
char globBuf[65536]; /* Uninitialized data segment */
int primes[] = { 2, 3, 5, 7 }; /* Initialized data segment */
static int
square(int x) /* Allocated in frame for square() */
{
int result; /* Allocated in frame for square() */
result = x * x;
return result; /* Return value passed via register */
}
static void
doCalc(int val) /* Allocated in frame for doCalc() */
{
printf("The square of %d is %d\n", val, square(val));
if (val < 1000) {
int t; /* Allocated in frame for doCalc() */
t = val * val * val;
printf("The cube of %d is %d\n", val, t);
}
}
int
main(int argc, char *argv[]) /* Allocated in frame for main() */
{
static int key = 9973; /* Initialized data segment */
static char mbuf[10240000]; /* Uninitialized data segment */
char *p; /* Allocated in frame for main() */
p = malloc(1024); /* Points to memory in heap segment */
doCalc(key);
exit(EXIT_SUCCESS);
}
虽然SUSv3未作规定,但在大多数 UNIX 实现(包括 Linux)中C语言编程环境提供了三个全局符号:etext、edata、end,可在程序内使用这些符号以获取相应程序文本段、初始化数据段和非初始化数据段结尾处下一字节的地址。使用这些符号,必须显式声明如下:
extern char etext, edata, end;
下图展示了各种内存段在 x86-32 体系结构中的布局:
- 这一布局存在于虚拟内存中
- 该图的顶部标记为 argv、environ的空间用来存储程序命令行实参(通过 C 语言中 main()函数的 argv 参数获得)和进程环境列表
- 图中十六进制的地址会因内核配置和程序链接选项差异而有所不同。
- 图中标灰的区域表示这些范围在进程虚拟地址空间中不可用,也就是说,没有为这些区域创建页表(page table)
# size /usr/bin/cc /bin/sh // 查看各空间长度
text data bss dec hex filename
754949 8496 81856 845301 ce5f5 /usr/bin/cc
904335 35984 22920 963239 eb2a7 /bin/sh