這篇筆記記錄了一次編譯問(wèn)題的排查過(guò)程鹿蜀,還簡(jiǎn)單介紹了一些C/C++編譯的知識(shí)夏哭,希望對(duì)jni編譯錯(cuò)誤的排查能有點(diǎn)幫助检柬。
沒(méi)有接觸過(guò)C/C++的安卓程序員可能在遇到so庫(kù)出現(xiàn)編譯問(wèn)題的時(shí)候會(huì)有點(diǎn)束手無(wú)措,如果這個(gè)庫(kù)是公司內(nèi)部開(kāi)發(fā)的還能丟給負(fù)責(zé)的同事分析竖配,如果是第三方的開(kāi)源庫(kù)可能就需要我們自己去分析了何址。
Alexa rapidjson符號(hào)未定義
我司基于亞馬遜開(kāi)源的Alexa應(yīng)用分為兩個(gè)工程,一個(gè)是avs-device-sdk用于打包出相關(guān)的動(dòng)態(tài)鏈接庫(kù)(當(dāng)然我們對(duì)這個(gè)開(kāi)源項(xiàng)目做了一些定制化修改)进胯。另外一個(gè)是安卓應(yīng)用工程用爪,包含java層的ui展現(xiàn)、jni接口和部分c++的alexa初始化邏輯胁镐。
在更新Alexa sdk版本打包apk的過(guò)程中出現(xiàn)了這樣一個(gè)編譯錯(cuò)誤:
error: undefined reference to 'alexaClientSDK::avsCommon::utils::json::jsonUtils::findNode(rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> > const&, std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> > const&, rapidjson::GenericMemberIterator<true, rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> >*)'
這個(gè)錯(cuò)誤的意思是找不到findNode這個(gè)方法的定義偎血。
c/c++編譯基礎(chǔ)
在這里要大概的介紹下c/c++的編譯流程,C/C++的編譯可以分為下面幾個(gè)步驟:
代碼通過(guò)前面的預(yù)處理希停、編譯烁巫、匯編之后就生成了包含機(jī)器指令的.o文件,一個(gè).c或者.cpp文件就會(huì)生成一個(gè).o文件宠能。每個(gè).o文件都只有自己那部分的代碼亚隙,需要將他們合并到一起才能組成一個(gè)可執(zhí)行程序,這個(gè)合并的過(guò)程叫做鏈接违崇。
而且.o文件之間是有依賴關(guān)系的阿弃,a.o可能調(diào)用到b.o的代碼,如果調(diào)用的代碼的實(shí)現(xiàn)找不到了羞延,就會(huì)出現(xiàn)上面所說(shuō)的undefined reference錯(cuò)誤渣淳。
實(shí)際上鏈接過(guò)程中除了.o文件之外還有有動(dòng)態(tài)鏈接庫(kù)、靜態(tài)鏈接庫(kù)參與進(jìn)來(lái)伴箩。鏈接庫(kù)是一些可復(fù)用的代碼入愧,
動(dòng)態(tài)鏈接庫(kù)、靜態(tài)鏈接庫(kù)windows上對(duì)應(yīng)的是.lib嗤谚、.dll linux上對(duì)應(yīng)的是.a棺蛛、.so它們的區(qū)別在于靜態(tài)鏈接庫(kù)在鏈接的時(shí)候會(huì)和.o文件一起打包到可執(zhí)行文件中,而動(dòng)態(tài)鏈接庫(kù)不會(huì)被打包進(jìn)可執(zhí)行文件巩步,而是在運(yùn)行過(guò)程中被加載旁赊。
.o文件依賴的代碼除了在其他.o文件中,還有可能在靜態(tài)鏈接庫(kù)或者動(dòng)態(tài)鏈接庫(kù)中椅野。(關(guān)于c/c++的編譯我之前有寫(xiě)一篇博客寫(xiě)給安卓程序員的C/C++編譯入門,感興趣的同學(xué)可以去看看)
nm命令使用
出現(xiàn)了上面的錯(cuò)誤终畅,我的第一反應(yīng)是so庫(kù)沒(méi)有鏈接進(jìn)來(lái)籍胯。該函數(shù)的定義在libAVSCommon.so,查看CMakeLists.txt之后發(fā)現(xiàn)該庫(kù)是有正常鏈接的:
target_link_libraries( AlexaJni AVSCommon)
而且如果將這行代碼注釋掉离福,不去鏈接它杖狼,會(huì)出現(xiàn)更多其他的符號(hào)未定義,所以這個(gè)庫(kù)肯定是正常鏈接的术徊。
這種情況下可能考慮是不是出現(xiàn)了宏沒(méi)有打開(kāi)的情況本刽,例如下面這樣的代碼:
#ifdef EN_XXX
// code
#endif
如果這個(gè)EN_XXX的宏沒(méi)有定義,中間的代碼就會(huì)在預(yù)編譯的時(shí)候被清除赠涮。
這樣的情況我們有兩種方法去判斷子寓,第一種是看代碼看看能不能找到這樣的宏,并且看看它是否真的沒(méi)有打開(kāi)笋除。另一種方法是通過(guò)nm命令列舉so文件中的符號(hào):
nm libAVSCommon.so
我們?cè)诶锩媸强梢哉业絝indNode符號(hào)的:
0013f768 T _ZN14alexaClientSDK9avsCommon5utils4json9jsonUtils8findNodeERKN9rapidjson12GenericValueINS4_4UTF8IcEENS4_12CrtAllocatorEEERKNSt6__ndk112basic_stringIcNSC_11char_traitsIcEENSC_9allocatorIcEEEEPNS4_21GenericMemberIteratorILb1ES7_S8_EE
我們首先可以看到的是方法的名字好像和之前不一樣了斜友,感覺(jué)多了很多內(nèi)容。
編譯后C++函數(shù)名會(huì)被修改
其實(shí)C/C++在編譯之后函數(shù)的名字就會(huì)被修改垃它,改成編譯器內(nèi)部的名字鲜屏,每個(gè)編譯器都有一套自己內(nèi)部的名字,這里就看看g++編譯器的實(shí)現(xiàn):
namespace mytest {
void foo(int a) {
}
void foo(int a, int b) {
}
}
namespace 關(guān)鍵字顧名思義是定義一個(gè)命名空間国拇。編程的時(shí)候一個(gè)很令人頭大的問(wèn)題是起名字洛史,很多的庫(kù)可能都會(huì)有個(gè)叫做Utils的類,也有可能有些比較常見(jiàn)的函數(shù)如PrintLog酱吝,當(dāng)導(dǎo)入多個(gè)不同的第三方庫(kù)的時(shí)候很容易出現(xiàn)命名沖突的問(wèn)題也殖,namespace的作用就在于將這些同名的類或者函數(shù)區(qū)分開(kāi)來(lái),一般的第三方庫(kù)都會(huì)以庫(kù)的名字起一個(gè)命名空間务热,這樣就能盡可能的減少命名沖突忆嗜。
c++編譯器實(shí)現(xiàn)命名空間的方式也很簡(jiǎn)單,就在將命名空間的名字拼到函數(shù)名前面崎岂,可以看到編譯之后foo的前面多了mytest這個(gè)命名空間:
0000000100000f60 T __ZN6mytest3fooEi
0000000100000f70 T __ZN6mytest3fooEii
然后我們可以在上面的例子看到有兩個(gè)名字一樣的函數(shù)捆毫,但是他們的參數(shù)數(shù)量不一樣,像這種函數(shù)名字一樣但是參數(shù)數(shù)量或者類型不一樣的情況叫做函數(shù)重載冲甘,其實(shí)C語(yǔ)言不支持函數(shù)重載绩卤,但是C++支持的。C++支持重載的原理就是在函數(shù)的后面拼接參數(shù)類型:
例如void foo(int a)有一個(gè)int類型的參數(shù)江醇,于是就在后面加了一個(gè)i:
__ZN6mytest3fooEi
而void foo(int a, int b)又兩個(gè)int類型的參數(shù)省艳,于是就在后面加了兩個(gè)i:
__ZN6mytest3fooEii
同理的如果有泛型參數(shù)也是會(huì)拼接在函數(shù)名字后面。
像我們?cè)谑褂肑NI的時(shí)候經(jīng)常能看到這樣的代碼
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
就是為了在c++編譯器聲明C代碼防止函數(shù)名被修改嫁审,因?yàn)樵陟o態(tài)注冊(cè)的情況下,java層的natvie方法名和c的函數(shù)名是有對(duì)應(yīng)關(guān)系的赖晶,一旦函數(shù)名被修改了就對(duì)應(yīng)不上了律适。
nm符號(hào)類型
我們可以看到上面nm命令的打印辐烂,在函數(shù)名簽名都會(huì)有一個(gè) T,這個(gè)T指的是該符號(hào)的類型捂贿。符號(hào)類型有很多種纠修,它和C/C++的內(nèi)存模型有很大關(guān)系,作為安卓程序員我們只需要大概了解就夠了厂僧,對(duì)于每一個(gè)符號(hào)來(lái)說(shuō)扣草,其類型如果是小寫(xiě)的,則表明該符號(hào)是local的颜屠;大寫(xiě)則表明該符號(hào)是global(external)的辰妙。下面這張表我是在網(wǎng)上摘抄的別人的博客:
符號(hào) 類型 | 說(shuō)明 |
---|---|
A | 該符號(hào)的值是絕對(duì)的,在以后的鏈接過(guò)程中甫窟,不允許進(jìn)行改變密浑。這樣的符號(hào)值,常常出現(xiàn)在中斷向量表中粗井,例如用符號(hào)來(lái)表示各個(gè)中斷向量函數(shù)在中斷向量表中的位置尔破。 |
B | 該符號(hào)的值出現(xiàn)在非初始化數(shù)據(jù)段(bss)中。例如浇衬,在一個(gè)文件中定義全局static int test懒构。則該符號(hào)test的類型為b,位于bss section中耘擂。其值表示該符號(hào)在bss段中的偏移胆剧。一般而言,bss段分配于RAM中 |
C | 該符號(hào)為common梳星。common symbol是未初始話數(shù)據(jù)段赞赖。該符號(hào)沒(méi)有包含于一個(gè)普通section中。只有在鏈接過(guò)程中才進(jìn)行分配冤灾。符號(hào)的值表示該符號(hào)需要的字節(jié)數(shù)前域。例如在一個(gè)c文件中,定義int test韵吨,并且該符號(hào)在別的地方會(huì)被引用匿垄,則該符號(hào)類型即為C。否則其類型為B归粉。 |
D | 該符號(hào)位于初始話數(shù)據(jù)段中椿疗。一般來(lái)說(shuō),分配到data section中糠悼。例如定義全局int baud_table[5] = {9600, 19200, 38400, 57600, 115200}届榄,則會(huì)分配于初始化數(shù)據(jù)段中。 |
G | 該符號(hào)也位于初始化數(shù)據(jù)段中倔喂。主要用于small object提高訪問(wèn)small data object的一種方式铝条。 |
I | 該符號(hào)是對(duì)另一個(gè)符號(hào)的間接引用靖苇。 |
N | 該符號(hào)是一個(gè)debugging符號(hào)。 |
R | 該符號(hào)位于只讀數(shù)據(jù)區(qū)班缰。例如定義全局const int test[] = {123, 123};則test就是一個(gè)只讀數(shù)據(jù)區(qū)的符號(hào)贤壁。注意在cygwin下如果使用gcc直接編譯成MZ格式時(shí),源文件中的test對(duì)應(yīng)_test埠忘,并且其符號(hào)類型為D脾拆,即初始化數(shù)據(jù)段中。但是如果使用m6812-elf-gcc這樣的交叉編譯工具莹妒,源文件中的test對(duì)應(yīng)目標(biāo)文件的test,即沒(méi)有添加下劃線名船,并且其符號(hào)類型為R。一般而言动羽,位于rodata section包帚。值得注意的是,如果在一個(gè)函數(shù)中定義const char *test = “abc”, const char test_int = 3运吓。使用nm都不會(huì)得到符號(hào)信息渴邦,但是字符串“abc”分配于只讀存儲(chǔ)器中,test在rodata section中拘哨,大小為4谋梭。 |
S | 符號(hào)位于非初始化數(shù)據(jù)區(qū),用于small object倦青。 |
T | 該符號(hào)位于代碼區(qū)text section瓮床。 |
U | 該符號(hào)在當(dāng)前文件中是未定義的,即該符號(hào)的定義在別的文件中产镐。例如隘庄,當(dāng)前文件調(diào)用另一個(gè)文件中定義的函數(shù),在這個(gè)被調(diào)用的函數(shù)在當(dāng)前就是未定義的癣亚;但是在定義它的文件中類型是T丑掺。但是對(duì)于全局變量來(lái)說(shuō),在定義它的文件中述雾,其符號(hào)類型為C街州,在使用它的文件中,其類型為U玻孟。 |
V | 該符號(hào)是一個(gè)weak object唆缴。 |
W | The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. |
- | 該符號(hào)是a.out格式文件中的stabs symbol。 |
? | 該符號(hào)類型沒(méi)有定義 |
函數(shù)的符號(hào)類型是T或者小寫(xiě)的t黍翎,當(dāng)然如果這個(gè)函數(shù)是在其他動(dòng)態(tài)鏈接庫(kù)里面定義的面徽,它的類型就是U。
那local和global有什么區(qū)別嗎匣掸?什么樣的函數(shù)是local的斗忌,什么樣的函數(shù)是global的质礼?
一般情況下函數(shù)默認(rèn)都是global的,這意味著so庫(kù)里面的函數(shù)可以被外部使用织阳,但是如果有些函數(shù)只想so庫(kù)內(nèi)部使用不暴露給外部,就可以添加static關(guān)鍵字:
static void foo() {
}
它的符號(hào)類型就是t砰粹,在so外部是看不到這個(gè)函數(shù)不能使用的:
0000000100000fb0 t __ZL3foov
rapidjson編譯配置
我們使用nm命令打印libAVSCommon.so的符號(hào)表可以看到唧躲,alexaClientSDK::avsCommon::utils::json::jsonUtils::findNode這個(gè)函數(shù)在so里面是存在的,而且是有定義的(符號(hào)類型是T不是U):
0013f768 T _ZN14alexaClientSDK9avsCommon5utils4json9jsonUtils8findNodeERKN9rapidjson12GenericValueINS4_4UTF8IcEENS4_12CrtAllocatorEEERKNSt6__ndk112basic_stringIcNSC_11char_traitsIcEENSC_9allocatorIcEEEEPNS4_21GenericMemberIteratorILb1ES7_S8_EE
一開(kāi)始看到這里我是懵逼的碱璃,很不科學(xué)弄痹。于是我用了一個(gè)方法,修改了findNode函數(shù)的名字嵌器,重新編譯libAVSCommon.so肛真,這樣的話在其他so庫(kù)使用它的地方就出現(xiàn)了找不到符號(hào)的錯(cuò)誤:
error: undefined reference to 'alexaClientSDK::avsCommon::utils::json::jsonUtils::findNode(rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::CrtAllocator> const&, std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> > const&, rapidjson::GenericMemberIterator<true, rapidjson::UTF8<char>, rapidjson::CrtAllocator>*)'
而這里的打印和我們?cè)诰幾gapk的時(shí)候的打印好像不太一樣,編譯apk的時(shí)候的打印如下:
error: undefined reference to 'alexaClientSDK::avsCommon::utils::json::jsonUtils::findNode(rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> > const&, std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> > const&, rapidjson::GenericMemberIterator<true, rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> >*)'
它們的參數(shù)類型不一樣,這個(gè)時(shí)候我們?cè)偃タ纯此暮瘮?shù)定義:
bool findNode(
const rapidjson::Value& jsonNode,
const std::string& key,
rapidjson::Value::ConstMemberIterator* iteratorPtr);
rapidjson::Value的定義如下:
template <typename Encoding, typename Allocator = RAPIDJSON_DEFAULT_ALLOCATOR >
class GenericValue {
...
typedef typename GenericMemberIterator<true,Encoding,Allocator>::Iterator ConstMemberIterator; //!< Constant member iterator for iterating in object.
...
};
//! GenericValue with UTF8 encoding
typedef GenericValue<UTF8<> > Value;
這里我們可以看到爽航,泛型參數(shù)Allocator由RAPIDJSON_DEFAULT_ALLOCATOR這個(gè)宏決定蚓让,默認(rèn)情況下是:
#ifndef RAPIDJSON_DEFAULT_ALLOCATOR
#define RAPIDJSON_DEFAULT_ALLOCATOR MemoryPoolAllocator<CrtAllocator>
#endif
也就是我們?cè)诰幾gapk的時(shí)候出現(xiàn)的類型。問(wèn)題到這里實(shí)際就已經(jīng)找到原因了讥珍,編譯so庫(kù)的時(shí)候和編譯apk的時(shí)候RAPIDJSON_DEFAULT_ALLOCATOR這個(gè)宏的定義不一致历极,導(dǎo)致findNode參數(shù)類型不一致。那肯定是編譯配置導(dǎo)致的,于是在工程下grep搜索一下RAPIDJSON_DEFAULT_ALLOCATOR可以發(fā)現(xiàn)編譯so的cmake配置里有這幾條:
if(RAPIDJSON_MEM_OPTIMIZATION STREQUAL "OFF")
...
elseif(RAPIDJSON_MEM_OPTIMIZATION STREQUAL "CUSTOM")
...
else()
# Use Memory Optimization
message(STATUS "rapidjson memory optimization used")
add_definitions(-DRAPIDJSON_DEFAULT_ALLOCATOR=CrtAllocator)
add_definitions(-DRAPIDJSON_VALUE_DEFAULT_OBJECT_CAPACITY=1)
add_definitions(-DRAPIDJSON_VALUE_DEFAULT_ARRAY_CAPACITY=1)
add_definitions(-DRAPIDJSON_DEFAULT_STACK_ALLOCATOR=CrtAllocator)
endif()
~
而RAPIDJSON_MEM_OPTIMIZATION這個(gè)配置再grep搜索下發(fā)現(xiàn)沒(méi)有地方配置衷佃,所以默認(rèn)使用了rapidjson內(nèi)存優(yōu)化選項(xiàng)趟卸。
所以只需要把這幾行配置拷貝到apk工程的CMakeList.txt就好:
add_definitions(-DRAPIDJSON_DEFAULT_ALLOCATOR=CrtAllocator)
add_definitions(-DRAPIDJSON_VALUE_DEFAULT_OBJECT_CAPACITY=1)
add_definitions(-DRAPIDJSON_VALUE_DEFAULT_ARRAY_CAPACITY=1)
add_definitions(-DRAPIDJSON_DEFAULT_STACK_ALLOCATOR=CrtAllocator)