靜態庫依賴靜態庫,有順序的問題,否則undefined reference
至於動態鏈接,鏈接器會對依賴進行整理,避免這個問題。動態庫就不存在依賴順序的問題。
https://murphypei.github.io/blog/2019/06/link-sort
如果庫是相互獨立的,則順序不重要。如果不是相互獨立,那么必須對它們進行排序
對於日常命令行編譯命令,一般從左到右分別是可執行文件 ——> 高級庫 ——> 底層庫,避免循環依賴;越是底層的庫,越是往后面寫,可以參考下述命令通式:
g++ ... obj($?) -l(上層邏輯lib) -l(中間封裝lib) -l(基礎lib) -l(系統lib) -o $@
- 靜態庫有順序問題,並且要把自己的庫所依賴的所有的第三方庫都要顯示的指定出來。
動態庫無順序問題,並且只需要顯示的連接自己的動態庫,自己的動態庫依賴的第三方的動態庫無需顯示指定,自己會從rpath中自動連接第三方的動態庫。
但必須把第三方庫依賴的所有的庫,包括路徑都拷貝出來。
例如使用libevent.so, 此時需要把下面這一堆文件都拷貝出來:
lrwxrwxrwx. 1 root root 21 Mar 25 15:18 deps/so/libevent-2.1.so.7 -> libevent-2.1.so.7.0.1 -rwxr-xr-x. 1 root root 386024 Mar 25 15:18 deps/so/libevent-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 26 Mar 25 15:17 deps/so/libevent_core-2.1.so.7 -> libevent_core-2.1.so.7.0.1 -rwxr-xr-x. 1 root root 241936 Mar 25 15:17 deps/so/libevent_core-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 26 Mar 25 15:17 deps/so/libevent_core.so -> libevent_core-2.1.so.7.0.1 lrwxrwxrwx. 1 root root 21 Mar 25 15:18 deps/so/libevent.so -> libevent-2.1.so.7.0.1
- 把第三方靜態庫鏈接到自己的.so動態庫,編譯第三方靜態庫的時候需要加 -fPIC這個參數。
- 可以依賴第三方動態庫生成自己的動態庫
set(CMAKE_INSTALL_RPATH ${MY_RUNTIME_PATH})
add_library(my_dynamic SHARED SRC_list)
TARGET_LINK_LIBRARIES(my_dynamic ${other_so1_so2})
此時生成的libmy_dynamic.so動態庫的rpath是${MY_RUNTIME_PATH},
別人利用我的libmy_dynamic.so動態庫編譯可執行程序時指定的rpath和編譯libmy_dynamic.so動態庫指定rpath不一致的時候,即使可執行程序指定的rpath路徑下包含了所有的動態庫,運行時還是報找不到動態庫。
兩種解決辦法:
- 兩處的rpath保持一致
- 通過export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}: new_path
所有的動態庫都拷貝到new_path路徑下。
https://blog.xizhibei.me/2019/02/24/why-library-order-matters-in-cpp-static-linking/
鏈接過程
我們在編譯的過程中,有兩種鏈接方式,動態與靜態。
- 靜態鏈接:即將依賴庫與調用程序鏈接成一個完成的庫或者可執行文件,運行的時候會將整個程序裝到內存中,方便部署但是體積較大,依賴庫升級的時候需要重新編譯;
- 動態鏈接:即將依賴庫與調用程序分離,不組裝成單個文件,而是在運行的時候,當調用到動態庫的庫時,才會將依賴庫裝載到內存中,這樣會方便與其它程序共享以及升級,可執行文件的體積小,但是不方便部署;
今天我們遇到的問題,發生在靜態鏈接過程中,而鏈接過程的細節如下:
靜態庫中,包含着所有的 obj(*.o) 文件,連接器從左至右搜索,維護着一個 undefined 列表,一旦遇到沒有定義的內容,就會將它加到列表中,如果搜索到了定義的內容,則抽取出 obj 文件,進行鏈接,並將 undefined 內容移出列表,
而其它 obj 文件就會被丟棄(為了減少最終的體積大小),於是一個靜態庫如果不能在搜索過程中被鏈接,它就會被丟棄,而在后面一旦遇到依賴它的庫,就會造成引用無法被鏈接,一直留在undefined 列表中,最終導致編譯錯誤。
- 靜態庫的環形鏈接:
然后我們再來個復雜點的:
$ cat a.cpp
extern int a;
int main() {
return a;
}
$ cat b.cpp
extern int b;
int a = b;
int d;
$ cat d.cpp
extern int d;
extern int e;
int b = d + e;
$ cat e.cpp
int e
再運行上面的編譯后:
$ g++ -c e.cpp -o e.o
$ g++ -c b.cpp -o b.o
$ ar cr libb.a b.o e.o # 注意這里,把 b 與 e 在同一個靜態庫 libb.a 里面
$ g++ -c d.cpp -o d.o
$ ar cr libd.a d.o
會發現那兩種方式都不能解決問題。
而下面這種方式卻能解決
$ g++ a.cpp -L. -lb -ld -lb
為什么?
很簡單,因為回顧下鏈接的過程就能發現,當鏈接器遇到第一個 lb 時,會將 b 加入 undefined 列表,而遇到 ld 時,會將 b 與 ld 鏈接,同時將 d 與 e 加入 undefined 列表,最后遇到 第二個 lb 時,重復同樣的過程,然后順利鏈接。
但是,反過來:
$ g++ a.cpp -L. -ld -lb -ld
卻會失敗。
https://blog.csdn.net/sweetfather/article/details/92691637
- 鏈接的庫文件中又使用了另一個庫文件
這種問題比較隱蔽,也是我最近遇到的與網上大家討論的不同的問題,舉例說明如下,首先,還是看看測試代碼。
從上圖可以看出,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的錯誤,完成編譯鏈接。
https://blog.csdn.net/kl1125290220/article/details/103081969
https://blog.csdn.net/yuhengyue/article/details/83542240
假設程序test依賴動態庫b,而動態庫b依賴動態庫a。
在編譯test的時候,我們希望的是只指定b,而不用指定a,因為我們不希望知道a的依賴庫有哪些,只需關心b。那么我們采用這樣的思路去編譯test的時候,是會報錯的。比如:
我們編譯動態庫a:gcc a.c -o liba.so -shared -fPIC
我們編譯動態庫b:gcc b.c -o libb.so -shared -fPIC -I../a/ -L../a/ -la
我們編譯test:gcc main.c -o test -I../b -L../b -lb
程序會報錯如下:
/usr/bin/ld: warning: liba.so, needed by ../b/libb.so, not found (try using -rpath or -rpath-link)
../b/libb.so: undefined reference to `a'
collect2: ld returned 1 exit status
錯誤的原因在於程序沒能自動找到liba.so,因為liba.so不在ld的默認搜索路徑里。解決方法是如下編譯:
gcc main.c -o test -I../b -L../b -lb -Wl,-rpath=../a
【不顯示指定第三方被依賴的動態庫也行,只要指定了其rpath路徑就可以,ldd libb.so可以看到依賴了liba.so】
————————————————
libB.so的源碼:
#include <stdio.h>
int funB1(){
printf("in funB1");
return 0;
}
int funB2(){
printf("in funB2");
return 0;
}
這里面有兩個函數:funB1和funB2。
其中funB1函數會被libA調用,而funB2會被可執行文件調用。
編譯libB.so:
$ gcc libB.cpp -fPIC -shared -o libB.so
1
libA.so的源碼:
#include <stdio.h>
int funB1();
int funA1(){
printf("in funA1 \n");
funB1();
return 0;
}
該庫中只有一個函數funA1,該函數在內部調用了libB中的funB1函數。且該函數會被可執行文件調用。
編譯libA.so:
$ gcc libA.cpp -fPIC -shared -o libA.so -Wl,-rpath=./ -L./ -lB
1
main.cpp的源碼:
int funA1();
int funB2();
int main(){
funA1();
funB2();
return 0;
}
編譯main.cpp:(復現錯誤的編譯方法)
gcc main.cpp -L./ -lA
1
當我們按照上面的指令編譯main.cpp的時候,便報錯了。
/usr/bin/ld: /tmp/ccDQXTKy.o: undefined reference to symbol '_Z5funB2v'
.//libB.so: error adding symbols: DSO missing from command line
collect2: error: ld returned 1 exit status
(問號.jpg)這,這GCC不是搞事嗎,你明明知道我需要連接libB.so為啥就不幫我鏈接上去呢?難道我libA.so沒有指明要使用libB.so?我們使用下面的指令來看一下
$ ldd libA.so
1
得到如下信息:
linux-vdso.so.1 => (0x00007ffd09def000)
libB.so => ./libB.so (0x00007fc513d7d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc5139b3000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc514181000)
明明libA.so已經顯式的指明我要依賴libB.so了,那為啥在編譯main.cpp的時候鏈接了libA.so,GCC卻還要我們顯式的鏈接libB.so呢?
3 答案
答案很簡單,那就是GCC就是想要你顯式鏈接唄。(你是編譯器,你牛好吧。)那這是為啥呢?
官方一點的答案就是,自從binutils 2.22版本以后,如果你在程序中使用了你依賴的動態庫所依賴的動態庫中的函數時,你就必須顯式的指定你依賴的動態庫所依賴的動態庫。
說那么多,我們更想知道的是,通過修改什么參數可以解決這個問題呢?因為你可能不想在編譯程序的時候要把動態庫所依賴的所有動態庫都顯示鏈接一遍。
4 究極答案
實際上,這是binutils在2.22版本以后,默認把--no-copy-dt-needed-entries這個選項打開了。當打開了這個選項的時候,編譯器在鏈接的時候是不會遞歸的去獲取依賴動態庫的依賴項的,於是就會出現上述的問題。關於該配置項的詳細說明如下:
--copy-dt-needed-entries
--no-copy-dt-needed-entries
This option affects the treatment of dynamic libraries referred to by DT_NEEDED tags inside ELF dynamic libraries mentioned on the command line. Normally the linker won't add a DT_NEEDED
tag to the output binary for each library mentioned in a DT_NEEDED tag in an input dynamic library. With --copy-dt-needed-entries specified on the command line however any dynamic
libraries that follow it will have their DT_NEEDED entries added. The default behaviour can be restored with --no-copy-dt-needed-entries.
This option also has an effect on the resolution of symbols in dynamic libraries. With --copy-dt-needed-entries dynamic libraries mentioned on the command line will be recursively
searched, following their DT_NEEDED tags to other libraries, in order to resolve symbols required by the output binary. With the default setting however the searching of dynamic
libraries that follow it will stop with the dynamic library itself. No DT_NEEDED links will be traversed to resolve symbols.
大概意思就是,跟在--no-copy-dt-needed-entries它后面的庫都不會遍歷其依賴項,使用--copy-dt-needed-entries則相反。也就是使用下面的指令來編譯mian.cpp就可以避免該問題了。
$ gcc main.cpp -L./ -Wl,--copy-dt-needed-entries -lA
1
題外話
在Linux的ELF文件中,如果依賴於其他的動態庫,那么改ELF文件會存在一個.dynamic的段,這個段里面會記錄其依賴的動態庫信息,其標志位為DT_NEEDED。
最近為公司開發的開源超級輕量級插件系統(https://github.com/HSRobot/HPlugin),歡迎大家交流討論。
5 參考文檔
1,DSO missing from command line原因及解決辦法:https://segmentfault.com/a/1190000002462705
2,折騰gcc/g++鏈接時.o文件及庫的順序問題: https://www.cnblogs.com/OCaml/archive/2012/06/18/2554086.html#sec-1-4-1
————————————————