[讀書筆記]Linkers and Loaders初探

potato123發表於2010-03-19

1.編譯
    編譯分成3個階段:
    *預編譯階段(g++ -E選項):這個階段主要完成預編譯指令(#)的處理,包括處理include、define、ifdef等等,譬如如果希望看到巨集展開後的結果,可以使用該命令進行預編譯處理。
如下將test.cpp預編譯後的結果存入test.i

g++ -E -o test.i test.cpp

    像include的檔案找不到等錯誤會在這個階段發生。
    (有用的編譯選項:預設情況下,include的標頭檔案會在當前目錄和系統標頭檔案目錄中搜尋,這個階段可以通過 -I選項設定需要include的標頭檔案的目錄)
    *編譯階段(g++ -S選項):編譯程式碼並生成彙編程式碼。譬如如下將預編譯後的test.i生成彙編程式碼test.s

g++ -S -o test.s test.i

    如果有語法錯誤,或者引用了沒有宣告的變數或函式的錯誤,會在這個階段提醒。
    *彙編階段:將彙編程式碼彙編成目標檔案(.o)。譬如如下將彙編檔案test.s彙編成test.o

g++ -c -o test.o test.s

    2.連結
    1)目標檔案分析
    編譯階段完成,生成了test2.o目標檔案。我們來看看到底生成了一些什麼。
    *test.cpp原始碼:

int add(int first, int second);
int myAdd(int first, int second, int third)
{
    int result = add(first, second);

    result = add(result, third);
    
    return result;
}

    *我們會發現,在如上的程式中,我們僅僅宣告瞭int add(int first, int second)這個函式,實際並沒有去實現,用objdump反彙編看一下結果(或者看如上的test2.s)

objdump -d test.o
test.o:     file format elf32-i386
Disassembly of section .text:
00000000 <main>:
   ...
  20:   e8 fc ff ff ff          call   21 <main+0x21>
  ...

      如上,實際發生了對int add(int first, int second)的呼叫,但在如上中並沒有指定 int add(int first, int second)的地址(實際也不可能指出),可以猜測,test.o必定有相關的引用說明,實際就是未解析的符號資訊,通過objdump看一下結果:objdump -r test.o

test.o:     file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000021 R_386_PC32        _Z3addii
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET   TYPE              VALUE
00000011 R_386_32          __gxx_personality_v0
00000024 R_386_32          .text

    -r選項指出了當前彙編結果中未解析的符號及在本彙編檔案相應的佔位地址,如上,是00000021
    2)連結過程
    在上面的說明中,我們知道,目標檔案並沒有包含所有引用到的符號,那麼必然有一個過程處理未解析的符號,並最終合併成一個執行檔案的過程,也就是連結過程,如下將test.cpp和test1.cpp連結並生成可執行程式test

g++ test.o test1.o -o test

    看看連結後對符號做了什麼處理?

 08048434 <main>:
....
 8048454:       e8 13 00 00 00          call   804846c <_Z3addii>
....

0804846c <_Z3addii>:
....

    可以看到,連結過程將實際的地址代入了呼叫地址中去了。
    3.靜態庫(.a)
    一般我們會將一些常用的功能打包成庫(譬如標準庫)。庫實際是多個目標檔案的聚合。庫分成靜態庫(.a)和動態庫(.so),對於靜態庫,在連結的過程會將相應的程式碼打包(patch)到相應的可執行程式當中,而動態庫,則不會打包到可執行程式中,而是在載入器載入程式的時候才將其連結進來,即載入器的連結。
    我們先來看靜態庫是怎麼回事
    *測試程式碼
     test2.cpp

int add(int first, int second)
{
    int result = first + second;

    return result;
}

     test3.cpp

int add(int first, int second);

int add2(int first, int second)
{
    int result = add(first, second);

    result++;

    return 0;
}

 

   *先編譯成目標檔案(.o),然後使用下面的命令打包
    ar -r libtest.a test2.o test3.o
    需要注意,一般打的包名字以lib開頭,我們看看到底生成了一些什麼東西:objdump -d libtest.a

In archive libtest.a:
test2.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_Z3addii>:
   ...
test3.o:     file format elf32-i386
Disassembly of section .text:
00000000 <_Z4add2ii>:
   ...
  13:   e8 fc ff ff ff          call   14 <_Z4add2ii+0x14>
   ...

    可以看出,雖然test3.cpp中使用的int add(int first, int second),在test2.cpp中定義,但ar並沒有對目標檔案做什麼處理,呼叫地址處仍然只是保留一個佔位符而已
   *使用靜態庫來連結成可執行程式
   主程式非常簡單,呼叫一下add2函式,執行下面命令產生執行程式
   g++ -o test4 test4.cpp -L. -ltest
   -L.表示需要在當前目錄下搜尋庫,-ltest表示連線需要依賴到libtest.a連線庫
   我們看看連結後的結果:objdump -d test4

08048434 <main>:
 ...
 8048454:       e8 13 00 00 00          call   804846c <_Z4add2ii>  (此處已經解析出了add2的地址)
 ...
0804846c <_Z4add2ii>:
 ...
 804847f:       e8 10 00 00 00          call   8048494 <_Z3addii>   (此處的地址在連結的時候被替換掉了)
 ...
08048494 <_Z3addii>:
 ...

   可以看出,在連結的時候,庫中的test2.o呼叫test3.o的add函式部分的地址被替換掉了
   4.再看連結符號解析
   結合靜態庫,我們再回頭看看連結的符號解析處理:
   *聯結器從左到右掃描所有指定的目標檔案和庫檔案,在掃描過程當中維持如下三個set:目標檔案Set (O Set)、未解析符號Set (U Set)、已解析符號Set (D Set),剛開始的時候,這3個Set都是空的
   *聯結器掃描到一個目標檔案,則將其加入O Set,同時更新U Set和D Set
   *聯結器掃描到一個庫檔案,則掃描庫檔案中的各目標檔案,如果在其中能夠找到U Set中的內容,則將該目標檔案加入到O Set,同時更新U Set和D Set中相應符號,如果該目標檔案有沒有解析的符號,
也相應將其加入到U Set中
   *在掃描完之後,如果U Set不為空,則連結器會報錯,如果U Set為空,則聯結器將O Set中的內容進行合併處理 
   從如上過程當中,可以注意到
   *各庫的順序是有依賴關係的,如果庫a依賴庫b,則連結時需要將a依賴放在b的左邊
   *不允許在目標檔案中定義同一個符號
   *如果在不同的庫中定義了同一個符號,則第一次被依賴到地先被掃描進來
   5.載入器
   連結完成,我們開始執行程式,這時候載入器登場了。引用《Linkers and Loaders》的話說:載入是將一個程式放到主存裡使其能執行的過程。基本過程大概包括:

<Linkers and Loaders>第8章 寫道
*從目標檔案中讀取足夠的頭部資訊,找出需要多少地址空間。
*分配地址空間,如果目的碼的格式具有獨立的段,那麼就將地址空間按獨立的段劃分。
*將程式讀入地址空間的段中。
*將程式末尾的bss段空間填充為0,如果虛擬記憶體系統不自動這麼做得話。
*如果體系結構需要的話,建立一個堆疊段(stack segment)。
*設定諸如程式引數和環境變數的其他執行時資訊。
*開始執行程式。

   需要注意的是,這個過程不涉及到動態連線庫,動態連線庫的載入涉及到符號重定位的問題,會更加複雜