Mach-O Inside: BSS Section

xue001發表於2023-10-29

1 BSS 起源

BSS(Block Started by Symbol)這個詞最初是 UA-SAP 彙編器(United Aircraft Symbolic Assembly Program)中的一個偽指令,用於為符號預留一塊記憶體空間。該彙編器由美國聯合航空公司於 20 世紀 50 年代中期為 IBM 704 大型機所開發。

後來,BSS 這個詞被作為關鍵字引入了 IBM 709 和 7090/94 機型上的標準彙編器 FAP(Fortran Assembly Program),用於定義符號並且為該符號預留給定數量的未初始化空間。

Unix FAQ section1.3 裡面有 Unix 和 C 語言之父 Dennis Rithcie 對 BSS 這個詞由來的解釋。

2 Macho-O 裡的 __bss

一般對 BSS Section 的介紹會說裡面存放的是未初始化的全域性變數未初始化的區域性靜態變數,但是實際對 Mach-O 目標檔案(.o 檔案)進行檢視,發現情況並不是這樣。

假設有a.ha.m檔案,a.h裡面宣告瞭一個函式 test(),程式碼如下:

void test();

a.m裡面定義了 2 個全域性變數和 2 個靜態變數:

global_init_var 全域性變數初始化為 84,global_uninit_var全域性變數未進行初始化。

static_init_var 靜態變數初始化為 85,static_uninit_var靜態變數未進行初始化。

// 1. 初始化全域性變數
int global_init_var = 84;

// 2. 未初始化全域性變數
int global_uninit_var;

void test(void) {
    // 3. 初始化靜態變數
    static int static_init_var = 85;
    
    // 4. 未初始化靜態變數
    static int static_uninit_var;
    
    // 5. 列印所有變數之和
    printf("%d", global_init_var + global_uninit_var + static_init_var + static_uninit_var);
}

建立一個 iOS 工程,將a.ha.m檔案拖入到工程,並且進行編譯。編譯成功之後在工程 DevivedData 目錄下找到a.o檔案,並將這個檔案拖入到 MachOView 工具中進行檢視:

image

上圖右側 Size 欄位代表 __bss Section 在虛擬記憶體中所佔用的大小,其值為 4 位元組,剛好是一個 int 型別的大小,表明為初始化的全域性變數global_uninit_var和未初始化的靜態變數static_uninit_var只有一個位於 __bss Section 中。

offset 欄位代表在 Macho-O 目標檔案 a.o 中,__bss Section 距離 a.0 目標檔案的起始位置偏移量為 0,而在目標檔案 a.o 的起始位置是檔案頭 Header,檔案頭和 __bss Section 不可能重合,所以這表明 __bss Section 在 Mach-O 檔案裡面不會佔用任何磁碟空間,只有在虛擬記憶體中,才會為 __bss Section 分配指定的空間。

flags 欄位值為S_ZEROFILL,這表明當在虛擬記憶體裡為 __bss Section 分配空間時,這個空間會全部被初始化為 0。

透過檢視a.o的符號表,可以發現未初始化的全域性global_uninit_var變數位於 __DATA Segment 的 __common Section 中:

image

只有未初始化的區域性靜態變數static_uninit_var 位於 __DATA Segment 的 __bss Section:

image

同樣可以透過 MachOView 檢視 __data Section Header,可以看到這個 Section 在虛擬記憶體中佔用的大小為 8 個位元組,剛好是 2 個 int 型別的大小。同時,這個 Section 位於距離 Mach-O 目標檔案 a.o 起始位置 1944 位元組處:

image

使用 MachOView 檢視這個 Section 如下:

image

上圖右側pFile欄位代表當前 Section 在 Mach-O 檔案中的偏移量,其值是 16 進位制 0x798,換算成 10 進位制剛好是 1944,整好複合 __data Section Header 中的偏移量offset

Data LO欄位儲存了 8 個位元組的資料,每 4 個位元組代表一個整數。由於 ARM 位元組序列是小端在前(Little-Endian),這兩個整數的值應該是 0x00000054 和 0x00000055,換算成 10 進位制就是 84 和 85,正好是全域性變數global_init_varstatic_init_var的值。

3 Mach-O 裡的 __common

上面提到未初始化的全域性變數global_uninit_var並不位於 __bss Section,而是位於 __common Section。

透過 MachOView 檢視 __common Section Header 如下圖所示:

image

上圖右側size欄位、offset欄位與S_ZEROFILL代表的意義與 __bss Section Header 中的一樣,從 offset 的值為 0 可知,__common Section 在 Macho-O 目標檔案 a.o 中也不佔用空間,只會在虛擬記憶體中為其分配大小為 4 位元組的空間,並且全部初始化為 0。

為什麼要把未初始化的全域性變數和未初始化的靜態變數分開放置?

這和具體的語言與編譯器的實現有關,有的編譯器會將未初始化的全域性變數和未初始化的靜態變數一起放在 __bss Section,有的編譯器會像這樣分開放置。

4 初始化值為 0

如果全域性變數和靜態變數初始化值為 0,效果和未初始化一樣。因為對於未初始化的全域性變數和靜態變數,當載入到虛擬記憶體之後,也會被初始化為 0,所以會有這樣的最佳化。

參考資料

程式設計師的自我修養-連結、裝載與庫

相關文章