編譯中的各種undefined reference解決方式和坑


 

最近在Linux下編程發現一個詭異的現象,就是在鏈接一個靜態/動態庫的時候總是報錯,類似下面這樣的錯誤:

(.text+0x13): undefined reference to `func'

    關於undefined reference這樣的問題,大家其實經常會遇到,在此,詳細地示例給出常見錯誤的各種原因以及解決方法。

1.  鏈接時缺失了相關目標文件(.o

    測試代碼如下:

test.c 中:

int test(){return 0;}

Main.c中:

Int main(){return test();}

    然后編譯。

gcc -c test.c

gcc –c main.c

    得到兩個 .o 文件,一個是 main.o,一個是 test.o ,然后我們鏈接 .o 得到可執行程序:

gcc -o main main.o

  這時,你會發現,報錯了:

main.o: In function `main':

main.c:(.text+0x7): undefined reference to `test'

collect2: ld returned 1 exit status

    這就是最典型的undefined reference錯誤,因為在鏈接時發現找不到某個函數的實現文件,本例中test.o文件中包含了test()函數的實現,所以如果按下面這種方式鏈接就沒事了。

gcc -o main main.o test.o

   【擴展】:其實上面為了讓大家更加清楚底層原因,把編譯鏈接分開了,下面這樣編譯也會報undefined reference錯,其實底層原因與上面是一樣的。

gcc -o main main.c //缺少test()的實現文件

需要改成如下形式才能成功,將test()函數的實現文件一起編譯。

gcc -o main main.c test.c //ok,沒問題了

2.    鏈接時缺少相關的庫文件(.a/.so

    在此,只舉個靜態庫的例子,假設源碼如下。

test.c 中:

int test(){return 0;}

    先把test.c編譯成靜態庫(.a)文件

gcc -c test.c

ar -rc test.a test.o

    至此,我們得到了test.a文件。我們開始編譯main.c

gcc -c main.c

    這時,則生成了main.o文件,然后我們再通過如下命令進行鏈接希望得到可執行程序。

gcc -o main main.o

    你會發現,編譯器報錯了:

/tmp/ccCPA13l.o: In function `main':

main.c:(.text+0x7): undefined reference to `test'

collect2: ld returned 1 exit status

    其根本原因也是找不到test()函數的實現文件,由於該test()函數的實現在test.a這個靜態庫中的,故在鏈接的時候需要在其后加入test.a這個庫,鏈接命令修改為如下形式即可。

gcc -o main main.o ./test.a //注:./ 是給出了test.a的路徑

     【擴展】:同樣,為了把問題說清楚,上面我們把代碼的編譯鏈接分開了,如果希望一次性生成可執行程序,則可以對main.ctest.a執行如下命令。

gcc -o main main.c ./test.a //同樣,如果不加test.a也會報錯

3.    鏈接的庫文件中又使用了另一個庫文件

    這種問題比較隱蔽,也是我最近遇到的與網上大家討論的不同的問題,舉例說明如下,首先,還是看看測試代碼。

Fun.c中:

Int fun(){return 0;}

Test.c

Int test(){return fun();}

Main.c中:

Int main(){return test();}

    從上可以看出,main.c調用了test.c的函數,test.c中又調用了fun.c的函數。    

 首先,我們先對fun.ctest.cmain.c進行編譯,生成 .o文件。

gcc -c func.c

gcc -c test.c

gcc -c main.c

    然后,將test.cfunc.c各自打包成為靜態庫文件。

ar –rc func.a func.o

ar –rc test.a test.o

    這時,我們准備將main.o鏈接為可執行程序,由於我們的main.c中包含了對test()的調用,因此,應該在鏈接時將test.a作為我們的庫文件,鏈接命令如下。

gcc -o main main.o test.a

    這時,編譯器仍然會報錯,如下:

test.a(test.o): In function `test':

test.c:(.text+0x13): undefined reference to `func'

collect2: ld returned 1 exit status

    就是說,鏈接的時候,發現我們的test.a調用了func()函數,找不到對應的實現。由此我們發現,原來我們還需要將test.a所引用到的庫文件也加進來才能成功鏈接,因此命令如下。

gcc -o main main.o test.a func.a

    ok,這樣就可以成功得到最終的程序了。同樣,如果我們的庫或者程序中引用了第三方庫(如pthread.a)則同樣在鏈接的時候需要給出第三方庫的路徑和庫文件,否則就會得到undefined reference的錯誤。

4 多個庫文件鏈接順序問題

    這種問題也非常的隱蔽,不仔細研究你可能會感到非常地莫名其妙。我們依然回到第3小節所討論的問題中,在最后,如果我們把鏈接的庫的順序換一下,看看會發生什么結果?

gcc -o main main.o func.a test.a

    我們會得到如下報錯.

test.a(test.o): In function `test':

test.c:(.text+0x13): undefined reference to `func'

collect2: ld returned 1 exit status

    因此,我們需要注意,在鏈接命令中給出所依賴的庫時,需要注意庫之間的依賴順序,依賴其他庫的庫一定要放到被依賴庫的前面,這樣才能真正避免undefined reference的錯誤,完成編譯鏈接。

5. c++代碼中鏈接c語言的庫

    如果你的庫文件由c代碼生成的,則在c++代碼中鏈接庫中的函數時,也會碰到undefined reference的問題。下面舉例說明。

    首先,編寫c語言版庫文件:

Test.c

Int test(){return 0;}

    編譯,打包為靜態庫:test.a

gcc -c test.c

ar -rc test.a test.o

    至此,我們得到了test.a文件。下面我們開始編寫c++文件main.cpp

    Main.cc中:

Int main(){return test();}

    然后編譯main.cpp生成可執行程序:

g++ -o main main.cpp test.a

    會發現報錯:

/tmp/ccJjiCoS.o: In function `main':

main.cpp:(.text+0x7): undefined reference to `test()'

collect2: ld returned 1 exit status

    原因就是main.cppc++代碼,調用了c語言庫的函數,因此鏈接的時候找不到,解決方法:即在main.cpp中,把與c語言庫test.a相關的頭文件包含添加一個extern "C"的聲明即可。例如,修改后的main.cpp如下:

extern “C”

{

#include “test.h”

}    

Int main(){

return test();

}

g++ -o main main.cpp test.a

    再編譯會發現,問題已經成功解決。

或者直接在test.h中加入:

ifdef __cplusplus

extern "C"

{

#endif

int cadd(int x, int y);

#ifdef __cplusplus

}

#endif

一樣也可以解決

6. 編譯參數加入-Wl--as-needed的好處和注意事項

--as-needed標志可使鏈接程序避免以二進制形式鏈接額外的庫。這不僅縮短了啟動時間(因為加載器不必每一步都加載所有庫)更重要的是,使用--as-needed避免將依賴項添加到二進制文件中,這是其直接或間接依賴項之一的先決條件。

6.1 最終鏈接失敗,未定義符號

這是使用時發生的最常見錯誤--as-needed。它發生在可執行文件的最后鏈接階段(庫不會造成問題,因為允許它們具有未定義的符號)。可執行鏈接階段之所以消失,是因為在饋送到命令行的庫中存在未定義的符號。但是,可執行文件本身未使用該庫,因此該庫將被刪除--as-needed。這通常意味着一個庫沒有鏈接到另一個庫,而是在使用它,然后依靠最終的可執行文件將它們鏈接在一起。對於使用該庫的開發人員來說,這種行為也是一種額外的負擔,因為他們必須檢查需求。

解決這類問題的方法通常很簡單:只需找到哪個庫提供了符號,哪個庫就需要它們(來自鏈接器的錯誤消息應包含后者的名稱)。然后確保從源文件鏈接庫時,它也鏈接到第一個庫。

6.2 執行失敗,未定義符號

有時,未定義的符號錯誤不會在鏈接時發生,而是在使用--as-need生成的應用程序執行時發生。但是,原因與鏈接中未定義符號的原因相同:直接鏈接的庫未鏈接其依賴項之一。它還具有相同的解決方案:查找哪個庫包含未定義的符號,並確保將其鏈接到提供它們的庫。

6.3 鏈接順序的重要性

盡管所有庫都出現在鏈接行中,但它們只是被忽略而不是完全鏈接。這導致了與上述相同的問題;在最終鏈接或執行期間缺少符號。這是因為強制實施了GNU鏈接程序的行為--as-needed導致的

 

基本上,鏈接器所做的是僅在緊隨其后的文件中查找給定文件(目標文件,靜態歸檔或庫)中缺少的符號。當使用普通鏈接時,如果不使用--as-needed,則這不是問題,盡管鏈接階段可能存在一些內部缺陷,但是文件鏈接在一起卻沒有考慮順序。但是使用該標志時,不用於解析符號的庫將被丟棄,因此不會鏈接。

 

6.4錯誤和正確的鏈接順序的編碼示例

(這種情況下,libm在對象文件之前被考慮,並且獨立於兩者的內容而被丟棄,即不會被編譯到pro

$ gcc -Wl--as-needed -lm someunit1.o someunit2.o -o pro

 

(這是僅在需要時才能鏈接libm的正確鏈接順序。)

$ gcc -Wl--as-needed someunit1.o someunit2.o -lm -o程序

通常這種情況下,解決方法是簡單地修復鏈接順序,以使提供給鏈接器的庫都位於目標文件和靜態檔案之后。

6.5 實例用法

1.  linux下查看一個可執行文件或動態庫依賴哪些動態庫的辦法

readelf -d PyGalaxy.so

ldd   PyGalaxy.so

load 動態庫過程基本的說就是符號重定位,然后合並到全局符號表。

  1. 在編譯動態庫時:關鍵的看as-needed,意思是說:只給用到的動態庫設置DT_NEEDED。比如:

g++ -shared  PyGalaxy.o -lGalaxyParser -lxxx  -lrt  -o PyGalaxy.so

像這樣鏈接一個PyGalaxy.so的時候,假設PyGalaxy.so里面用到了libGalaxyParser.so但是沒 有用到libxxx.so。查看依賴關系如下:(不加不管什么指定了就加進來)

ocaml@ocaml:~$ readelf -d PyGalaxy.so

 0x0000000000000001 (NEEDED)             Shared library: [libGalaxyParser.so]

 0x0000000000000001 (NEEDED)             Shared library: [libxxx.so]

 

當開啟–as-needed的時候,像

g++ -shared  -Wl,--as-needed PyGalaxy.o -lGalaxyParser -lxxx  -lrt  -o PyGalaxy.so

這樣鏈接PyGalaxy.so的時候,查看依賴關系如下:

ocaml@ocaml:~$ readelf -d PyGalaxy.so

 0x0000000000000001 (NEEDED)             Shared library: [libGalaxyParser.so]

as-needed就是忽略鏈接時沒有用到的動態庫,只將用到的動態庫set NEEDED

 

3 開啟as-needed的一些常見的問題:

一)鏈接主程序模塊(可執行程序bin)或者是靜態庫的時的‘undefined reference to: xxx’

:

g++ -Wl,--as-needed -lGalaxyRT -lc -lm -ldl -lpthread   -L/home/ocaml/lib/  -lrt -o mutex mutex.o

假設可執行程序mutex依賴libGalaxyRT.so中的東西。因為gcc對庫的順序要求和–as-needed(因為libGalaxyRT.somutex.o的左邊,所以gcc認為沒有用到它,–as-needed將其忽略),ld忽略libGalaxyRT.so,定位mutex.o的符號的時候當然會找不到符號的定義,所以‘undefined reference to’這個錯誤是正常地!

 

正確的鏈接方式是:

g++ -Wl,--as-needed mutex.o -lGalaxyRT -lc -lm -ldl -lpthread   -L/home/ocaml/lib/  -lrt -o mutex

二) 編譯動態庫(shared library)的時候會導致一個比較隱晦的錯誤

編譯出來的動態庫的時候沒有問題,但是加載(link)的時候有“undefined symbol: xxx”這樣的錯誤。

假如像這也鏈接PyGalaxy.so

g++ -shared  -Wl,--as-needed -lGalaxyParser -lc -lm -ldl -lpthread   -L/home/ocaml/lib/  -lrt  -o PyGalaxy.so PyGalaxy.o

load PyGalaxy.so的時候會有上面的運行時錯誤!

簡單分析原因:因為libGalaxyParser.soPyGalaxy.o的左邊,所以gcc認為沒有用到–as-needed將其忽略。但是前面說的動態庫符號解析的特點導致ld認為某些符號是加載(link)的時候才去地址重定位的。但是 libGalaxyParser.so已經被忽略了。所以就算你寫上了依賴的庫,load的時候也會找不到符號,因為編譯庫時已經被忽略啦,一般鏈接PyGalaxy.so庫時會報未定義錯誤。但是為什么沒有-Wl–as-needed的時候是正確的呢?沒有的話,ldset NEEDED libGalaxyParser.so(用前面提到的查看動態庫 依賴關系的辦法可以驗證)。load的時候還是可以找到符號的,所以正確因為沒有會將所以的庫都編譯進去,不管是否需要,只要被列出

正確的鏈接方式是:

g++ -shared  -Wl,--as-needed PyGalaxy.o -lGalaxyParser -lc -lm -ldl -lpthread   -L/home/ocaml/lib/  -lrt  -o PyGalaxy.so

三) 對鏈接順序導致問題的解決方案

在項目開發過層中盡量讓lib是垂直關系,避免循環依賴;越是底層的庫,越是往后面寫!

例如:

g++ ...  obj($?) -l(上層邏輯lib) -l(中間封裝lib) -l(基礎lib) -l(系統lib)  -o $@

這樣寫可以避免很多問題,這個是在搭建項目的構建環境的過程中需要考慮 清楚地,在編譯和鏈接上浪費太多的生命不值得!

四)通過-(-)強制repeat

-(-),它能夠強制"The specified archives are searched repeatedly", 這就是我們要找的啦。比如:

g++ -shared  -Wl,--as-needed PyGalaxy.o Xlinker "-("-lGalaxyParser -lxxx  -lrt"-)"  -o PyGalaxy.so

簡單解釋一下,Xlinker是將后面的一個參數傳給ld(這里就是 "-("-lGalaxyParser -lxxx -lrt"-)"),然后-(-)強制repeat當然就可以找到了可以沒有順序。但是這樣的repeat需要浪費一些時間。

7. 編譯指定庫路徑

在鏈接時語句后面添加如下命令:

-Wl,-rpath=my_thirdparty_lib_path

  對比一下添加前后的Makefile語句。not found時的語句:

 

 

 

  更改之后的語句:

 

 

 

  來看看更改之后的編譯結果:

 

 

 

  可以看到,我的libpaho-mqtt3cs.so.1從我在文章開頭時的【not found】變成了有來源了,而綠色部分的路徑就是我剛剛Makefile中的-Wl,-rpath=之后的路徑。

通常設置庫的方式有四種:

第一種方法:找到缺少的動態庫(由於編譯和鏈接時候的使用到了這個動態庫,所以很容易找得到),將其加到/lib,/usr/lib中的一個文件夾下,這幾個文件夾是系統默認的搜索路徑。將庫文件放置在其中,運行時就可以搜索到了。

第二種方法:設置臨時增加鏈接動態庫的路徑;使用

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path

比如我的libpaho-mqtt3cs.so.1/home/mqtt/MQTT-c/lib目錄下,那我使用的是:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/mqtt/MQTT-c/lib

這種方法設置的是臨時的,系統重啟之后就沒了。當然也可以設置為持久的,這里就不過多講述。

還有一種方法是不常用的,更改配置文件:

   第三種方法:/etc/ld.so.cache中緩存了動態庫路徑,可以通過修改配置文件/etc/ld.so.conf中指定的動態庫搜索路徑,然后執行ldconfig命令來改變。

第四種就是-Wl,-rpath=my_thirdparty_lib_path

這四種方法的優先順序:四->->->


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM