程序的 text段、data段、bss段与rodata段

现代操作系统的内存分配以页为单位进行管理,而页通过段进行管理,组成了段页式内存管理。
本文对C++程序的各段进行简单的区分,并厘清各段在可执行程序与进程中的状态关系。

程序大体被划分为两部分,只读部分和读/写部分,这源于历史上ROM和RAM两类存储器的划分。尽管现代存储器的发展早就突破了这种分类方式,程序内存的某些部分不应该被修改的想法却被保留了下来。

text段

代码段(code segment/text segment)用于存放可执行程序,也就是对应架构下程序的指令序列。代码段包含在可执行程序中,大小是确定的;加载到进程中之后,所在内存区域通常被MMU设置为只读,以保护代码段不会被意外改写(如出错时)。当然,没有MMU的系统,就没有这种写保护。代码段也可能包含一些只读的常量,如字符串常量等。

data段、bss段、rodata段

  • 数据段(data segment)用于存放编译时就能确定的全局数据,包括已初始化的全局变量和静态变量。数据段包含在可执行程序中,大小是确定的;加载到进程中,所在内存区域可读可写。数据段属于静态内存分配。
  • bss是英文Block Started by Symbol的缩写(奇怪的历史遗留),用于存放编译阶段无法确定的全局数据,包括未初始化的全局变量和静态变量。可执行程序不含bss段,只记录区域大小;进程为bss段开辟内存空间,并清零。从优化的角度出发,初始化为零的全局变量,也会被放进bss段从大压缩可执行文件的大小。
  • 常量区rodata段存放的是只读数据,比如字符串常量、全局const变量。常量区在程序中大小确定;在进程中内存只读。

注意,并不是所有的常量都会被保存到rodata段,存在一些特殊情况:

  1. 有些立即数会直接编码到指令里,位于代码段;
  2. 重复的字符串常量会合并,程序中只保留一份;
  3. 某些系统中rodata段会被多个进程共享,用于提高空间利用率。 —— amazing~
    另外,有的嵌入式系统中,rodata不会加载到RAM中,而是直接在ROM中读取。
    由此可见,将不会修改的变量设为const是有很多好处的,不仅提高空间利用率,还能得到系统的写保护,有利于提高系统稳定性。

rel.text段、rel.data段

分别是针对text段和data段的重定位表。

strtab段、symtab段、shstrtab段

  • .strtab是字符串表(String Table);
  • .shstrtab是段表字符串表(Section Header String Table),针对段表;
  • .symtab是符号表,一般是变量、函数;
    shstrtab及symtab会引用strtab中的字符串。

stack段、heap段

堆和栈都是进程中的概念,可执行程序中并不包括这两个段,更不会预先指定其大小。堆和栈的大小都受到具体的机器限制,当堆和栈用尽系统内存的时候,分别会发生常见的 Stack OverflowOut of Memory 错误。

  • stack在内存空间中位于用户空间的顶部,自顶向下增长。栈底是main函数,在执行完bss段清零等初始化工作后才进入main函数开始执行。
  • heap在内存空间中位于用户空间的底部,但是在text、bss、data等各段之上,与stack段相向增长。堆的空间增长和回收由malloc/free、new/delete来操作,并可通过为系统配置不同的内存分配库如 ptmalloc(glibc标配)tcmalloc(google)jemalloc(facebook) 来切换不同的内存分配算法,以提高内存分配效率。

总结

进程中地址从小到大包含四部分:代码、数据、堆、栈;其中数据又由三部分构成 —— 常量、已初始全局/静态变量、未初始全局/静态变量。
可执行程序中确定包含的段有:text/code段、data段、rodata段,可能包含的段有:符号表和重定位表。
编译时gcc -g选项可以打开调试信息。重定位分为链接时重定位和装载时重定位,链接时重定位是把各个不同.o文件的符号合并到目标地址,从而形成一个统一的可执行文件;装载时重定位是因为可能引用外部的动态链接库.so文件中的变量或函数,无法预先确定偏移量。如果没有引用外部.so,则无需装载时重定位。

一些验证代码

  1. 验证全局变量位于data段还是bss段
#include <stdio.h>

int a[100]; // case 1
// int a[100] = {1}; // case 2
// int a[100] = {0}; // case 3

int main() {
    printf("%d\n", a[0]);
    return 0;
}

在本地对于以上三种情况分别进行编译,并使用 ls -al查看文件大小显示分别为 8328、8744、8328。显然,未初始化和初始化为零的大小一样,都是作为bss段处理,并不占用可执行程序大小;初始化为非零值之后,除了100个int元素应占的400B空间之外,另有16B额外空间开销(可能是用于strtab或者rel.data开销?)。

posted @ 2021-02-23 20:16  与MPI做斗争  阅读(7464)  评论(0编辑  收藏  举报