C++ 面試題:const 全局變量存在 .data 段還是 .bss 段?
這是今年騰訊最新春招實習生的面試題,不知道是面試者總結手誤寫錯了還是面試官就這么問的,這問的就沒有正確答案。
我們都知道:全局變量和靜態變量一般存放在 .data 或者 .bss 段,具體取決于是否被初始化。
- 如果全局變量被顯式初始化為非零值,它們通常放在 .data 段;
- 而如果未初始化或者初始化為零,則可能放在 .bss 段。
.data 段(已初始化數據段):存放顯式初始化且初始值非零的全局變量和靜態變量。
int global_var = 42; // 存儲在 .data 段
static int static_var = 10; // 存儲在 .data 段
.bss 段(未初始化數據段):存放未顯式初始化或初始化為零的全局變量和靜態變量(節省磁盤空間,加載時自動清零)。
int global_uninit; // 存儲在 .bss 段
static int static_zero = 0; // 存儲在 .bss 段(初始化為零)
為什么說節省空間?
比如,如果一個全局數組很大但未初始化,那么在編譯后的文件中,BSS段可能只是記錄這個數組的大小,而不是實際存儲所有的零值。當程序加載到內存時,系統會根據需要分配內存并清零,這樣在磁盤上的可執行文件就不會包含這些零值的數據,從而減少了文件的大小。
const 全局變量不一樣
首先 const全局變量必須顯式初始化(否則編譯報錯),因此不存在“未初始化”的情況。
const 全局變量會存放在只讀數據段(.rodata),而非.data或.bss段。因為.data段存放可讀寫的已初始化數據,而 const 變量需要保證只讀屬性,編譯器會將其分配到.rodata段。
我們測試驗證下:
編寫如下測試代碼:
const int a = 10;
const int b = 0;
int main()
{
return 0;
}
然后執行 g++ 1.cpp 生成 a.out, 我們使用 nm 工具查看符號表,可以發現輸出:
0000000000400530 r _ZL1a
0000000000400534 r _ZL1b
這里 r 表示符號位于只讀數據段。
所以const全局變量若初始化(無論是否為非零),通常位于.rodata段。
nm輸出中 B b T 這種字符,代表什么意思
在 nm 的輸出中,符號類型通過 大小寫字母 表示符號的存儲位置和屬性。以下是常見符號類型的解釋:
字母 | 大小寫 | 解釋 |
B | 大寫 | 未初始化的全局變量 |
b | 小寫 | 未初始化的靜態變量 |
T | 大寫 | 全局函數/代碼 |
t | 小寫 | 靜態函數/代碼 |
r | 小寫 | 只讀數據 |
D | 大寫 | 已初始化的全局變量 |
d | 小寫 | 已初始化的靜態變量 |
U | 大寫 | 未定義符號 |
關鍵區別:
- 大寫字母(B/T/D):表示符號是全局的(全局變量或函數),可以被其他文件訪問。
- 小寫字母(b/t/d):表示符號是局部的或靜態的(作用域限于當前文件)。
- 大小寫 B/b:都位于 .bss 段,但作用域不同(大寫為全局,小寫為靜態)。
- 大小寫 T/t:都位于 .text 段,但作用域不同(大寫為全局函數,小寫為靜態函數)。
我們寫個測試代碼再驗證下:
const int a = 10; //const 初始化
constint b = 0;//const 初始化為0
int c; //全局 未初始化
int d = 9; //全局 初始化
int dd = 0; //全局 初始化0
staticint e; //靜態 未初始化
staticint f = 10; //靜態初始化
staticint g = 0; //靜態初始化0
intmain()
{
return0;
}
編譯后用 nm 查看:
nm a.out
0000000000601024 B __bss_start
0000000000601028 B c
0000000000601024 b completed.7247
000000000060101c D d
0000000000601018 D __data_start
0000000000601018 W data_start
000000000060102c B dd
0000000000400400 t deregister_tm_clones
0000000000400470 t __do_global_dtors_aux
0000000000600e28 t __do_global_dtors_aux_fini_array_entry
0000000000400528 R __dso_handle
0000000000600e30 d _DYNAMIC
0000000000601024 D _edata
0000000000601038 B _end
0000000000400514 T _fini
00000000004004a0 t frame_dummy
0000000000600e20 t __frame_dummy_init_array_entry
000000000040062c r __FRAME_END__
0000000000601000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000400538 r __GNU_EH_FRAME_HDR
00000000004003b8 T _init
0000000000600e28 t __init_array_end
0000000000600e20 t __init_array_start
0000000000400520 R _IO_stdin_used
0000000000400510 T __libc_csu_fini
00000000004004b0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
00000000004004a2 T main
0000000000400430 t register_tm_clones
00000000004003d0 T _start
0000000000601028 D __TMC_END__
0000000000400530 r _ZL1a
0000000000400534 r _ZL1b
0000000000601030 b _ZL1e
0000000000601020 d _ZL1f
0000000000601034 b _ZL1g
這是截圖證明上面不是我虛造的:
可以發現:
- a b 前面的符號是 r,表示存放在 .rodata 段
- e g 前面的符號是 b,表示存放在 .bss 段
- f 前面的符號是 d,表示存放在 .data 段
- c dd 前面的符號是 B ,表示存放在 .bss 段
- D 前面的符號是 D,表示存放在 .data 段
所以總結就是:
- .bss 段:未初始化或零初始化的全局/靜態變量(B 和 b)。
- .text 段:可執行代碼(T 和 t)。
- .rodata 段:只讀數據(r)。
- .data 段:已初始化的全局/靜態變量(D 和 d)。
rodata 段在內存哪個位置呢?
答案是:在程序的內存布局中,.rodata 段(只讀數據段) 通常位于 代碼段(.text)和數據段(.data/.bss)之間。
我們看前面nm 輸出中,.rodata 段的符號地址集中在 0x400520 到 0x40062c 范圍內:
0000000000400520 R _IO_stdin_used # .rodata 起始附近
0000000000400530 r _ZL1a # const int a = 10
0000000000400534 r _ZL1b # const int b = 0
000000000040062c r __FRAME_END__ # .rodata 結束附近
(1) .rodata 段地址范圍:0x400520 ~ 0x40062c 該段緊鄰代碼段(.text 段),代碼段的符號地址(如 _start、main 等)在 0x4003d0 ~ 0x400514 之間
(2) .data 和 .bss 段地址范圍:
- .data 段地址:0x6001018 ~ 0x6001020
- .bss 段地址:0x6001020 ~ 0x6001030 這些段位于高地址區域(0x60...),與 .rodata 段(0x40...)明顯分離。
這種布局體現為:
低地址(0x400000) → 高地址(0x600000)
.text → .rodata → ... → .data → .bss
為什么 .rodata 和 .text 都在 0x40... 地址?
- 權限隔離: .text(代碼)和 .rodata(只讀數據)通常被標記為 可讀+可執行(r-x),而 .data 和 .bss 被標記為 可讀+可寫(rw-)。 操作系統會將權限相同的段映射到相鄰區域,因此 .rodata 與 .text 位于同一地址范圍內(0x40...)。
- 優化內存使用: 將只讀數據與代碼相鄰,可以減少內存碎片,提高緩存命中率。
驗證 .rodata 的物理位置
可以通過 readelf工具直接查看段頭信息:
readelf -S a.out
輸出示例(關鍵部分):
[11] .text PROGBITS 00000000004003d0 000003d0
0000000000000141 0000000000000000 AX 0 0 16
[12] .fini PROGBITS 0000000000400514 00000514
0000000000000009 0000000000000000 AX 0 0 4
[13] .rodata PROGBITS 0000000000400520 00000520
0000000000000018 0000000000000000 A 0 0 8
[21] .data PROGBITS 0000000000601018 00001018
000000000000000c 0000000000000000 WA 0 0 4
[22] .bss NOBITS 0000000000601024 00001024
0000000000000014 0000000000000000 WA 0 0 4
.rodata 的物理地址:0x400520,與 nm 輸出的符號地址一致。
完整信息這里也放出來:
readelf -S a.out
There are34 section headers, starting atoffset0x2708:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 000000000000000000000000
00000000000000000000000000000000 0 0 0
[ 1] .interp PROGBITS 000000000040023800000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040025400000254
00000000000000200000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 000000000040027400000274
00000000000000240000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 000000000040029800000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
00000000000000480000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 000000000040030000000300
000000000000005f 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040036000000360
00000000000000060000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 000000000040036800000368
00000000000000200000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 000000000040038800000388
00000000000000300000000000000018 A 5 0 8
[10] .init PROGBITS 00000000004003b8 000003b8
00000000000000170000000000000000 AX 0 0 4
[11] .text PROGBITS 00000000004003d0 000003d0
00000000000001410000000000000000 AX 0 0 16
[12] .fini PROGBITS 000000000040051400000514
00000000000000090000000000000000 AX 0 0 4
[13] .rodata PROGBITS 000000000040052000000520
00000000000000180000000000000000 A 0 0 8
[14] .eh_frame_hdr PROGBITS 000000000040053800000538
000000000000002c 0000000000000000 A 0 0 4
[15] .eh_frame PROGBITS 000000000040056800000568
00000000000000c8 0000000000000000 A 0 0 8
[16] .init_array INIT_ARRAY 0000000000600e2000000e20
00000000000000080000000000000008 WA 0 0 8
[17] .fini_array FINI_ARRAY 0000000000600e2800000e28
00000000000000080000000000000008 WA 0 0 8
[18] .dynamic DYNAMIC 0000000000600e3000000e30
00000000000001c0 0000000000000010 WA 6 0 8
[19] .got PROGBITS 0000000000600ff0 00000ff0
00000000000000100000000000000008 WA 0 0 8
[20] .got.plt PROGBITS 000000000060100000001000
00000000000000180000000000000008 WA 0 0 8
[21] .data PROGBITS 000000000060101800001018
000000000000000c 0000000000000000 WA 0 0 4
[22] .bss NOBITS 000000000060102400001024
00000000000000140000000000000000 WA 0 0 4
[23] .comment PROGBITS 000000000000000000001024
000000000000002c 0000000000000001 MS 0 0 1
[24] .debug_aranges PROGBITS 000000000000000000001050
00000000000001000000000000000000 0 0 16
[25] .debug_info PROGBITS 000000000000000000001150
00000000000003130000000000000000 0 0 1
[26] .debug_abbrev PROGBITS 000000000000000000001463
00000000000001950000000000000000 0 0 1
[27] .debug_line PROGBITS 0000000000000000000015f8
000000000000023f 0000000000000000 0 0 1
[28] .debug_str PROGBITS 000000000000000000001837
00000000000002bd 0000000000000001 MS 0 0 1
[29] .debug_loc PROGBITS 000000000000000000001af4
00000000000001550000000000000000 0 0 1
[30] .debug_ranges PROGBITS 000000000000000000001c50
00000000000000800000000000000000 0 0 16
[31] .symtab SYMTAB 000000000000000000001cd0
00000000000007080000000000000018 32 56 8
[32] .strtab STRTAB 0000000000000000000023d8
00000000000001d8 0000000000000000 0 0 1
[33] .shstrtab STRTAB 0000000000000000000025b0
00000000000001520000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
.rodata段與其他段的關系
段名 | 內容 | 讀寫權限 | 位置特征 |
.text | 可執行代碼 | 只讀 + 可執行 | 最低地址區域 |
.rodata | 只讀常量(如 | 只讀 | 緊鄰 |
.data | 已初始化全局/靜態變量 | 可讀寫 | 高于 |
.bss | 未初始化全局/靜態變量 | 可讀寫 | 緊鄰 |
總結
- 答案:const 全局變量既不在 .data 段也不在 .bss 段,而是在 .rodata 段
- 位置:.rodata 段位于 代碼段(.text)和數據段(.data/.bss)之間。
- 權限:與代碼段共享只讀屬性,但不可執行。