fw:C靜態庫連接的順序問題

C靜態庫連接的順序問題

C語言的靜態連接,簡單的說就是將編譯得到的目標文件.o(.obj),打包在一起,並修改目標文件中函數調用地址偏移量的過程。當在大一點的項目中,可能會遇到連接時,由於靜態庫在鏈接器命令行中出現順序的問題,造成undefined reference錯誤。本文深入探討一下這個問題,以及如何解決。

作者:P_Chou來源:segmentfault|2016-09-19 10:54

C語言的靜態連接,簡單的說就是將編譯得到的目標文件.o(.obj),打包在一起,並修改目標文件中函數調用地址偏移量的過程。當在大一點的項目中,可能會遇到連接時,由於靜態庫在鏈接器命令行中出現順序的問題,造成undefined reference錯誤。本文深入探討一下這個問題,以及如何解決。

問題

如下圖。假設有這麼一個場景,在我們的構建系統中,構建了一個兩個靜態庫文件liba.a和libb.a,其中liba.a包含兩個目標文件a1.o和a2.o,而libb.a包含一個目標文件b1.o。希望將main.o靜態連接liba.a和libb.a。

注意到黃色的箭頭表示調用關係:b1.o需要調用a1.o中的某函數,而main.o調用了a2.o和b1.o中的函數。你可以把.o文件理解爲對應的.c文件。

那麼如下的兩個命令哪個會成功執行呢?注意到這兩個命令唯一的區別是對liba.a和libb.a的書寫順序

 
  1. # gcc -o a.out main.o liba.a libb.a 
  2.  
  3. ...undefined reference... 
  4. error: ld returned 1 exit status  
 
  1. # gcc -o a.out main.o libb.a liba.a 

靜態連接的算法

要理解上面這個問題,需要理解鏈接器在處理靜態連接時候的算法。此處的闡述參考《深入理解計算機系統》中的“鏈接”章節。

首先,需要明確的是,鏈接器在考察庫文件(.a)的時候,不是把庫文件看做一個整體,而是將打包在其中的目標文件(.o)作爲考察單元。在整個連接過程中,如果某個目標文件中的符號被用到了,那麼這個目標文件會單獨從庫文件中提取出來,而不會把整個庫文件連接進來。

然後,鏈接器在工作過程中,維護3個集合:需要參與連接的目標文件集合E、一個未解析符號集合U、一個在E中所有目標文件定義過的所有符號集合D。

以上面第一條命令gcc -o a.out main.o liba.a libb.a爲例,我們來一步步看看鏈接器的工作過程:

當輸入main.o後,由於main調用了a2.o和b1.o中的函數,而此時並沒有在D中找到該符號,於是將引用的兩個函數保存在U中,此處假設兩個函數分別爲a2_func和b1_func:

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |    a2_func    |               | 
  4. +---------------+---------------+---------------+ 
  5. |               |    b1_func    |               | 
  6. +---------------+---------------+---------------+ 

接下來,輸入liba.a,鏈接器發現,a2_func存在於liba.a的a2.o中,於是將a2.o加入到E,並在D中加入a2.o中所有定義的符號,其中包括a2_func,最後移除U中的a2_func,因爲這個符號已經在a2.o中找到了的。然而,U中還有b1_func,所以連接還沒有完成。

 
  1.        E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |    b1_func    | a2_func_other | 
  6. +---------------+---------------+---------------+ 

接着,輸入libb.a,同理,鏈接器發現b1_func定義在b1.o中,所以在E中加入b1.o,移除U中的b1_func,在D中加入b1.o裏面所有定義的符號

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |               | a2_func_other | 
  6. +---------------+---------------+---------------+ 
  7. |     b1.o      |               |    b1_func    | 
  8. +---------------+---------------+---------------+ 

然而,由於b1.o調用到a1.o中的函數,我們假設是a1_func,但在D中並沒有找到這個函數,所以a1_func還需要加入到U中

 
  1.         E               U               D          
  2. +---------------+---------------+---------------+ 
  3. |     main.o    |               |    a2_func    | 
  4. +---------------+---------------+---------------+ 
  5. |     a2.o      |               | a2_func_other | 
  6. +---------------+---------------+---------------+ 
  7. |     b1.o      |    a1_func    |    b1_func    | 
  8. +---------------+---------------+---------------+ 

但是,輸入結束了!鏈接器發現U中還有未解析的符號,所以報錯了!

可以看到由於鏈接器的算法實現,導致a1.o並沒有被鏈接器考察,所以產生了未解析符號。仔細分析,可以知道,只要將liba.a和libb.a換一下順序,就可以鏈接成功!

解決辦法

一般來說有兩種辦法,一種是仔細分析依賴關係,並按照正確的順序書寫庫文件的引用。原則是被依賴的儘量寫在右邊。但是在有些大型項目中,依賴關係可能並不容易梳理清楚。此時可以在命令行參數中重複對庫文件的引用:

 
  1. # gcc -o a.out main.o liba.la libb.la liba.a 

在上面的命令中,liba.a重複書寫了兩次。

如果你使用automake,可以用xxx_LIBADD和xxx_LDADD來控制目標文件的引用關係:

  • xxx_LIBADD:對於目標文件爲庫文件或可執行文件,需使用這個選項。表示在打包目標庫文件的時候,就將依賴的文件一併打包進來。
  • xxx_LDADD:對於可執行文件可用這個選項,來控制鏈接器的參數,如果你能分析清楚依賴關係,可以在這個選項中按照正確的順序書寫,從而成功連接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章