天天看點

從程式員角度看ELF

特殊說明(by jfo)

  對于static-linked或shared-linked的ELF可執行檔案,他們的入口點都是 _start,

  然後由 _start 函數調用 _init 執行相關的 .init 節中的初始化代碼!(just disassemble the code)

  這說明核心在加載image後,在控制轉入_start之前,_init 沒有被調用;

  對于需要動态連結的可執行檔案,核心将控制權轉移給interpreter,

  interpreter 在完成連結工作後,将控制權轉移給 _start ,也不會直接執行

  .init 節中的代碼!(這裡針對ELF可執行檔案,對于共享庫的.init段,還是由interpreter來調用的!!!see 啟動過程::共享庫的初始化)

  而dlopen 一個 .so 共享庫後,_init 函數在傳回前會被調用,.so 共享庫

  是沒有 _start 的!

  This is the example, which makes a shared .so file executable:

  http://hi.baidu.com/j%5Ffo/blog/item/1568184cf23d6dfad72afca3.html

  一個連結的example

  ld -o test -e_start -dynamic-linker=/lib/ld-linux.so.2 crt1.o crti.o crtbegin.o test.o -L /usr/lib/gcc/i386-redhat-linux/4.0.0/ -ldl -lc crtend.o crtn.o

  crt1.o中含有_start

  1 0×080480c0 : /* entry point in .text */

  2 call __libc_init_first /* startup code in .text */

  3 call _init /* startup code in .init */

  4 call atexit /* startup code in .text */

  5 call main /* application main routine */

  6 call _exit /* returns control to OS */

  7 /* control never reaches here */

  crti.o、test.o、crtn.o中的.init section中的代碼共同組成了_init()函數,crti.o結尾代碼會call 下一條指令,也就是說跳轉到test.o中的.init section的代碼繼續執行,test.o中的.init代碼不必ret,由crtn.o中的ret傳回。

  crtend.o的.init代碼含有對__do_global_ctors_aux()的調用,這說明C++構造函數是在前面所有.o檔案(如 crti.o、crtbegin.o、test.o以及其他libc.a中的*.o)的.init代碼執行之後才開始構造的,為什麼放在最後,而不把對 __do_global_ctors_aux()的調用放在crtbegin.o中呢?那樣可能更直覺。

  其實也可 以了解,因為構造函數位于較高層次,很可能依賴于很多其他元素,如libc.a中的函數,是以先調用這些元素的.init代碼也合情合理,就像C++構造子類時要先構造其父類一樣。

  crtbegin.o的.fini代碼含有對__do_global_dtors_aux()的調用,這說明C++析構函數是在後面所有.o檔案(如 test.o、libc.a中的*.o、crtend.o、crtn.o)的.fini代碼執行之前就開始析構了,同樣也可以了解,應當先把位于較高層次的析構完成,再進行其他底層的析構代碼,就像C++先析構子類再析構其其父類一樣。

  crtbegin.o的.init代碼還有一個對frame_dummy的調用,這個函數主要作用是注冊exception frame(__register_frame_info_bases()函數),用于C++的異常處理機制(如復原unwind),在 __do_global_dtors_aux()中析構對象後會unregister exception frame(__deregister_frame_info_bases()函數)。

  ———————–

  特殊參數

  ”-Wl,-Bstatic”參數,實際上是傳給了****ld。訓示它與靜态庫連接配接,如果系統中隻有靜态庫當然就不需要這個參數了。 如果要和多個庫相連接配接,而每個庫的連接配接方式不一樣,比如上面的程式既要和libhello進行靜态連接配接,又要和libbye進行動态連接配接,其指令應為:

  $gcc testlib.o -o testlib -Wl,-Bstatic -lhello -Wl,-Bdynamic -lbye

  $gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 hello.o

  When creating an ELF shared object, set the internal DT_SONAME field to the

  specified name. When an executable is linked with a shared object which has a

  DT_SONAME field, then when the executable is run the dynamic linker will

  attempt to load the shared object specified by the DT_SONAME field rather than

  the using the file name given to the linker.

  啟動過程 (linker and loader)

  啟動動态連結器

  在作業系統運作程式時,它會像通常那樣将檔案的頁映射進來,但注意在可執行程式

  中存在一個INTERPRETER區段。這裡特定的解釋器是動态連結器,即ld.so,它自己也是ELF

  共享庫的格式。作業系統并非直接啟動程式,而是将動态連結器映射到位址空間的一個合适

  的位置,然後從ld.so處開始,并在棧中放傳入連結接器所需要的輔助向量(auxiliary vector)

  資訊。向量包括:

  AT_PHDR,AT_PHENT,和AT_PHNUM:程式頭部在程式檔案中的位址,頭部中每個表項的

  大小,和表項的個數。頭部結構描述了被加載檔案中的各個段。如果系統沒有将程式映射到

  記憶體中,就會有一個AT_EXECFD項作為替換,它包含被打開程式檔案的檔案描述符。

  AT_ENTRY:程式的起始位址,當動态連結器完成了初始化工作之後,就會跳轉到這個

  位址去。

  AT_BASE:動态連結器被加載到的位址。

  此時,位于ld.so起始處的自舉代碼找到它自己的GOT,其中的第一項(GOT[0])指向了ld.so文

  件中的DYNAMIC段。通過dynamic段,連結器在它自己的資料段中找到自己的重定位項表和

  重定位指針,然後解析例程需要加載的其它東西的代碼引用(Linux ld.so将所有的基礎例

  程都命名為由字串_dt_起頭,并使用專門代碼在符号表中搜尋以此字串開頭的符号并解析它

  們)。

  連結器然後通過指向程式符号表和連結器自己的符号表的若幹指針來初始化一個符号

  表鍊。從概念上講,程式檔案和所有加載到程序中的庫會共享一個符号表。但實際中連結器

  并不是在運作時建立一個合并後的符号表,而是将個個檔案中的符号表組成一個符号表鍊。

  每個檔案中都有一個散清單(一系列的散列頭部,每個頭部引領一個散列隊列)以加速符号

  查找的速度。連結器可以通過計算符号的散列值,然後通路相應的散列隊列進行查找以加速

  符号搜尋的速度。

  庫的查找

  連結器自身的初始化完成之後,它就會去尋找程式所需要的各個庫。程式的程式頭部

  有一個指針,指向dynamic段(包含有動态連結相關資訊)在檔案中的位置。在這個段中包

  含一個指針DT_STRTAB,指向檔案的字串表,和一個偏移量表DT_NEEDED,其中每一個表項

  包含了一個所需庫的名稱在字串表中的偏移量。

  對于每一個庫,連結器在以下位置搜尋庫:

  ● 是否dynamic段有一個稱為DT_RPATH的表項,它是由分号分隔開的可以搜尋庫的目錄清單。

  它可以通過一個指令行參數或者在程式連結時正常(非動态)連結器的環境變量來添加。它經

  常會被諸如資料庫類這樣需要加載一系列程式并可将庫放在單一目錄的子系統使用,

  ● 是否有一個環境符号LD_LIBRARY_PATH,它可以是由分号分隔開的可供連結器搜尋庫的目錄

  清單。這就可以讓開發者建立一個新版本的庫并将它放置在LD_LIBRARY_PATH的路徑中,這

  樣既可以通過已存在的程式來測試新的庫,或用來監測程式的行為。(因為安全原因,如果程

  序設定了set-uid,那麼這一步會被跳過)

  ● 連結器檢視庫緩沖檔案/etc/ld.so.conf,其中包含了庫檔案名和路徑的清單。如果要查找的

  庫名稱存在于其中,則采用檔案中相應的路徑。大多數庫都通過這種方法被找到(路徑末尾的

  檔案名稱并不需要和所搜尋的庫名稱精确比對,詳細請參看下面的庫版本章節)。

  ● 如果所有的都失敗了,就查找預設目錄/usr/lib,如果在這個目錄中仍沒有找到,就列印錯

  誤資訊,并退出執行。

  一旦找到包含該庫的檔案,動态連結器會打開該檔案,讀取ELF頭部尋找程式頭部,它

  指向包括dynamic段在内的衆多段。連結器為庫的文本和資料段配置設定空間,并将它們映射進

  來,對于BSS配置設定初始化為0的頁。從庫的dynamic段中,它将庫的符号表加入到符号表鍊

  中,如果該庫還進一步需要其它尚未加載的庫,則将那些新庫置入将要加載的庫連結清單中。

  在該過程結束時,所有的庫都被映射進來了,加載器擁有了一個由程式和所有映射進

  來的庫的符号表聯合而成的邏輯上的全局符号表。

  共享庫的初始化

  現在加載器再次檢視每個庫并處理庫的重定位項,填充庫的GOT,并進行庫的資料段所

  需的任何重定位。

  在x86平台上,加載時的重定位包括:

  R_386_GLOB_DAT:初始化一個GOT項,該項是在另一個庫中定義的符号的位址。

  R_386_32:對在另一個庫中定義的符号的非GOT引用,通常是靜态資料區中的指針。

  R_386_RELATIVE:對可重定位資料的引用,典型的是指向字串(或其它局部定義靜态數

  據)的指針。

  R_386_JMP_SLOT:用來初始化PLT的GOT項,稍後描述。

  如果一個庫具有.init區段,加載器會調用它來進行庫特定的初始化工作,諸如C++的

  靜态構造函數。庫中的.fini區段會在程式退出的時候被執行。它不會對主程式進行初始化,

  因為主程式的初始化是有自己的啟動代碼完成的。當這個過程完成後,所有的庫就都被完全

  加載并可以被執行了,此時加載器調用程式的入口點開始執行程式。

  靜态的初始化

  如果一個程式存在對定義在一個庫中的全局變量的引用,由于程式的資料位址必須在

  連結時被綁定,是以連結器不得不在程式中建立一個該變量的副本,如圖4所示。這種方法

  對于共享庫中的代碼沒有問題,因為代碼可以通過GOT中的指針(連結器會調整它)來引用

  變量。但如果庫初始化這個變量就會産生問題。為了解決問題,連結器在程式的重定位表

  (僅僅包含類型為R_386_JMP_SLOT、R_386_GLOB_DAT、R_386_32和R_386_RELATIVE的表項)

  中放入一個類型為R_386_COPY類型的表項,指向該變量在程式中的副本被定義的位置,并

  告訴動态連結器從共享庫中将該變量被初始化的數值複制過來。

  —————————————————————————

  圖10-4:全局資料初始化

  主程式中:

  extern int token;

  共享庫中的例程:

  int token = 42;

  雖然這個特性對于特定類型的代碼是關鍵的,但在實際中很少發生。這是一種橡皮膏

  (譯者注:權宜之計的意思),因為它隻能用于單字的資料。好在初始化程式通常的對象是

  指向過程或其它資料的指針,是以這個橡皮膏夠用了。

  庫的版本

  動态連結庫通常都會結合主版本和次版本号來命名,例如libc.so.1.1。但是應用程式

  隻會和主版本号綁定,例如libc.so.1,次版本号是用于更新的相容性的。

  為了保持加載程式合理的速度,系統會設法維護一個緩沖檔案,儲存最近用過的每一

  個庫的全路徑檔案名,該檔案會在一個新庫被安裝時有一個配置管理程式來更新。

  為了支援這個設計,每一個動态連結的庫都有一個在庫建立時賦予的稱為SONAME的“

  真名”。例如,被稱為libc.so.1.1的庫的SONAME為libc.so.1(預設的SONAME是庫的名

  稱)。當連結器建立一個使用共享庫的程式時,它會列出程式所使用庫的SONAME而不是庫

  的真實名稱。緩沖檔案建立程式掃描包含共享庫的所有目錄,查找所有的共享庫,提取每一

  個的SONAME,對于具有相同SONAME的多個庫,除版本最高的外其餘的忽略。然後它将SONAM

  E和全路徑名稱寫入緩沖檔案,這樣在運作時動态連結器可以很快的找到每一個庫的目前版

  本。