iOS底層探索 --- dyld加載流程

dyld加載流程圖

建議大家在閱讀文章的時候傻工,結(jié)合流程圖閱讀。這樣方便理解這個流程孵滞,可以將圖片下載到本地中捆,一邊閱讀一邊比對。


dyld加載流程圖

1坊饶、dyld

1.1 簡介
dyld(The dynamic link editor) --- Apple的動態(tài)鏈接器泄伪。
是蘋果操作系統(tǒng)一個重要的組成部分,在應用被編譯打包成Mach-O文件之后匿级,交由dyld負責鏈接蟋滴,加載程序。在MacOS系統(tǒng)中痘绎,其在/usr/lib/dyld目錄下津函。
1.2 共享緩存
在日常開發(fā)的過程中,我們會用到很多的系統(tǒng)庫孤页,比如:UIKit尔苦、Foundation等等;這些系統(tǒng)庫都是dyld幫我們加載到內(nèi)存中的散庶。但是不同的APP會用到相同的系統(tǒng)庫蕉堰,如果每一個APP運行的時候,dyld都去加載一遍悲龟,那豈不是對內(nèi)存極大的浪費屋讶。于是,為了節(jié)省空間须教,Apple將這些系統(tǒng)庫統(tǒng)一的放在了一個地方:動態(tài)庫共享區(qū)(dyld shared cache)


2皿渗、dyld加載流程

  • 2.1 Demo準備
    在探索dyld加載流程之前斩芭,我們先做好準備工作。
    首先請大家思考一個問題:我們APP的啟動乐疆,是先執(zhí)行main函數(shù)嗎划乖?在main函數(shù)之前,還有其他的操作嗎挤土?
    這里我們創(chuàng)建一個Demo琴庵,一起來探索一下。我們在ViewController中重寫load方法仰美,然后在main.m中添加一個C++方法迷殿,然后觀察一下它們的執(zhí)行順序:

    我們會發(fā)現(xiàn),作為APP入口的main函數(shù)并不是第一個被執(zhí)行的函數(shù)咖杂。這又是為什么呢庆寺?
    接下來我們就一步一步的探索。
  • 2.2 APP啟動流程探索
    通過上面诉字,我們知道懦尝,load方法是最先被執(zhí)行的,那么我們就在load處打一個斷點壤圃,來看看一下load之前還有沒有什么操作陵霉。
    load 斷點

    通過bt指令,在控制臺打印堆棧信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1
  * frame #0: 0x0000000104ea9d8c test`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
    frame #1: 0x00000001a922c25c libobjc.A.dylib`<redacted> + 944
    frame #2: 0x0000000104ef621c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
    frame #3: 0x0000000104f075e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
    frame #4: 0x0000000104f05878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
    frame #5: 0x0000000104f05940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
    frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
    frame #7: 0x0000000104efb928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
    frame #8: 0x0000000104ef5208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
    frame #9: 0x0000000104ef5038 dyld`_dyld_start + 56

可以看到在[ViewController load]還有很多dyld的方法被執(zhí)行埃唯。最早的一個是_dyld_start撩匕。
這個時候就需要我們?nèi)ハ螺ddyld源碼去分析一下了。源碼地址

本次使用的是dyld-832.7.3

  • 2.2.1 _dyld_start
    我們在源碼中搜索_dyld_start墨叛,會發(fā)現(xiàn)是由匯編實現(xiàn)的止毕,我們找到arm64架構對應的代碼如下:
    _dyld_start --- arm64架構

    看到這里大家可能會懵,這些匯編代碼都是干什么用的漠趁?
    其實我們沒必要每一句代碼都弄清楚扁凛,我們只需要找到關鍵信息,理解其意圖就可以了闯传。首先我們會看到下面這一段代碼以及注釋:
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl  __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm

通過注釋可以看到谨朝,這段代碼進入的是dyldbootstrap::start這個函數(shù);這里請注意甥绿,有沒有感覺這個函數(shù)名有點熟悉舔庶。沒錯狐史,我們在上面打印堆棧信息的時候,_dyld_start緊跟著的就是dyldbootstrap::start

  • 2.2.2 dyldbootstrap::start
    dyldbootstrap::start 是指 dyldbootstrap這個命名空間作用域內(nèi)的start函數(shù)朦促。
    cmd + Shift + o 搜索dyldbootstrap :
    dyldbootstrap

    在作用域內(nèi)搜索start函數(shù):
    start函數(shù)

    可以看到start函數(shù)的返回值是通過dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue)獲得的暇矫;這是dyldmain函數(shù)并级。
  • 2.2.3 dyld::main()
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
......
......
......
}

由于main函數(shù)的代碼過長,這里就不展示出來了阱洪,下面我們把關鍵的部分講清楚就可以了。

  • 2.2.3-1 配置相關環(huán)境變量
    1?? :獲取相關環(huán)境信息
    配置環(huán)境變量

如上面的:
getHostInfo(mainExecutableMH, mainExecutableSlide);就是獲取當前運行環(huán)境的架構信息菠镇。
sMainExecutableMachHeader = mainExecutableMH;設置MachHeader(這個我就不多做解釋冗荸,不了解的同學可以看這里3、iOS強化 --- Mach-O 文件).
sMainExecutableSlide = mainExecutableSlide; 設置slide利耍;這個slide就是ASLR計算出來的一個隨機值蚌本,保證程序每一次運行的偏移值都不一樣,防止黑客通過固定地址進行惡意攻擊堂竟。
2?? :設置上下文信息魂毁,檢測進程是否受限
i玻佩、 調(diào)用setContext函數(shù)出嘹,傳入MachHeader以及一些參數(shù)設置上下文。
ii咬崔、 configureProcessRestrictions檢測進程是否受限税稼,在上下文中做出對應的處理。

// _main函數(shù)
setContext(mainExecutableMH, argc, argv, envp, apple);
......
configureProcessRestrictions(mainExecutableMH, envp);

3?? :配置環(huán)境變量

// 檢查設置的環(huán)境變量
checkEnvironmentVariables(envp);
// 如果DYLD_FALLBACK為nil垮斯,將其設置為默認值
defaultUninitializedFallbackPaths(envp);
  • 2.2.3-2 共享緩存
    共享緩存

1?? :檢查是否開啟了共享緩存 checkSharedRegionDisable(iOS下不會被禁用)郎仆。
2?? :加載共享緩存庫 mapSharedCache ---> loadDyldCache ;加載共享緩存有幾種情況:
i :僅加載到當前進程 mapCachePrivate兜蠕,模擬器僅支持加載到當前進程扰肌。
ii :共享緩存是第一次被加載,就去做加載操作 mapCacheSystemWide熊杨。
iii :共享緩存不是第一次被加載曙旭,那么就不做任何處理。

loadDyldCache

  • 2.2.3-3 實例化主程序
    實例化主程序
// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    // try mach-o loader
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);
        return (ImageLoaderMachO*)image;
    }
    
//  throw "main executable not a known format";
}

isCompatibleMachO((const uint8_t*)mh, path) --- 通過macho_header里面的magic晶府、cputype桂躏、cpusubtype去檢測是否兼容。


當檢測通過之后川陆,執(zhí)行instantiateMainExecutable(實例化image)剂习,接著將image添加到鏡像列表中(addImage(image))。

instantiateMainExecutable中较沪,使用sniffLoadCommands來實例化主程序鳞绕;下面我們來簡單了解一下這個函數(shù):

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
                                            unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
                                            const linkedit_data_command** codeSigCmd,
                                            const encryption_info_command** encryptCmd)
{
    *compressed = false;
    *segCount = 0;
    *libCount = 0;
    *codeSigCmd = NULL;
    *encryptCmd = NULL;
        ......
        ......
        ......
// fSegmentsArrayCount is only 8-bits
    if ( *segCount > 255 )
        dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

    // fSegmentsArrayCount is only 8-bits
    if ( *libCount > 4095 )
        dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

    if ( needsAddedLibSystemDepency(*libCount, mh) )
        *libCount = 1;

    // dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
    if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
        *compressed = true;
}

compressed --- 根據(jù)LC_DYLD_INFO_ONLY 來決定。
segCount --- MachO文件中segment的數(shù)量尸曼,最大不能超過 255 個们何。
libCount --- 依賴庫數(shù)量,最大不能超過 4095個骡苞。
codeSigCmd --- 應用簽名垂蜗。
encryptCmd --- 應用加密信息

  • 2.2.3-4 加載插入動態(tài)庫
    插入動態(tài)庫

    ○ 首先利用DYLD_INSERT_LIBRARIES環(huán)境變量來判斷楷扬,是否需要插入動態(tài)庫。
    ○ 然后調(diào)用loadInsertedDylib加載插入動態(tài)庫贴见。
    ○ 最后記錄插入動態(tài)庫的數(shù)量烘苹,sInsertedDylibCount = sAllImages.size()-1;
    插入動態(tài)庫這整個是一個名字片部,這樣的機制給我逆向的時候的代碼注入提供了可能镣衡。
  • 2.2.3-5 鏈接主程序
    鏈接主程序
  • 2.2.3-6 鏈接動態(tài)庫
    鏈接動態(tài)庫
  • 2.2.3-7 符號綁定
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true); 遞歸綁定符號表
    sMainExecutable->weakBind(gLinkContext); 弱符號綁定
  • 2.2.3-8 執(zhí)行初始化方法
    我們在上面打印的函數(shù)調(diào)用棧里面,main之后就是:
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216

我們進入initializeMainExecutable內(nèi)部看一下:

initializeMainExecutable

我們發(fā)現(xiàn)initializeMainExecutable內(nèi)部有一個循環(huán)遍歷档悠,每次都會執(zhí)行runInitializers廊鸥。
cmd + shift + o 搜索runInitializers,跟進去:
runInitializers

發(fā)現(xiàn)runInitializers中又調(diào)用processInitializers為初始化做準備辖所。那么我們跟進processInitializers:
processInitializers

注意看processInitializers里面的for循環(huán)惰说,recursiveInitialization遞歸初始化鏡像。

同樣的搜索recursiveInitialization缘回,跟進去:

recursiveInitialization

我們會發(fā)現(xiàn)吆视,在recursiveInitialization中關鍵的兩步是:
i : 初始化鏡像 --- doInitialization
ii :鏡像初始化完成后,發(fā)送廣播通知 --- notifySingle

notifySingle
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
    //dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
    std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
    if ( handlers != NULL ) {
        dyld_image_info info;
        info.imageLoadAddress   = image->machHeader();
        info.imageFilePath      = image->getRealPath();
        info.imageFileModDate   = image->lastModified();
        for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
            const char* result = (*it)(state, 1, &info);
            if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
                //fprintf(stderr, "  image rejected by handler=%p\n", *it);
                // make copy of thrown string so that later catch clauses can free it
                const char* str = strdup(result);
                throw str;
            }
        }
    }
    if ( state == dyld_image_state_mapped ) { // 是否被映射
        // <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
        // <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
        if (!image->inSharedCache()
            || (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
            dyld_uuid_info info;
            if ( image->getUUID(info.imageUUID) ) {
                info.imageLoadAddress = image->machHeader();
                addNonSharedCacheImageUUID(info);
            }
        }
    }
    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        uint64_t t1 = mach_absolute_time();
        uint64_t t2 = mach_absolute_time();
        uint64_t timeInObjC = t1-t0;
        uint64_t emptyTime = (t2-t1)*100;
        if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
            timingInfo->addTime(image->getShortName(), timeInObjC);
        }
    }
    // mach message csdlc about dynamically unloaded images
    if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
        notifyKernel(*image, false);
        const struct mach_header* loadAddress[] = { image->machHeader() };
        const char* loadPath[] = { image->getPath() };
        notifyMonitoringDyld(true, 1, loadAddress, loadPath);
    }
}

這個函數(shù)中酥宴,重點代碼是:

(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

全局搜索sNotifyObjCInit啦吧,并沒有發(fā)現(xiàn)其函數(shù)實現(xiàn),但是我們找到了它的賦值操作:


這樣我們繼續(xù)跟進拙寡,查找registerObjCNotifiers在哪里被調(diào)用了:

?????? :這個時候授滓,我們發(fā)現(xiàn)在函數(shù)_dyld_objc_notify_register中,registerObjCNotifiers被調(diào)用肆糕。_dyld_objc_notify_register的函數(shù)調(diào)用般堆,我們dyld源碼中并沒有找到,這個時候擎宝,我們需要去libobjc源碼中去尋找郁妈。(本次使用的是objc4-818.2

  • objc4-818.2中我們找到了這個一段代碼
    _objc_init

    我們仔細的比對會發(fā)現(xiàn),_dyld_objc_notify_register傳入的第二個參數(shù)是load_images绍申,那么也就是說給sNotifyObjCInit賦的值就是load_images噩咪;而load_images會調(diào)用所有的+load方法。因此:notifySingle是一個回調(diào)函數(shù)极阅。
  • load_images
    load_images

    接著跟進call_load_methods:
    call_load_methods

    我們會發(fā)現(xiàn)call_load_methods的核心是通過do-while循環(huán)調(diào)用call_class_loads()胃碾。那我們就繼續(xù)跟進:
    call_class_loads

    這里就可以很清晰的看到,最終循環(huán)調(diào)用了所有的+load方法筋搏。

這個時候我們來梳理一下load的函數(shù)調(diào)用棧:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle --> sNotifyObjCInit --> load_images

\color{orange}{這個時候就有一個問題仆百,我們找了}_dyld_objc_notify_register\color{orange}{是在}_objc_init\color{orange}{中被調(diào)用的,那}_objc_init\color{orange}{又是什么時候被調(diào)用的呢奔脐?}
我們繼續(xù)往下探索

doInitialization

doInitialization

我們會發(fā)現(xiàn)俄周,doInitialization主要分成兩部分:
idoImageInit
iidoModInitFunctions

  • doImageInit
    doImageInit

    進入doImageInit源碼發(fā)現(xiàn)吁讨,其核心是一個for循環(huán),注意看代碼的注釋峦朗,libSystem的初始化必須先執(zhí)行建丧。
  • doModInitFunctions
    doModInitFunctions

    doModInitFunctions這個方法內(nèi)部會調(diào)用全局C++對象的構造函數(shù)__attribute__((constructor))C函數(shù)
    這個我們可以通過測試Demo的函數(shù)調(diào)用棧來驗證一下:

探索到這里波势,我們?nèi)匀粵]有發(fā)現(xiàn)_objc_init被調(diào)用的相關信息翎朱。這個時候,我們就需要下一個符號斷點尺铣,來查看_objc_init對應的堆棧信息了拴曲。



符號斷點埋下之后,運行程序凛忿,bt打印堆棧信息:

大家觀察此時的函數(shù)調(diào)用棧澈灼,是不是清晰了很多。

我們先來捋一下_objc_init的函數(shù)調(diào)用棧(此時用的是模擬器侄非,整理函數(shù)調(diào)用棧的時候蕉汪,省略了模擬器相關的一些函數(shù)調(diào)用):_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> ImageLoaderMachO::doInitialization --> ImageLoaderMachO::doModInitFunctions --> libSystem_initializer(libSystem.B.dylib) --> libdispatch_init(libdispatch.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

這里我們一起來回憶一下,在初始化_objc_init的時候逞怨,調(diào)用_dyld_objc_notify_registerload_images被注冊福澡;在我們分析notifySingle的時候叠赦,發(fā)現(xiàn)了重要的函數(shù)sNotifyObjCInit,并且我們之后還發(fā)現(xiàn) sNotifyObjCInit = init = load_images革砸。這樣就形成了一個閉環(huán)除秀。

  • 2.2.3-9 尋找主程序入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();

探索了那么多,我們來串一下dyld::main的調(diào)用流程:

dyld::main

recursiveInitialization

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末算利,一起剝皮案震驚了整個濱河市册踩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌效拭,老刑警劉巖暂吉,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缎患,居然都是意外死亡慕的,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門挤渔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肮街,“玉大人,你說我怎么就攤上這事判导〖蹈福” “怎么了沛硅?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绕辖。 經(jīng)常有香客問我稽鞭,道長,這世上最難降的妖魔是什么引镊? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任朦蕴,我火速辦了婚禮,結(jié)果婚禮上弟头,老公的妹妹穿的比我還像新娘吩抓。我一直安慰自己,他們只是感情好赴恨,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布疹娶。 她就那樣靜靜地躺著,像睡著了一般伦连。 火紅的嫁衣襯著肌膚如雪雨饺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天惑淳,我揣著相機與錄音额港,去河邊找鬼。 笑死歧焦,一個胖子當著我的面吹牛移斩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绢馍,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼向瓷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了舰涌?” 一聲冷哼從身側(cè)響起猖任,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瓷耙,沒想到半個月后朱躺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡哺徊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年室琢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片落追。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡盈滴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巢钓,我是刑警寧澤病苗,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站症汹,受9級特大地震影響硫朦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜背镇,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一咬展、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞒斩,春花似錦破婆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烹笔,卻和暖如春裳扯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谤职。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工饰豺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柬帕。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓哟忍,卻偏偏與公主長得像陷寝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子其馏,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容