如何解決C程式中不同靜態庫之間的符號衝突問題
之前在將helix player移植到ios平臺時遇到過這個問題,現在整理一下,給自己做個總結,也希望能對別人有所幫助。
問題的描述:
如果helix在ffmpeg之前是一個小有名氣的開源的播放引擎,由Realnetworks維護,像nokia的塞班系統上都用的是這個播放引擎,而且現在國內的一些手機上還有它的影子。helix將各個功能模組以動態庫的形式進行組織管理。當播放一個視訊時,比如rmvb,它會解析fileformat,然後知道要載入哪個模組來解析這個檔案; decode時,也是根據codec的fourcc來判斷載入哪個decoder庫。
這種組織方式確實有一些好處,比如各個模組相對獨立,每個模組只有在用到時才會被載入,用完後一段時間內不再使用的話會自動解除安裝。但是由於一些基礎的靜態庫會被每個動態庫都包含,所以整個程式所佔的記憶體減小了,但佔的磁碟增加了。
Helix在android平臺上工作得很好,但是當我們想要把它移植到iOS平臺時,卻遇到了一個大麻煩,因為iOS不允許APP中包含動態庫,否則app審查通不過。沒辦法,只得將這些程式編譯成靜態庫。 這樣基礎庫只有一份了,能夠節省一些磁碟空間。
在這個過程中我們遇到的一個問題是: 有幾個格式相同但版本不同的編解碼器(codec),它們是用c語言寫的,它們的程式碼結構和檔名相同,而且很多函式名都一樣。由於c語言的函式編譯之後沒有mangling,導致最終程式中會有多個同名的函式,這在連結的時候能通過(gcc會選擇所遇到第一個同名函式,所以這些同名函式就不能保證哪個會先被連結進可執行程式),但是在執行的時候就會出現各種奇怪的現象,嚴重的甚至crash。
這裡需要理解靜態庫方式和動態庫方式的區別,動態庫方式時不會有問題(這裡指的是dlopen這種顯式的方式,如果是隱式的方式,即將動態庫和可執行程式一起連結的話,也會存在和靜態庫一樣的問題),因為動態庫本身就是一個獨立的實體,它有自己的程式碼段和資料段,它裡面的函式都是相對於它載入時的起始地址來定位的,載入地址在載入前是不確定的,但載入後就是確定的了。從某種意義上說,動態庫有點類似於可執行程式。而靜態庫方式在連結時必須先找到所依賴的函式,然後確定它的偏移地址,這個地址相對於可執行程式的起始地址是固定的,這個地址是連結時確定的。
解決方法:
假設我們有libA和libB兩個靜態庫,他們有很多同名函式,並且這兩個庫通常是在兩個不同的目錄下dir1和dir2。
由於我們有原始碼,所以可以通過修改原始碼的方式來區分不同的codec版本。如果是到原始檔中一行一行地找,那樣將會是一個噩夢。
幸運的是,我們有perl這樣的指令碼語言,它的正則表示式功能會讓你感到遊刃有餘。對了,寫一個perl指令碼,自動地完成這個任務吧。
比如dir1/f1.c和dir2/f2.c都包含了func這個函式,那麼我們可以在dir1/f1.c的開頭加上:#define func LIBA_func, 在dir2/f2.c的開頭加上:#define func LIBB_func。
在用到func的檔案中也如法炮製,比如dir1/f3.c用到了dir1/f1.c中的func,則在dir1/f3.c開頭加上:#define func LIBA_func
dir2/f4.c用到了dir2/f2.c中的func,則在dir2/f4.c開頭加上:#define func LIBB_func
再重新編譯這兩個靜態庫,連結就OK了。
以下是具體步驟:
(1)先分別編譯這兩個靜態庫,得到libA.a和libB.a;
(2)用nm命令將他們的符號表給打印出來,儲存到兩個檔案中;
(3)解析這兩個含有符號表的檔案,找到同時存在這兩個檔案中的那些符號以及它們所在檔案的檔名;
(4)第三步得到了符號->檔名的對映關係,我們將其轉換成檔名->符號的對映關係;
(5)對每個檔案,它們應該同時存在於libA和libB所對應的兩個目錄下。我們對這個檔案中的每個符號,像上面那樣加上巨集定義。
(6)再重新編譯兩個庫就行了。
例子:
設我們在test目錄下有兩個庫lib1和lib2,這兩個庫中都有一個duplicate_func函式。
如果我們將這兩個庫編譯成動態庫,再在main.cpp中呼叫之,顯示的結果是正確的:
可以參考test目錄下build.sh來編譯:
gcc -fPIC -shared -o lib1.so lib1/lib1.c lib1/caller1.cgcc -fPIC -shared -o lib2.so lib2/lib2.c lib2/caller2.cgcc -o dlltest main.cpp ./lib1.so ./lib2.so -ldl執行dlltest結果是:$ ./dlltest lib1/caller1.c: caller1: hello lib1/lib1.c: duplicated_func: hello lib2/caller2.c: caller2: world lib2/lib2.c: duplicated_func: world 如果我們不對這兩個庫做任何改動,直接編譯成靜態庫再連結,則顯示的結果不對:可以參考test目錄下build2.sh來編譯: dir=`pwd` cd lib1 gcc -c lib1.c caller1.c ar -r $dir/lib1.a lib1.o caller1.o ranlib $dir/lib1.a cd $dir cd lib2 gcc -c lib2.c caller2.c ar -r $dir/lib2.a lib2.o caller2.o ranlib $dir/lib2.a cd $dir gcc -o statictest12 main2.cpp ./lib1.a ./lib2.a gcc -o statictest21 main2.cpp ./lib2.a ./lib1.a 執行結果是:$ ./statictest12 caller1.c: caller1: hello lib1.c: duplicated_func: hello caller2.c: caller2: world lib1.c: duplicated_func: world $ ./statictest21 caller1.c: caller1: hello lib2.c: duplicated_func: hello caller2.c: caller2: world lib2.c: duplicated_func: world 可以看到,在上面的靜態庫方式下,執行結果不符合我們的預期,並且執行結果和連結時靜態庫的順序有關。下面採用本文提到的辦法來解決符號衝突:(1) 先提取兩個靜態庫中的符號資訊:./extract_symbol.pl test/lib1.a test/lib2.a > ds.txt extract_symbol.pl 負責將兩個靜態庫中的符號表提取出來,然後將同名的函式和所在的檔案列出來。我們需要將它的結果儲存到一個檔案中,以便在modify.pl中使用。上面例子中的同名函式是duplicate_func,所以extract_symbol.pl的結果是:printf: lib1.o caller1.o lib2.o caller2.o__func__.2181: lib1.o caller1.o lib2.o caller2.oduplicated_func: lib1.o caller1.o caller2.o lib2.o這個結果除了duplicated_func外,還指出printf和__func__.2181重複了,這個沒錯,因為printf確實在兩個庫中都用到了。所以我們需要在modify.pl中把一些我們不需要處理的函式排除掉,通常是一些libc裡的函式,如printf,vsprintf,memcpy,strcmp等。這個可以參考modify.pl中的陣列: my@_exclude_symbols=qw/printf __func__.2181/;這一步也許你會擔心,我沒發現printf在兩個庫中都出現怎麼辦,沒關係,因為如果你沒把它加入到_exclude_symbols中,則printf也會被加上巨集定義,比如#define printf LIB1_printf,這樣在連結時會報錯,因為這個是庫函式,不是你自己定義的函式嘛。這時你再把它加到_exclude_symbols中就行了。(2) 給每個需要解決衝突的檔案加上一行特殊標記: 如 // by lfs./modify.pl -a 1 -f ds.txt -d test/lib1 -d test/lib2 -m LIB1 -m LIB2modify.pl 在action=0時,只是將需要修改的檔案列出來。action=1時,在每個需要改動的檔案頭加上一行特殊的標記。action=2時,會將巨集定義插入到標記行的下面。action=3時,會將巨集定義刪除,但標記行還在。如果想把標記行也刪除,可以通過原始碼管理工具來實現,比如在git中,git checkout就會恢復到沒做任何修改的狀態。(3) 給每個同名函式加上巨集定義:./modify.pl -a 2 -f ds.txt -d test/lib1 -d test/lib2 -m LIB1 -m LIB2(4)再編譯之: $ ./build2.sh (5)執行結果為:$ ./statictest12
caller1.c: caller1: hello
lib1.c: LIB1_duplicated_func: hello
caller2.c: caller2: world
lib2.c: LIB2_duplicated_func: world
$ ./statictest21
caller1.c: caller1: hello
lib1.c: LIB1_duplicated_func: hello
caller2.c: caller2: world
lib2.c: LIB2_duplicated_func: world
從結果可以看出,符號衝突解決後,結果正確。感想與體會:這個過程給我的體會是,做c/c++開發的,掌握一個像perl或python這樣的指令碼語言,有的時候真的是方便很多。當然你可以用c++去實現上面的這些步驟,但用perl這樣原生支援正則表示式的指令碼語言,會事半功倍。用perl來解析符號表,來處理文字相關的任務,再合適不過了。此外,shell也是一個好幫手。