JNI編譯錯(cuò)誤-符號(hào)未定義

這篇筆記記錄了一次編譯問(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è)步驟:

1.png

代碼通過(guò)前面的預(yù)處理希停、編譯烁巫、匯編之后就生成了包含機(jī)器指令的.o文件,一個(gè).c或者.cpp文件就會(huì)生成一個(gè).o文件宠能。每個(gè).o文件都只有自己那部分的代碼亚隙,需要將他們合并到一起才能組成一個(gè)可執(zhí)行程序,這個(gè)合并的過(guò)程叫做鏈接违崇。

2.png

而且.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)的:

3.png
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)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市氏义,隨后出現(xiàn)的幾起案子锄列,更是在濱河造成了極大的恐慌,老刑警劉巖惯悠,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邻邮,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吮螺,警方通過(guò)查閱死者的電腦和手機(jī)饶囚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸠补,“玉大人萝风,你說(shuō)我怎么就攤上這事∽涎遥” “怎么了规惰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)泉蝌。 經(jīng)常有香客問(wèn)我歇万,道長(zhǎng)揩晴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任贪磺,我火速辦了婚禮硫兰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寒锚。我一直安慰自己劫映,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布刹前。 她就那樣靜靜地躺著泳赋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喇喉。 梳的紋絲不亂的頭發(fā)上祖今,一...
    開(kāi)封第一講書(shū)人閱讀 49,842評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音拣技,去河邊找鬼千诬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛过咬,可吹牛的內(nèi)容都是我干的大渤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼掸绞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泵三!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起衔掸,我...
    開(kāi)封第一講書(shū)人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤烫幕,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后敞映,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體较曼,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年振愿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捷犹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冕末,死狀恐怖萍歉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情档桃,我是刑警寧澤枪孩,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響蔑舞,放射性物質(zhì)發(fā)生泄漏拒担。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一攻询、第九天 我趴在偏房一處隱蔽的房頂上張望从撼。 院中可真熱鬧,春花似錦蜕窿、人聲如沸谋逻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至浙滤,卻和暖如春阴挣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纺腊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工畔咧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人揖膜。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓誓沸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親壹粟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拜隧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349