原來編譯鏈接還有這么多套路
本文轉(zhuǎn)載自微信公眾號「程序喵大人」,作者程序喵大人 。轉(zhuǎn)載本文請聯(lián)系程序喵大人公眾號。
大家好,我是程序喵。
不知道大家平時編程過程中使用動態(tài)鏈接庫的情況多不多,如果一個程序引用了無數(shù)個動態(tài)鏈接庫,那就有可能引入符號沖突的問題,問題如下:
想象中
實際上
下面我們嘗試解決它:
最開始介紹下g++基本命令參數(shù):
- g++
- -c <file> 編譯源文件,但是不進(jìn)行鏈接
- -o <file> 指定輸出文件的名字
- -s strip,移除符號信息
- -L <dir> 指令搜索鏈接庫的路徑
- -l <lib> 指定要鏈接的鏈接庫
- -shared 產(chǎn)生動態(tài)目標(biāo)文件
先來看一段代碼:
- #include <stdio.h>
- void DoThing() { printf("work \n"); }
再定義一個簡單的main.cc程序:
- #include <stdio.h>
- void DoThing();
- int main() {
- printf("start \n");
- DoThing();
- printf("finished \n");
- return 0;
- }
編譯這兩個文件,并分別打包成靜態(tài)庫:
- g++ -c work.cc -o work.o
- ar rc libwork.a work.o
- g++ -c main.cc -o main.o
- ar rc libmain.a main.o
現(xiàn)在將這兩個靜態(tài)庫鏈接成一個可執(zhí)行文件,注意鏈接器如果發(fā)現(xiàn)當(dāng)前庫中使用了沒有被定義的符號,它只會向后查找,因此,最低級別沒有其它依賴的庫應(yīng)該放在最右邊,如果出現(xiàn)了符號沖突問題,鏈接器會使用最左邊的符號。
如果這樣進(jìn)行鏈接:
- $ g++ -s -L. -o main.exe -lwork -lmain
- ./libmain.a(main.o): In function `main':
- main.cc:(.text+0x11): undefined reference to `DoThing()'
- collect2: error: ld returned 1 exit status
鏈接失敗,因為main庫里的DoThing符號沒有被定義,鏈接器向后查找,沒有找到對應(yīng)的符號定義,這里更改下鏈接庫的順序:
- g++ -s -L. -o main.exe -lmain -lwork
- $ ./main.exe
- start
- work
- finished
鏈接成功。
現(xiàn)在寫一個簡單的容易產(chǎn)生符號沖突的文件conflict.cc:
- #include <stdio.h>
- void DoThing() { printf("conflict \n"); }
編譯并打包成靜態(tài)庫:
- g++ -c conflict.cc -o conflict.o
- ar rc libconflict.a conflict.o
如果按這樣的順序鏈接成一個可執(zhí)行程序:
- $ g++ -s -L. -o main.exe -lmain -lwork -lconflict
- $ ./main.exe
- start
- work
- finished
如果稍微更改一下鏈接的順序:
- $ g++ -s -L. -o main.exe -lmain -lconflict -lwork
- $ ./main.exe
- start
- conflict
- finished
這里發(fā)現(xiàn)順序的不同導(dǎo)致了程序輸出內(nèi)容不同,究其原因就是那潛在的符號沖突。
現(xiàn)在再試試動態(tài)庫,先介紹如何使用動態(tài)庫:
- $ rm libconflict.a
- $ g++ -shared conflict.o -o libconflict.so
- $ g++ -s -L. -o main.exe -lmain -lconflict
- $ LD_LIBRARY_PATH=. ./main.exe
- start
- conflict
- finished
現(xiàn)在再引用一個中間層在動態(tài)鏈接庫中調(diào)用conflict的文件layer.cc
- #include <stdio.h>
- void DoThing();
- void DoLayer() {
- printf("layer \n");
- DoThing();
- }
并把layer和conflict打包成一個動態(tài)鏈接庫:
- $ g++ -c layer.cc -o layer.o
- $ g++ -shared layer.o conflict.o -o libconflict.so
然后更新main.c程序,main里面調(diào)用layer,layer里調(diào)用conflict:
- #include <stdio.h>
- void DoLayer();
- int main() {
- printf("start \n");
- DoLayer();
- printf("finished \n");
- return 0;
- }
編譯鏈接執(zhí)行:
- $ g++ -c main.cc -o main.o
- $ ar rc libmain.a main.o
- $ g++ -s -L. -o main.exe -lmain -lconflict
- $ LD_LIBRARY_PATH=. ./main.exe
- start
- layer
- conflict
- finished
正常輸出,沒啥問題,現(xiàn)在再把之前的work.cc也塞到main.cc中,觀察下沖突:
- #include <stdio.h>
- void DoThing();
- void DoLayer();
- int main() {
- printf("start \n");
- DoThing();
- DoLayer();
- printf("finished \n");
- return 0;
- }
把work.o和main.o打包成一個庫,之后和conflict鏈接成一個可執(zhí)行程序,運行:
- $ g++ -c main.cc -o main.o
- $ ar rc libmain.a main.o work.o
- $ g++ -s -L. -o main.exe -lmain -lconflict
- $ LD_LIBRARY_PATH=. ./main.exe
- start
- work
- layer
- work
- finished
這里輸出了兩個work,正常情況下第二個work應(yīng)該輸出conflict,怎么解決呢?可以考慮使用-fvisibility=hidden來隱藏內(nèi)部的符號,鏈接庫內(nèi)部使用的符號把它隱藏掉,不讓它被導(dǎo)出,外部也不會改變它的調(diào)用路徑。
先使用nm看一下libconflict.so里面的符號:
- $ nm -CD libconflict.so
- w _ITM_deregisterTMCloneTable
- w _ITM_registerTMCloneTable
- 000000000000065a T DoLayer()
- 0000000000000672 T DoThing()
- 0000000000201030 B __bss_start
- w __cxa_finalize
- w __gmon_start__
- 0000000000201030 D _edata
- 0000000000201038 B _end
- 0000000000000688 T _fini
- 0000000000000528 T _init
- U puts
如果把符號隱藏掉,
- $ g++ -fvisibility=hidden -c layer.cc -o layer.o
- $ g++ -fvisibility=hidden -c conflict.cc -o conflict.o
- $ g++ -shared layer.o conflict.o -o libconflict.so
- 再使用nm看一下libconflict.so里面的符號:
- $ nm -CD libconflict.so
- w _ITM_deregisterTMCloneTable
- w _ITM_registerTMCloneTable
- 0000000000201028 B __bss_start
- w __cxa_finalize
- w __gmon_start__
- 0000000000201028 D _edata
- 0000000000201030 B _end
- 0000000000000618 T _fini
- 00000000000004c0 T _init
- U puts
這樣的話main函數(shù)肯定不能調(diào)用DoLayer啦,因為DoLayer符號沒有暴露出來:
- $ g++ -s -L. -o main.exe -lmain -lconflict
- ./libmain.a(main.o): In function `main':
- main.cc:(.text+0x16): undefined reference to `DoLayer()'
- collect2: error: ld returned 1 exit statu
那怎么暴露出來特定符號呢,直接看代碼,改動了layer.cc:
- #include <stdio.h>
- void DoThing();
- __attribute__ ((visibility ("default"))) void DoLayer() {
- printf("layer \n");
- DoThing();
- }
再編譯鏈接運行看看結(jié)果:
- $ g++ -fvisibility=hidden -c layer.cxx -o layer.o
- $ g++ -shared layer.o conflict.o -o libconflict.so
- $ g++ -s -L. -o main.exe -lmain -lconflict
- $ LD_LIBRARY_PATH=. ./main.exe
- start
- work
- layer
- conflict
- finished
發(fā)現(xiàn)已經(jīng)是我們期待的結(jié)果啦,符號沖突的問題因此被解決。
是不是感覺很麻煩,難道每個要暴露的符號都要加上__attribute__這種修飾嗎,這里其實可以寫一個export文件,告訴編譯器要導(dǎo)出的所有符號有哪些。
- export.txt
- {
- global: *DoLayer*;
- local: *;
- };
- g++ -Wl,--version-script=export.txt -s -shared layer.o conflict.o -o libconflict.so
但是這種方式只有在gcc中才可以被使用,我在clang中嘗試使用但是失敗啦,所以為了兼容性不建議使用這種方式,還是消停的使用__attribute__來解決符號沖突問題吧。