Glibc 的全域建構與解構

用 gcc 來 compile 一個簡單的程式時,如果仔細看,會發現 gcc 還連結了 glibc 中的crti.o 及 crtn.o 進來。這兩個 object file 是什麼呢?


在 ELF 檔中,.init 與 .fini 區段的內容為在 main 之前/之後會執行的程式碼。事實上它們最後會被連結成 _init() 及 _finit() 兩個函式。而 _init() 的開始部份放在 crti.o 中的 .init 區段,結束部份放在 crtn.o 中的 .init 區段。_finit() 開始的部份放在 crti.o 中的 .finit 區段,結束部份放在 crtn.o 中的 .finit 區段。


換句話說,連結時的順序會變成:ld crti.o a.o crtn.o。而 _init() 的組成等於是:


_init:


XXX   <-- 來自 crti.o
YYY   <-- 來自 a.o
ZZZ   <-- 來自 crtn.o


同樣地,_finit() 也是類似。


如果程式使用了 class,並且宣告了一個該 class 的全域變數。則我們可以知道,在進入 main 之前,必需先呼叫這些全域變數的建構。相同的,在離開 main 之後,也必需做解構動作。在 ELF 檔中,全域變數的建構是放在 .ctors 區段。.ctors 裡頭有一個指標,指向一個執行建構動作的函式。換句話說,針對每個 object file,都會有一個 .ctors 區段,裡頭單單只存放了一個指標。在連結時 linker 會將所有 .ctors 區段合併,變成一個指標陣列。


那問題來了,我們怎麼知道這個指標陣列有幾個元素?這時候 gcc 中的 crtbegin.o 與 crtend.o 就出場啦!crtbegin.o 裡頭也有一個 .ctors 區段,裡頭預設放 0xFFFFFFFF。連結時它做為 .ctors 區段的開頭,linker 會將指標陣列的大小填入這裡。這個區段還會將起始位址定義成 __CTOR_LIST__,這樣子它就指向 .ctors 段的第一個元素,也就是該指標陣列的大小。


crtend.o 的 .ctors 則是寫了一個 0,它並定義了 __CTOR_END__,指向該指標陣列的末端。當程序執行時,在 _init() 中就可以先讀出 __CTOR_LIST__ 第一個元素,然後用一個 for 迴圈持續呼叫陣列中的指標,直到等於 0 為止。


到這裡應該就可以看出來,crtbegin.o 與 crtend.o 是用在有全域變數建構/解構時,需要連結進來的檔案。因此統合剛剛的 _init() 與 _finit(),連結的順序變成:


ld crti.o crtbegin.o a.o crtend.o crtn.o


留言

熱門文章