C++ 面試題:const 全局變量存在 .data 段還是 .bss 段?
這是今年騰訊最新春招實(shí)習(xí)生的面試題,不知道是面試者總結(jié)手誤寫錯了還是面試官就這么問的,這問的就沒有正確答案。
我們都知道:全局變量和靜態(tài)變量一般存放在 .data 或者 .bss 段,具體取決于是否被初始化?!?/p>
- 如果全局變量被顯式初始化為非零值,它們通常放在 .data 段;
- 而如果未初始化或者初始化為零,則可能放在 .bss 段?!?/li>
.data 段(已初始化數(shù)據(jù)段):存放顯式初始化且初始值非零的全局變量和靜態(tài)變量?!?/p>
int global_var = 42; // 存儲在 .data 段
static int static_var = 10; // 存儲在 .data 段
.bss 段(未初始化數(shù)據(jù)段):存放未顯式初始化或初始化為零的全局變量和靜態(tài)變量(節(jié)省磁盤空間,加載時自動清零)。
int global_uninit; // 存儲在 .bss 段
static int static_zero = 0; // 存儲在 .bss 段(初始化為零)
為什么說節(jié)省空間?
比如,如果一個全局?jǐn)?shù)組很大但未初始化,那么在編譯后的文件中,BSS段可能只是記錄這個數(shù)組的大小,而不是實(shí)際存儲所有的零值。當(dāng)程序加載到內(nèi)存時,系統(tǒng)會根據(jù)需要分配內(nèi)存并清零,這樣在磁盤上的可執(zhí)行文件就不會包含這些零值的數(shù)據(jù),從而減少了文件的大小?!?/p>
const 全局變量不一樣
首先 const全局變量必須顯式初始化(否則編譯報錯),因此不存在“未初始化”的情況?!?/p>
const 全局變量會存放在只讀數(shù)據(jù)段(.rodata),而非.data或.bss段。因?yàn)?data段存放可讀寫的已初始化數(shù)據(jù),而 const 變量需要保證只讀屬性,編譯器會將其分配到.rodata段?!?/p>
我們測試驗(yàn)證下:
編寫如下測試代碼:
const int a = 10;
const int b = 0;
int main()
{
return 0;
}
然后執(zhí)行 g++ 1.cpp 生成 a.out, 我們使用 nm 工具查看符號表,可以發(fā)現(xiàn)輸出:
0000000000400530 r _ZL1a
0000000000400534 r _ZL1b
這里 r 表示符號位于只讀數(shù)據(jù)段?!?/p>
所以const全局變量若初始化(無論是否為非零),通常位于.rodata段?!?/p>
nm輸出中 B b T 這種字符,代表什么意思
在 nm 的輸出中,符號類型通過 大小寫字母 表示符號的存儲位置和屬性。以下是常見符號類型的解釋:
字母 | 大小寫 | 解釋 |
B | 大寫 | 未初始化的全局變量 |
b | 小寫 | 未初始化的靜態(tài)變量 |
T | 大寫 | 全局函數(shù)/代碼 |
t | 小寫 | 靜態(tài)函數(shù)/代碼 |
r | 小寫 | 只讀數(shù)據(jù) |
D | 大寫 | 已初始化的全局變量 |
d | 小寫 | 已初始化的靜態(tài)變量 |
U | 大寫 | 未定義符號 |
關(guān)鍵區(qū)別:
- 大寫字母(B/T/D):表示符號是全局的(全局變量或函數(shù)),可以被其他文件訪問。
- 小寫字母(b/t/d):表示符號是局部的或靜態(tài)的(作用域限于當(dāng)前文件)。
- 大小寫 B/b:都位于 .bss 段,但作用域不同(大寫為全局,小寫為靜態(tài))。
- 大小寫 T/t:都位于 .text 段,但作用域不同(大寫為全局函數(shù),小寫為靜態(tài)函數(shù))。
我們寫個測試代碼再驗(yàn)證下:
const int a = 10; //const 初始化
constint b = 0;//const 初始化為0
int c; //全局 未初始化
int d = 9; //全局 初始化
int dd = 0; //全局 初始化0
staticint e; //靜態(tài) 未初始化
staticint f = 10; //靜態(tài)初始化
staticint g = 0; //靜態(tài)初始化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
這是截圖證明上面不是我虛造的:
可以發(fā)現(xiàn):
- a b 前面的符號是 r,表示存放在 .rodata 段
- e g 前面的符號是 b,表示存放在 .bss 段
- f 前面的符號是 d,表示存放在 .data 段
- c dd 前面的符號是 B ,表示存放在 .bss 段
- D 前面的符號是 D,表示存放在 .data 段
所以總結(jié)就是:
- .bss 段:未初始化或零初始化的全局/靜態(tài)變量(B 和 b)。
- .text 段:可執(zhí)行代碼(T 和 t)。
- .rodata 段:只讀數(shù)據(jù)(r)。
- .data 段:已初始化的全局/靜態(tài)變量(D 和 d)。
rodata 段在內(nèi)存哪個位置呢?
答案是:在程序的內(nèi)存布局中,.rodata 段(只讀數(shù)據(jù)段) 通常位于 代碼段(.text)和數(shù)據(jù)段(.data/.bss)之間。
我們看前面nm 輸出中,.rodata 段的符號地址集中在 0x400520 到 0x40062c 范圍內(nèi):
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 結(jié)束附近
(1) .rodata 段地址范圍:0x400520 ~ 0x40062c 該段緊鄰代碼段(.text 段),代碼段的符號地址(如 _start、main 等)在 0x4003d0 ~ 0x400514 之間
(2) .data 和 .bss 段地址范圍:
- .data 段地址:0x6001018 ~ 0x6001020
- .bss 段地址:0x6001020 ~ 0x6001030 這些段位于高地址區(qū)域(0x60...),與 .rodata 段(0x40...)明顯分離?!?/li>
這種布局體現(xiàn)為:
低地址(0x400000) → 高地址(0x600000)
.text → .rodata → ... → .data → .bss
為什么 .rodata 和 .text 都在 0x40... 地址?
- 權(quán)限隔離: .text(代碼)和 .rodata(只讀數(shù)據(jù))通常被標(biāo)記為 可讀+可執(zhí)行(r-x),而 .data 和 .bss 被標(biāo)記為 可讀+可寫(rw-)。 操作系統(tǒng)會將權(quán)限相同的段映射到相鄰區(qū)域,因此 .rodata 與 .text 位于同一地址范圍內(nèi)(0x40...)?!?/li>
- 優(yōu)化內(nèi)存使用: 將只讀數(shù)據(jù)與代碼相鄰,可以減少內(nèi)存碎片,提高緩存命中率?!?/li>
驗(yàn)證 .rodata 的物理位置
可以通過 readelf工具直接查看段頭信息:
readelf -S a.out
輸出示例(關(guān)鍵部分):
[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段與其他段的關(guān)系
段名 | 內(nèi)容 | 讀寫權(quán)限 | 位置特征 |
.text | 可執(zhí)行代碼 | 只讀 + 可執(zhí)行 | 最低地址區(qū)域 |
.rodata | 只讀常量(如 | 只讀 | 緊鄰 |
.data | 已初始化全局/靜態(tài)變量 | 可讀寫 | 高于 |
.bss | 未初始化全局/靜態(tài)變量 | 可讀寫 | 緊鄰 |
總結(jié)
- 答案:const 全局變量既不在 .data 段也不在 .bss 段,而是在 .rodata 段
- 位置:.rodata 段位于 代碼段(.text)和數(shù)據(jù)段(.data/.bss)之間?!?/li>
- 權(quán)限:與代碼段共享只讀屬性,但不可執(zhí)行?!?/li>