C++变量存储与ELF段布局详解_从const全局到rodata与nm_readelf验证实践
一道常见面试题:const 全局变量落在 .data 还是 .bss? 若只背「非零进 .data、零进 .bss」,容易忽略 只读语义与段权限 :在典型 Linux ELF + GCC/Clang 下,文件作用域的 const 已初始化整型常量 往往进 .rodata (只读数据段),由加载器映射为 不可写页 ,与 .data / .bss 的可读写页 分离。本文从 段语义 讲到 nm / readelf 自证,并交代 C++ 链接属性 、编译器差异 与 局部 const 的常见落点,便于面试与排障对照。
目录
- [1. 面试题:先给结论与边界](#1. 面试题:先给结论与边界)
- [2. 进程虚拟地址里常见「段」在说什么](#2. 进程虚拟地址里常见「段」在说什么)
- [3. .data 与 .bss:谁占磁盘、谁运行时清零](#3. .data 与 .bss:谁占磁盘、谁运行时清零)
- [4. .rodata:const 全局为何常在这里](#4. .rodata:const 全局为何常在这里)
- [5. 动手验证:nm 符号类型速查](#5. 动手验证:nm 符号类型速查)
- [6. readelf:看段地址与顺序](#6. readelf:看段地址与顺序)
- [7. 速查表与常见误区](#7. 速查表与常见误区)
- [8. 延伸阅读与免责声明](#8. 延伸阅读与免责声明)
1. 面试题:先给结论与边界
| 问题 | 典型答案(ELF + g++/Clang,Linux x86_64 常见) |
|---|---|
const int g = 10;(文件域) |
多在 .rodata ,不可写 ;nm 常为 r。 |
int g = 10; |
.data ,nm 常为 D (全局)或 d (static 文件域)。 |
int g; 或 int g = 0; |
多在 .bss (零或未初始化由运行时清零),nm 常为 B / b。 |
边界 :具体是否完全进 .rodata、是否与其他常量合并、是否 mergeable ,随 优化级别(-O2) 、是否取址 、是否 extern const 跨 TU 等变化;以本机 nm/readelf 与编译器文档为准 。下文示例默认 -O0,便于对照符号。
面试官常通过这个题看什么
| 考察点 | 说明 |
|---|---|
| 不止语法糖 | 能否把 const 与 存储期、链接、段权限 联系起来,而不是只背「常量不能改」。 |
| UB 与信号 | 是否知道 强转去掉 const 再写 是 未定义行为(UB) ;在 Linux 上若对象真在 只读页 ,常见表现是 SIGSEGV,与「改没改成」无关。 |
| Debugging 习惯 | 遇到「符号找不到 / 多重定义 / 段异常」时,会不会用 nm、 objdump -t、 readelf 对照 TU(翻译单元) 与 VMA,而不是只盯着源码猜。 |
2. 进程虚拟地址里常见「段」在说什么
下面为 教科书式示意 (真实地址与是否合并 PIE 、ASLR 、链接脚本有关),表达的是 相对顺序、权限分工 与后文 「.text 与 .rodata 常同映射为只读」 的呼应。权限列为进程视角常见简写(R ead / W rite / eX ecute;- 表示无)。
text
High Address
+------------------+ Permissions 说明
| Stack | RW- 局部自动变量、调用帧
+------------------+
| ↑ |
| heap growth |
+------------------+
| Heap | RW- malloc / new
+------------------+
| .bss | RW- 运行时清零的可写全局/静态
+------------------+
| .data | RW- 带非零初值映像的可写全局/静态
+------------------+
| .rodata | R-- <--- const 全局、字符串字面量等(只读)
+------------------+
| .text | R-X 机器码(一般不可写、可执行)
Low Address
要点 :.text 与 .rodata 常为 非可写 映射,便于 页权限隔离 与 TLB 行为;.data/.bss 为 可读写 。.rodata 与 .text 在 VMA 上常相邻,便于操作系统用 同一类只读(及代码段的 RX)策略 管理相邻页。
3. .data 与 .bss:谁占磁盘、谁运行时清零
| 段 | 典型内容 | 可执行文件里 |
|---|---|---|
.data |
已初始化且 在映像里要占位的非零初值 | 占磁盘,加载时拷入 RW 页 |
.bss |
未初始化 或 全零初值 的可写全局/静态 | 常 不占 磁盘字节(NOBITS),只在内存占位,由加载/启动路径 清零 |
直觉 :巨大全零数组若硬塞进 .data,会把 ELF 撑胖;放 .bss 只记录大小更省镜像体积。
4. .rodata:const 全局为何常在这里
- 语义 :
const对象 不应通过合法 C++ 语义被改写 ;放进 可写.data会与「只读」目标冲突(仍可能通过未定义行为改内存,但 不应 被映射策略鼓励)。 - 实现 :编译器把「编译期已知、只读」数据放进
.rodata,映射为 RO ,越界写易 SIGSEGV。 - 字符串字面量 :如
"hello"的存储体,通常也在.rodata(char*指向它时,改p[0]常崩溃,即此类权限问题)。
C++ 链接 :文件域 const int x = 1; 默认 内部链接 (等价于 static const 的文件内可见性),nm 里常出现 _ZL... 风格 的修饰名;若需要跨翻译单元共享,通常用 extern const int x; 在某处定义------符号形态与是否仍进 .rodata 需以实际 nm 为准。
5. 动手验证:nm 符号类型速查
5.1 示例源码
cpp
// segdemo.cpp --- 建议用 g++ -O0 -g 编译便于对照
const int a = 10;
const int b = 0;
int c;
int d = 9;
static int e;
static int f = 10;
int main() { return a + b + c + d + e + f; }
5.2 命令
bash
g++ -O0 -g segdemo.cpp -o segdemo
nm -C --defined-only segdemo | sort
5.3 如何读第二列类型(常见子集)
nm 字母 |
常见含义 | 常与哪类段对应 |
|---|---|---|
r |
read-only data | .rodata |
D / d |
已初始化 data object | .data (大写/小写与 全局 vs static 可见性相关,依 nm 手册) |
B / b |
BSS | .bss |
T / t |
text(代码) | .text |
你应能在输出里看到 a/b 一带为 r ,d/f 为 D/d ,c/e 为 B/b (具体符号名是否被修饰取决于 C++ ABI 与是否 extern "C")。
6. readelf:看段地址与顺序
bash
readelf -S segdemo
关注 .text、.rodata、.data、.bss 的 VMA 与 Align ;常见现象是 .rodata VMA 紧挨或靠近 .text ,而 .data/.bss 落在更高 VMA 区域 (与链接脚本、PIE 有关)。这支持「代码与只读数据共享只读映射」的工程叙述。
发布或内部分享时,可附一张本机终端 readelf -S segdemo 的截图(高亮上述四段),读者对 VMA 顺序 一眼更稳。
7. 速查表与常见误区
7.1 速查(文件域 / 静态存储期,Linux ELF 常见)
| 写法 | 常见段 | nm 线索 |
|---|---|---|
const int x = k; |
.rodata |
r |
int x = 非零; |
.data |
D/d |
int x; / int x = 0; |
.bss |
B/b |
| 字符串字面量 | .rodata |
常表现为 r 或与合并常量相邻 |
函数内 const int y = 3; |
多为 栈上常量 (或优化进立即数),不 与全局 .rodata 混谈 |
7.2 误区与陷阱示例
| 误区 | 更正 |
|---|---|
「const int g = 0 一定在 .bss」 |
零初值 可写 全局才典型进 .bss;const 只读 常在 .rodata。 |
「nm 大小写只是大小写」 |
在 GNU nm 里常区分 全局(Global)可见 与 局部(Local)/ 文件内 static 等 符号绑定属性 ,以手册为准。 |
| 「所有平台都一样」 | Windows PE 中类似只读常量区常用 .rdata 等节名表达;嵌入式裸机 、不同链接脚本 与 ELF 也不尽相同;本文以 Linux ELF 为主。 |
陷阱代码(UB,勿依赖「是否崩溃」当逻辑) :通过 const_cast 或 C 风格强转 去掉 const 再写入,若对象实际位于 只读映射 ,在 Linux 上常见 Segmentation fault ;即便未立刻崩溃,仍是 C++ 未定义行为。
cpp
const int g_const = 10;
int main() {
int* p = const_cast<int*>(&g_const); // 仍不保证可写
*p = 20; // Undefined Behavior
return 0;
}
实际现象以 页权限、编译器是否把常量完全优化掉 为准;教学上可用 readelf -l 看 LOAD 段 RWE 与 gdb/catch syscall 对照,但结论应写 UB,不要写「一定崩 / 一定不崩」。
8. 延伸阅读与免责声明
| 检索线索 | 用途 |
|---|---|
man nm / man readelf |
符号字母与段表字段权威说明。 |
| ELF 与 Linkers and Loaders | 段、节、加载与权限。 |
GCC/Clang -fdata-sections、LTO |
可能改变合并与段布局时的对照方法。 |
objdump -h / -t |
与 readelf 互补看节名与符号表。 |
免责声明 :段布局、符号名修饰与 const 合并 行为随 编译器版本、优化、语言标准模式 变化;面试回答建议句式为「在 Linux ELF + g++/Clang 的典型配置下,我会用 nm/readelf 验证为... 」,避免绝对化。上文 §1 已归纳面试官常见考察点,可与本节工具链 disclaimer 一并使用。
记住:段名是工具链与 OS 加载约定的结果;会查 nm/readelf 比背「标准答案」更经得起追问。