最近在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.c和test.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.c,test.c,main.c進行編譯,生成 .o文件。
gcc -c func.c
gcc -c test.c
gcc -c main.c
然后,將test.c和func.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.cpp為c++代碼,調用了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 動態庫過程:基本的說就是符號重定位,然后合並到全局符號表。
- 在編譯動態庫時:關鍵的看–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.so在mutex.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.so在PyGalaxy.o的左邊,所以gcc認為沒有用到–as-needed將其忽略。但是前面說的動態庫符號解析的特點導致ld認為某些符號是加載(link)的時候才去地址重定位的。但是 libGalaxyParser.so已經被忽略了。所以就算你寫上了依賴的庫,load的時候也會找不到符號,因為編譯庫時已經被忽略啦,一般鏈接PyGalaxy.so庫時會報未定義錯誤。但是為什么沒有-Wl,–as-needed的時候是正確的呢?沒有的話,ld會set 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》
這四種方法的優先順序:四->二->三->一