obj文件,動態鏈接文件和ELF文件

1.obj文件
      程序員編寫程序,其實就是編寫出一個2進制(binary)文件。假如我們聲明一個變量char c,也就是聲明需要一個8bit的空間,那麼就需要向系統聲明豫留8bit的空間,怎麼做到這一點呢?就是編譯一個特殊的2進制文件--obj文件,用gcc編譯的C語言得到的執行文件,裏面不僅包含CPU指令,還有很多別的信息在裏面,它有很多格式COFF、ELF……等等,在最後一道編譯過程中,鏈接器(linker)ld會加載一堆信息進入可執行文件。例如,當有多個編譯後等待鏈接的.o這種可重定位(relocatable)文件,既然這些文件裏面參數或者函數名的相對位置只是本身所在.o文件的相對位置,就有一些信息要告訴鏈接編輯器(link editor)怎麼修改section的內容,來做relocate,也就是做地址的重新參照以便合成一個新的可執行文件。
      一個obj文件有兩個重要時期,一個是正在鏈接(link)的時候,也就是處在
硬盤(disk)裏的時候;一個是正在執行的時候,當然這時它位於內存裏。我們平時說的ld linker其實叫link editor,最後編譯的步驟ld把該有的信息寫進可執行文件。如果是static link就會去找libxxx.a的函數庫文件,把想要的程序代碼片段拷貝一份進可執行文件,並且做成relocation後,把跳來跳去的參照寫進可執行文件,這個文件就可以執行。
2、動態鏈接文件
      相對於靜態鏈接(static link)拷貝原有的程序代碼進可執行文件,動態鏈接不那樣做,link editor把一些信息寫進可執行文件而已。例如,需要的程序庫名、函數名等,最後執行的時候,必須呼叫dynamic linker來做program
intepreter,dynamic linker會根據需要的函數庫名稱,把想要的函數名字創造一個可執行的image放到內存,所以執行有動態鏈接的執行文件,最後通常都是由OS的exec系列的system call與dynamic linker如ld.so聯合完成。
dynamic linker通常會做如下工作:
(1)把可執行文件的內容加載到process image
(2)把shared obj需要的東西加載到process image
(3)完成relocation
       本來這些obj文件裏面的虛擬地址應該和文件的地址有相對應的偏移(offset),而文件首地址通常是0x08040800,這是絕對虛擬地址,但它只適合可執行文件,例如Linux extuable file通常是:
          file offset         virtual  address
          -----------         ----------------
          0x0                 0x08048000
          0x100             0x08048100
       shared obj函數庫裏的程序代碼必須爲位置無關代碼Position
Independent Code (PIC),也就是說它的地址可能會隨不同process而有不同,例如,一個程序只用了libc.so、ld-linux.so,通常這時候lib.so是從0x40017000開始的,但如果另一個程序多用一個libm.so,那麼libc.so從0x40034000開始兩個的printf參照(reference),就會有不同的地址,所以這種動態函數庫的內部資料就要說明這些code是PIC。
3、ELF文件
(1)簡介
      現在最常用的是一種叫ELF格式(executable and linkable format)的執行文件,ELF定義了一些變量與信息使得動態鏈接更有彈性,一個ELF的2進制文件按照spec 1.1版的說法有6種,下列是較常見的:

relocatable:它就是編譯時產生的.o文件,包含了代碼和數據(這些數據是和其 他 重定位文件和共享的object文件一起連接時使用的)

executable:它就是最後的可執行文件,包含了代碼和數據shared obj:它就是在/lib /usr/lib下那些可動態鏈接的函數庫文件,包含了代碼和數據(這些數據是在連接時候被連接器ld和運行時動態連接器使用的)

core:Core Dump時產生的文件,包含了一堆garbage數據

      注意:這些ELF文件已經是廣義的2進制文件,不單指可執行文件。
    
(2)ELF組成
      一個ELF obj文件隨它存在的時期有不一樣的需求和組成名字,在要鏈接linking時期位於硬盤,包含了:
ELF header
program header table (可以不要)
section 0
section 1
section 2
section 3
section ...
section n
section header table 
      ELF header放了ELF定義的一些ELF格式識別字串(俗稱magic number),還有obj文件(shared obj,relocatable或者executable)這些一般(general)的信息;program header table是描述了段(segment)信息的結構數組和一些爲程序運行準備的信息。segement和section不大一樣,是屬於程序執行時期的元素,所以在程序執行時期是必要的,在鏈接時期是不必要的,所以如果程序不做鏈接動作,只要有program header table就可以;section header table就是一個索引表,來記錄各個section的索引,sections就是把需要的資料根據屬性用途分門別類後的小集合,有.bss .data .init .debug .dynamic .fini .text………,其中比較重要的有:
.text
       裏面保存真的CPU指令
.bss
      保存沒有initialize的data
      主要是聲明的global與static變量
.data
      保存initialize的data

      寫程序用到的函數名,變量名分佈在多個source code目錄裏時,需要一個
參照(reference)的信息做連接這些名字,symbol是着被給linker來做連接用的,因爲obj文件分散存在,要把這些obj文件的代碼集合起來,就要靠symbol
來辨別,string table存有很多行字串,每行字串用NULL來分開,每行字串就是symbol和section的名字。symbol table是一張表,存有將來要定址或重新定址所要的symbol定義和參照信息。shared lib的obj文件還有.dynsym這個section,裏面存有dynamic symbol table,動態鏈接的時候使用。另外,如果將來的程序要用debug工具調試,編譯時要加-g這個選項,它會根據sumbol
和string table放進debug多需要的信息給obj文件,這樣的信息現在大都用一種叫stab的格式存放,這同時也會讓執行文件大小增加到將近3倍。
      在ELF不同的文件型態裏,ELF定義的信息該有的都有,header section……只是裏面的值或有不同而已。
      Unix/Linux通常從一個_start函數開始而不是從main開始,_start後來會調用main,所以如果要精簡程序,就不要用gcc編譯,直接彙編用_start就可以了(^_^)。另外像section header table如果不需要做鏈接也可以不要,還有可執行文件的symbol table等,其實這些可以全部不要,不過要用匯編並同GAS來生成可執行文件。其實還有很多東西,這就是爲什麼即使根本沒有調用任何函數,做成的動態文件,用ldd看一定有ld-linux.so libc.so了。
      而一個存在內存中的process image,如下所示:
ELF header
program header table
segment 0
segment 1
segment 2
segment ...
segment n
section header table (可以不要)
      Segment有Text,Data等,根據OS定義不同,Text根據存在硬盤文件裏的.txt .fini等section來的,Data段根據.data .bss等section來的,一個segment通常包含了 一個或一個以上的section,這些section在程序員角度來看更顯的重要。
      在支持ELF的系統上,一個程序是由可執行文件或者加上一些shared obj文件組成。爲了執行這樣的程序,系統使用那些文件創建進程的內存映像。爲了使一個ELF文件裝載到內存,必須有一個program header table(該program header table 是一個描述段信息的結構數組和一些爲程序運行準備的信息)。這裏有幾個在ELF文檔中定義的比較特別的sections。以下這些是對程序特別有用的:
.fini
     保存進程終止代碼指令
     因此,當一個程序正常退出時,系統安排執行這個section中的代碼
.init
     保存可執行指令,它構成了進程的初始化代碼
     因此,當一個程序開始運行時,在main函數被調用前(C語言稱爲main),
系統安排執行這個section中的代碼

      .init和.fini sections的存在有着特別的目的。假如一個函數放到.init section,在main函數執行前系統就會執行它。同理,假如一個函數放到.fini section,在main函數返回後該函數就會執行。該特性被C++編譯器使用,完成全局的構造和析構函數功能。
      當ELF可執行文件被執行,系統將在把控制權交給可執行文件前裝載所以相關
的共享object文件。構造正確的.init和.fini sections,構造函數和析構函數將以正確的次序被調用。
      Unix/Linux的虛擬內存使用有這樣的範圍:

user area

0x0 ~ 0x0bffffff  ->; 3GB

kernel area

0x0c000000 ~ 0xffffffff  ->; 1GB

以下面程序代碼爲例:
int global;

static int func1 (void)
{
                static int b;
                int *c;
                int d;
                func2();
                return 1;
}
int func2 (void)
{
                int c;
                static int d;
                return 2;
}
int main(void)
{
                int a;
                static int b;
                int init = 3;
                func1();
                return 3;
}

那麼從一個Linux執行文件在內存中看起來是這個樣子:

i386 Linux的執行image

            Virtual   Address  Allocation

              |----------------------------------|0x0   
              | |-----------------------------|  |
              | |                                     |  | 
              | |  Thread  stack              |  |  
              | |------------------------------|  |       
              |                                           |
              | |------------------------------|  |0x08048000 Text                           
              | |  executable                  |  |                    Data            
              | |                                      |  |                     ……
              | |                                      |  | 
              | |                                      |  | 
              | |                                      |  | 
              | |------------------------------|  |
              |                                           |
              | |------------------------------|  |0x40000000 ld-linux.so
              | |                                     |  |                     libm.so
              | |  shared    LIB               |  |                     libc.so
              | |                                     |  |           
              | |  Stack                           |  |
              | |                                     |  |
        3GB| |----------------------------- |  |
              |                                           |
              | |------------------------------|  |0xc0000000
              | |                                      |  |
              | | Kernel Code and Data  |  |
              | |                                      |  |
              | |-------------------------------|  |
        4GB|---------------------------------   |0xffffffff

其中0x08048000 ~ 0x40000000 ~ 0xc0000000是這樣存在的。

從C角度看的image:

       0x08048000
     |--------------------------------------------------------|
     |   |--------------------------------------------------|  |
     |   |   main()                                                 |  |                              
     |   |           xxxx                    Text                 |  |
     |   |   func1                           (instrction)   |  |   
     |   |           xxxx                                            |  |
     |   |   func2                                                  |  |   
     |   |           xxxx                                           |  |
     |   |-------------------------------------------------|  |
     |                                                                     |
     |   |-------------------------------------------------|  |
     |   |   int global                       Data             |  |
     |   |   static int b(main)  static int b(func1)  |  |
     |   |   static int c(func2)                               |  |
     |   |-------------------------------------------------|  |
     |                                                                     |
     |   |-------------------------------------------------|  |
     |   |   malloc(int)                      Heap            |  |
     |   |-------------------------------------------------|  |
     |                      |                                              |    
     |                      |                                              |
     |                     /|/                                             |                             
     |--------------------------------------------------------|
     |    0x40000000                                              |
     |                                                                      |
     |                                                                      |
     |--------------------------------------------------------|
     |                     /|/                                             |
     |                      |                                               |
     |                      |                                               |
     |    |-------------------------------------------------|  |
     |    |  func2 int c                     Stack  2          |  |
     |    |-------------------------------------------------|  |
     |                                                                      |
     |    |-------------------------------------------------|  |
     |    |  func1 int b                     Stack  1         |  |
     |    |-------------------------------------------------|  |
     |                                                                      |
     |    |-------------------------------------------------|  |
     |    |  main()  argv[0]  argv[1]  …                  |  |
     |    |-------------------------------------------------|  |
     |--------------------------------------------------------|
       0xbfffffff

       所以可以清楚的知道不同變量(global,static or auto)的生命週期(storage class),和不同變量的有效範圍(scope)。
       Kernel code和data當然存在內存中,所以實際上都還要經過page table
轉成實際地址。在0x0~ 0xbfffffff中的page table,每個process有不同page
table,但在0xc0000000以下的page table,則都一樣。


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章