外观
03.内存段详解:.text、.data、.bss、.rodata
约 3071 字大约 10 分钟
内存C个人随笔
2025-12-11
一、核心概念:内存分段
在程序执行时,操作系统和链接器会将程序的不同部分组织到内存的不同区域,这些区域称为"段"(Section)。每个段有特定的用途和访问权限,这种分段管理有助于:
- 内存保护:代码段只读,防止意外修改
- 效率优化:将相似特性的数据放在一起
- 节省空间:如.bss段在文件中不占实际空间
- 共享内存:多个进程可共享相同的代码段
二、各段详解
1. .text 段(代码段/文本段)
作用:存放可执行代码(机器指令)
特性:
- 通常是只读的(Read-Only)
- 可执行(eXecute)
- 在内存中被标记为可执行
包含内容:
- 所有函数体
- 程序指令
- 汇编代码
不同叫法:
- 有些书叫
.code(特别是Windows相关) - 但主流还是叫
.text
- 有些书叫
示例:
// 所有函数代码都在.text段 int main() { return 0; // 这行代码对应的机器指令在.text段 } void calculate() { // 这个函数的全部机器指令在.text段 int x = 10 + 20; }
2. .data 段(数据段)
- 作用:存放已初始化的全局变量和静态变量,且初始值非零
- 特性:
- 可读可写
- 程序加载时从可执行文件中读取初始值
- 包含内容:
int global_var = 100; // 全局已初始化变量 → .data static int static_var = 200; // 静态已初始化变量 → .data char str[] = "Hello"; // 已初始化数组 → .data // 复杂初始化也在.data struct Point { int x; int y; }; struct Point p = {10, 20}; // → .data - 重要规则:
- 必须是非零初始化(零值初始化会进入.bss)
- 包括全局变量和静态变量(无论全局静态还是局部静态)
3. .bss 段(Block Started by Symbol)
- 作用:存放未初始化或初始化为0的全局/静态变量
- 特性:
- 可读可写
- 在可执行文件中不占实际空间
- 程序加载时分配内存并清零
- 名称来源:"Block Started by Symbol"(历史术语)
- 优势:显著节省磁盘空间,特别是对于大型零值数组
- 包含内容:
int global_uninit; // 未初始化的全局变量 → .bss static int static_uninit; // 未初始化的静态变量 → .bss int global_zero = 0; // 初始化为0的全局变量 → .bss int global_array[10000] = {0}; // 全零大数组 → .bss(节省大量文件空间) // 静态局部变量未初始化也在.bss void func() { static int local_static_uninit; // → .bss }
4. .rodata 段(只读数据段)
- 作用:存放只读数据
- 特性:
- 只读(Read-Only)
- 尝试修改会导致段错误(Segmentation Fault)
- 多个进程可共享同一.rodata段
- 包含内容:
const int MAX_SIZE = 100; // 全局常量 → .rodata const char* str = "Hello World"; // 指针可能在.data,但字符串"Hello World"在.rodata // 字符串字面量总是放在.rodata char* message = "Error occurred"; // 字符串"Error occurred"在.rodata // 常量数组 const int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // → .rodata // 格式化字符串 printf("Value: %d\n", x); // 字符串"Value: %d\n"在.rodata - 不同叫法:
- Linux/ELF格式:
.rodata - Windows/PE格式:
.rdata - 某些文档可能简称为"常量段"
- Linux/ELF格式:
三、为什么会混淆?命名差异解析
混淆原因分析:
历史演变:
- 早期不同厂商有自己的命名约定
- UNIX、Windows、嵌入式系统各自发展
平台差异:
Linux/ELF格式: .text, .data, .bss, .rodata Windows/PE格式: .text或.code, .data, .bss, .rdata 嵌入式系统: CODE, DATA, BSS, CONST编译器差异:
- GCC、Clang使用ELF标准
- MSVC使用PE格式
- 嵌入式编译器可能有自定义命名
常见混淆点澄清表:
| 实际内容 | 可能的混淆名称 | 标准名称(Linux/ELF) | 说明 |
|---|---|---|---|
| 代码/指令 | .code, .text | .text | 大多数系统现在统一用.text |
| 只读数据 | .rdata, .rodata, .const | .rodata | Linux用.rodata,Windows用.rdata |
| 未初始化数据 | .bss, .COMMON, .ubss | .bss | .COMMON是早期变体 |
| 已初始化数据 | .data, .sdata | .data | .sdata是小数据段的优化 |
四、完整内存布局示例
32位Linux程序典型内存布局:
高地址 0xFFFFFFFF
┌─────────────────────────────────┐
│ 内核空间 │ (1GB)
│ Kernel Space │ 用户程序无法访问
├─────────────────────────────────┤ 0xC0000000
│ 栈区 │
│ Stack │ 向下增长
│ │ │
│ ↓ 局部变量、函数参数 │
│ 返回地址、临时数据 │
│ │
├─────────────────────────────────┤
│ 共享库 │
│ Shared Libraries │ .text/.data/.bss/.rodata
│ (libc.so等) │
├─────────────────────────────────┤
│ 堆区 │
│ Heap │ 向上增长
│ ↑ 动态分配内存 │ malloc/free/new/delete
│ │ │
├─────────────────────────────────┤
│ .bss段 │
│ 未初始化/零初始化全局变量 │ 程序启动时清零
│ │
├─────────────────────────────────┤
│ .data段 │
│ 已初始化全局/静态变量 │ 从可执行文件加载初始值
│ │
├─────────────────────────────────┤
│ .rodata段 │
│ 只读数据 │ 字符串常量、全局const
│ │
├─────────────────────────────────┤
│ .text段 │
│ 程序代码 │ 只读、可执行
│ 机器指令 │
└─────────────────────────────────┘ 低地址 0x08048000详细的内存地址范围示例:
0xFFFFFFFF ┌─────────────────┐
│ 内核 │
0xC0000000 ├─────────────────┤
│ 栈 │ 0xBFFFFFF0 (栈顶)
│ ↓ │
0xBF800000 ├─────────────────┤
│ ... │
├─────────────────┤
│ 共享库 │
├─────────────────┤
│ 堆 │ 0x09000000 (堆底)
│ ↑ │
0x08049000 ├─────────────────┤ ← 程序break位置
│ .bss │
0x08048300 ├─────────────────┤
│ .data │
0x08048200 ├─────────────────┤
│ .rodata │
0x08048100 ├─────────────────┤
│ .text │
0x08048000 ├─────────────────┤ ← 程序入口
│ ELF头 │
0x08047000 └─────────────────┘五、验证与查看方法
C语言示例程序:
#include <stdio.h>
// 全局变量示例
int data_var = 42; // 将在.data段
int bss_var; // 将在.bss段
const int rodata_var = 100; // 将在.rodata段
char* rodata_str = "Constant"; // 字符串在.rodata,指针在.data
int main() { // main函数代码在.text段
static int static_data = 10; // 在.data段
static int static_bss; // 在.bss段
printf("演示各内存段中的变量:\n");
printf("1. 代码段 (.text):\n");
printf(" main函数地址: %p\n", main);
printf(" printf函数地址: %p\n", printf);
printf("\n2. 数据段 (.data):\n");
printf(" 全局初始化变量: %p (值=%d)\n", &data_var, data_var);
printf(" 静态初始化变量: %p (值=%d)\n", &static_data, static_data);
printf("\n3. BSS段 (.bss):\n");
printf(" 全局未初始化: %p (值=%d)\n", &bss_var, bss_var);
printf(" 静态未初始化: %p (值=%d)\n", &static_bss, static_bss);
printf("\n4. 只读数据段 (.rodata):\n");
printf(" 全局常量: %p (值=%d)\n", &rodata_var, rodata_var);
printf(" 字符串字面量: %p (内容='Constant')\n", rodata_str);
printf(" 格式字符串: %p\n", "格式字符串: %p\\n");
printf("\n5. 栈和堆:\n");
int stack_var = 50; // 栈变量
int* heap_var = malloc(sizeof(int)); // 堆变量
*heap_var = 99;
printf(" 栈变量: %p (值=%d)\n", &stack_var, stack_var);
printf(" 堆变量: %p (值=%d)\n", heap_var, *heap_var);
free(heap_var);
return 0;
}编译和查看命令:
# 1. 编译程序
gcc -o memory_demo memory_demo.c
# 2. 查看程序各段大小
size memory_demo
# 输出示例:
# text data bss dec hex filename
# 1520 560 24 2104 838 memory_demo
# 3. 查看详细段信息
objdump -h memory_demo
# 输出每个段的详细信息:虚拟地址、大小、文件偏移等
# 4. 查看ELF文件段头(最详细)
readelf -S memory_demo
# 会显示所有段的信息,包括.text, .data, .bss, .rodata等
# 5. 查看符号及其所在段
nm memory_demo | sort
# 显示所有符号及其地址,可以看变量在哪个段
# 6. 查看.rodata段的具体内容
objdump -s -j .rodata memory_demo
# 显示.rodata段的十六进制和字符串内容size命令输出解析:
text data bss dec hex filename
1520 560 24 2104 838 memory_demo- text: 1520字节 - 代码段大小
- data: 560字节 - 已初始化数据段大小
- bss: 24字节 - 未初始化数据段大小
- dec: 2104字节 - 十进制总大小
- hex: 838字节 - 十六进制总大小
六、各段的关键特性对比表
| 特性 | .text (代码段) | .data (数据段) | .bss (BSS段) | .rodata (只读段) |
|---|---|---|---|---|
| 内容 | 可执行代码 | 已初始化全局/静态变量 | 未初始化全局/静态变量 | 只读数据 |
| 初始值 | 机器指令 | 从文件加载 | 全零 | 从文件加载 |
| 权限 | 只读+执行 | 可读可写 | 可读可写 | 只读 |
| 文件空间 | 占用 | 占用 | 不占用(只有大小信息) | 占用 |
| 内存分配 | 加载时 | 加载时 | 加载时(清零) | 加载时 |
| 共享性 | 可共享(多进程) | 不共享 | 不共享 | 可共享 |
| 修改后果 | 段错误 | 正常修改 | 正常修改 | 段错误 |
| 示例内容 | 函数体 | int x=5; | int y; | const int z=10; |
七、编程实践指南
1. 如何控制变量进入特定段:
#include <stdio.h>
// 默认情况下编译器自动决定
int normal_var = 10; // .data
// 使用编译器特定扩展手动指定段(GCC/Clang)
int __attribute__((section(".mysection"))) custom_var = 20;
// 强制变量到.data段(即使零初始化)
int __attribute__((section(".data"))) force_data = 0;
// 常量字符串(自动进入.rodata)
const char* ERROR_MSG = "Fatal error occurred";
int main() {
// 查看变量地址
printf("normal_var: %p\n", &normal_var);
printf("custom_var: %p\n", &custom_var);
printf("force_data: %p\n", &force_data);
printf("ERROR_MSG字符串: %p\n", ERROR_MSG);
return 0;
}2. 优化建议:
// ✅ 推荐做法:
// 1. 大数组如果不需要初始值,让其进入.bss节省磁盘空间
double large_array[100000]; // 在.bss,文件大小几乎不增加
// 2. 需要初始值的常量使用const,享受只读保护
const int BUFFER_SIZE = 1024;
// 3. 频繁使用的字符串设为全局常量
const char* LOG_FORMAT = "[%s] %s\n";
// ❌ 不推荐做法:
// 1. 大数组全零初始化但写在.data
// double zero_array[100000] = {0}; // 这会在.data占大量文件空间
// 正确做法:让编译器放到.bss
double zero_array[100000]; // 未初始化,进入.bss
// 或者
double zero_array2[100000] = {0}; // 全零,GCC优化后可能进入.bss3. 不同编译器的处理差异:
// GCC/Clang优化示例
const int value = 100;
void test() {
// 情况1:直接使用值 - 可能优化成立即数
int x = value * 2; // 可能直接编译成 mov eax, 200
// 情况2:取地址 - 必须在内存中
const int* ptr = &value; // value必须在.rodata有地址
// 情况3:static const - 肯定在.rodata
static const int local_const = 200;
}八、常见问题解答(FAQ)
Q1: 为什么要有.bss段?
A: 主要为了节省磁盘空间。假设程序有10MB的全零数组:
- 放在.data段:可执行文件增加10MB
- 放在.bss段:文件只增加"需要10MB零初始化内存"的信息(几个字节) 程序加载时,操作系统自动分配10MB内存并清零。
Q2: 如何知道变量在哪个段?
A: 可以通过以下方法:
- 全局已初始化非零 →
.data - 全局未初始化或零初始化 →
.bss - 全局const →
.rodata - 函数代码 →
.text - 使用
nm或objdump工具查看
Q3: 静态局部变量在哪里?
A:
- 初始化的静态局部变量 →
.data - 未初始化的静态局部变量 →
.bss无论是否初始化,静态局部变量都在全局区(.data或.bss),不在栈上。
Q4: 字符串字面量在哪里?
A:
char* str1 = "Hello"; // "Hello"在.rodata,str1指针在.data(全局)或栈(局部)
char str2[] = "World"; // 如果全局:在.data;如果局部:在栈
const char* str3 = "Test"; // "Test"在.rodata,str3在.data或栈Q5: 修改.rodata或.text段会发生什么?
A: 会发生段错误(Segmentation Fault)。操作系统保护这些只读内存区域。
Q6: 多个进程可以共享这些段吗?
A:
.text和.rodata段:可以共享(节省物理内存).data和.bss段:每个进程有独立副本(写时复制技术)
九、总结
理解内存分段是掌握程序底层工作原理的关键:
- .text段 - 程序的"大脑",存放执行指令
- .data段 - 程序的"记忆",存放已知初始值的全局数据
- .bss段 - 程序的"预留空间",存放待初始化的全局数据
- .rodata段 - 程序的"参考资料",存放不可修改的常量数据
这些分段机制体现了计算机科学中的重要权衡:
- 空间 vs 时间:.bss段节省磁盘空间但需要运行时初始化
- 安全 vs 灵活:.text/.rodata只读保护安全,.data/.bss可写提供灵活
- 共享 vs 私有:代码和常量可共享,数据需私有
掌握这些概念有助于:
- 编写更高效的程序
- 调试内存相关错误
- 理解程序底层行为
- 进行系统级编程和优化
