一個令人蛋疼的NDK鏈接錯誤

背景

我們APP的引擎包engine.so,包含了A、B、C三個工程,但每次都是源碼形式編譯,導致svn上存在多份相同代碼拷貝。非常不科學。。。核心的B工程由我維護,整個SO編譯工程由多個人維護。於是乎偶進行了一次升級:將B源代碼從so工程中解耦:將B打成一個靜態庫,然後編譯So的時候鏈接靜態庫。

開始行動

基本思路:將B的源碼包到一個guide_b外殼工程中,ndk-build生成guide_b.so 的同時誘導生成libB.a靜態庫,然後這個libB.a可以發佈。
android的編譯目錄實在蛋疼,eclipse下設置路徑難用的很。還是習慣命令行下的ndk-build,但是ndk-build的前提是:當前路徑下必須有一個jni文件夾,且它裏面有一個Android.mk文件,以及srcxx子目錄,裏面放了源代碼
爲了遵循android ndk編譯這個蛋疼的規定,而且又不破壞B工程的項目結構(舊的支持xcode和vs編譯),在build目錄下增加一個android子目錄,創建Android.mk文件,然後通過python腳本將所需源碼文件拷貝到android/jni下,所有這些操作通過一個run.bat批處理腳本串聯,build完成以後刪掉拷貝的源碼和編譯中間結果。

將python引入編譯非常靈活。

踩雷了

拿到編譯出的libB.a,放入engine.so編譯工程中,修改mk文件,頭部加入靜態庫預編譯段,
include $(CLEAR_VARS)

LOCAL_MODULE := BModule

LOCAL_SRC_FILES := libB.a
  
include $(PREBUILT_STATIC_LIBRARY)
在so編譯部分加載BModule模塊:LOCAL_STATIC_LIBRARIES := BModule
編譯so非常順利。但是拿到APP工程中,傻眼了build以後的包只有地圖文字,沒有底圖了。

淚奔了,libB.a和engine.so的編譯過程都自我感覺非常之良好啊。。。尼瑪。只能不斷自我打擊,暗示一定什麼環節出問題了。。沒有crash,文字標註還有,但是底圖一直渲染不成功。。。


排雷的過程

1)將B的源碼放入SO編譯工程,最終so包沒問題。只能懷疑自己的libB.a編譯有問題或者鏈接有問題咯,於是進行第一個嘗試:

不直接進行源碼編譯,而是通過ndk自帶的arm-linux-androideabi-ar.exe工具,將源碼編譯時產生的一系列.o文件,手工編譯成.a,然後鏈接這個.a,發現build的so包還是有問題。

source =>	*.o	=>	engine.so

*.o	=>	libB.a	=>	engine.so
	ar
上述兩種路徑:第一條表示源碼編譯,ok;第二條是源碼編譯的中間結果.o文件,手工通過ar打包成 libB.a,然後鏈接libB.a,就有問題。真是見鬼了。!。

2)躲不過了,只能source中增加log,第一次build成libB.a,然後第二次build成engine.so,最後拷貝到android工程中,build APK。

source =>	libB.a	=>	engine.so	=>	apk.

整個蛋疼的定位過程得益於windows的批處理腳本,可以實現半自動化。

不斷重複這個過程,不斷調整log精度。最終定位到底圖瓦片繪製失敗的問題:座標轉化函數GetGeoRect的結果錯誤,導致繪製時候取不到數據

定位問題的原因

尼瑪,疑問重重:源碼編譯沒問題,build成靜態庫,然後再鏈接就有問題。代碼沒有改動,爲何單單這個核心函數出問題呢?
進一步驗證:
GetGeoRect是一個類靜態函數,寫一個main.cpp,測試libB.a中的該函數是否正確,顯式傳入制定的參數,build成可執行程序,然後推到手機上執行,具體參見http://blog.csdn.net/ryfdizuo/article/details/28891649 結果函數執行結果正確。。說明libB.a內這些函數本身沒有問題。問題出在so包的鏈接階段

跟組內一經驗豐富的哥們討論,那天恰好週五,下班前還是沒結果。。。晚上回去後回一直在回想編譯的整個過程,想起他無心的一句話:“是不是可能有重複的定義啥的“。終於想到了一個問題,A工程裏面B工程的兩個頭文件,當時爲了解耦其他人將兩個頭文件重複拷貝了一份,(明顯觸犯了DRY原則)如下, yy.h中包含了靜態函數的GetGeoRect定義,vv.h中包含了render_config_t結構體定義,而GetGeoRect中使用了render_config_t結構體。


我最近一次B模塊升級,更新了vv.h中的render_config_t結構體,內部增加了一個256的char數組。。

附圖新舊vv.h頭文件中的render_config_t結構體:

舊的:新的:

第二天週六,按耐不住奔到公司,更新A模塊中的vv.h頭文件,build出的so包終於正確了。總算是找到問題所在了:

A和B工程中的vv.h和yy.h文件重複,B中vv.h文件最近被更新過。

1)當A、B工程均採用源碼編譯時,最終SO中的GetGeoRect函數內部使用了最新的render_config_t結構體佈局(編譯器可能根據文件的修改時間等等作爲比對條件吧),因此底圖繪製正確。

2)但是當B工程build成靜態庫libB.a時,此時build成SO時,GetGeoRect函數定義採用了A工程中源碼(編譯器可能更加信賴源碼吧),因此render_config_t也採用了舊的內存佈局。因此當調用SO運行時,傳入GetGeoRect函數的render_config_t的對象採用最新的內存佈局,但是內部實際上是按舊的結構體解析和執行,當然結果就完全錯了。


一句話總結問題

文件重複拷貝 =》 導致兩個地方文件更新不同步 =》導致同一個結構體有兩份定義,兩種內存佈局 =》 導致SO中的全局函數中,libB.a中沒有被重複定義的函數採用了新的結構體佈局,被重複的函數則採用了舊的內存佈局 =》最終結果:傳入GetGeoRect函數之前的結構體是新佈局,函數內部按舊佈局解析,所有參數錯亂。

附錄


靜態庫,只是.o文件的集合打包。
動態庫文件:沒有作用域區分,所有函數都以唯一的全局函數形式存在,C++的函數會被name-mangling處理,如果我們希望直接通過函數名獲取so中的函數地址,則使用extern C包裹防止函數名被修改。通過objdump工具可以驗證。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章