iOS底层探索之dyld(下):动态链接器流程源码分析

1.回顾

上一篇博文中介绍了动态库静态库的区别,对dyld动态链接器做了初步的探索分析,本篇博文就进一步的对dyld的源码进行分析。

在这里插入图片描述

2. MachO

上篇文章中,已经找到了dyld的入口了,但是在分析源码之前,还得补充点内容。

在iOS中Mach-O(可执行文件)怎么获取呢?

2.1 macOS工程查看MachO

直接编译运行之后就可以得到Mach-O,就是下面这个黑不溜秋的东西。

在这里插入图片描述

2.2 iOS工程查看MachO

iOS工程的话就需要找到Products里面的.app文件

.app文件

然后Show in Finder找到文件所在位置

查看可执行文件

同样这个黑不溜秋的就是MachO可执行文件

MachO文件

2.3 MachOView查看MachO结构

把这个MachO文件,拖拽到MachOView里面就可以查看MachO的结构。

MachOView查看结构

  • Header 头部,包含可以执行的CPU架构,比如x86,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。
MachO结构

3. dyld 源码分析

3.1 dyld::_main

dyld的入口main函数,好家伙!我直呼好家伙啊!近千行的代码!

我直呼好家伙啊!
dyld::_main

这太长了,代码就不贴出来了,一贴出来,本篇博文基本就结束了,太TM长了😂。

  • 弱水三千,我只取一瓢,
  • 代码千行,我只看几行!

好诗好诗啊!哈哈😁

3.1.1 环境变量设置

从底层源码的注释也能知道,这里是dyld的入口

//
// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
        launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
    }

    //Check and see if there are any kernel flags
    dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));

#if __has_feature(ptrauth_calls)
    // Check and see if kernel disabled JOP pointer signing (which lets us load plain arm64 binaries)
    if ( const char* disableStr = _simple_getenv(apple, "ptrauth_disabled") ) {
        if ( strcmp(disableStr, "1") == 0 )
            sKeysDisabled = true;
    }
    else {
        // needed until kernel passes ptrauth_disabled for arm64 main executables
        if ( (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_V8) || (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_ALL) )
            sKeysDisabled = true;
    }
#endif

上面截取main函数部分代码, 主要是if条件对各种环境变量设置的判断

3.1.2 平台信息设置

在所有镜像文件中设置平台 ID,以便于调试器可以判断进程类型。
注意:这里的image不是图像的意思,是指镜像

在这里插入图片描述

3.1.3 共享缓存

检查是否开启,以及共享缓存是否映射到共享区域,这都是系统级别的,系统控制的,缓存是很宝贵的资源。

  • mapSharedCache
    if ( sJustBuildClosure )
        sClosureMode = ClosureMode::On;

    // load shared cache
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache(mainExecutableSlide);
#else
        mapSharedCache(mainExecutableSlide);
#endif

太难了!这么上千行的代码一行一行的往下看,不说眼睛受不了,人都要疯了!(PS:痛苦)


我太难了

那么博主,你有什么好的探索方式吗?
哎,巧了!靓仔!还真有哦!

反推,直接看最后一行代码。我们从结果反推,看看都是什么条件导致的最后return

在这里插入图片描述

通过搜索在main函数里面定位result,发现和sMainExecutable有关系。


        CRSetCrashLogMessage(sLoadingCrashMessage);
        // instantiate ImageLoader for main executable
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

再搜索看看sMainExecutable是什么关键的东西。

sMainExecutable

貌似找sMainExecutable是找对了,在推导的时候,我们要明确我们的目标是要找什么?我们现在不就是要找images镜像嘛!所以就应该对linkbindload这些词要敏感。

从上图中代码中的一些关键代码sInsertedDylibCount/weakBind /linkingMainExecutable这些也可以验证,我们找sMainExecutable是找对了。

3.1.4 link 链接

从代码中也可以发现,sMainExecutablelink链接的一个参数

// load any inserted libraries 加载任何插入的库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        sInsertedDylibCount = sAllImages.size()-1;

        // link main executable 链接main可执行文件
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }

  • 遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载动态库。
  • 链接主程序

3.1.5 主程序main的入口

    #if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else
        // run all initializers
        initializeMainExecutable(); 
    #endif

        // notify any montoring proccesses that this process is about to enter main()
        notifyMonitoringDyldMain();
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        ARIADNEDBG_CODE(220, 1);

#if TARGET_OS_OSX
        if ( gLinkContext.driverKit ) {
            result = (uintptr_t)sEntryOverride;
            if ( result == 0 )
                halt("no entry point registered");
            *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
        }
        else
#endif
        {
            // find entry point for main executable
            result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();//找到主可执行文件的入口点 
            if ( result != 0 ) {
                // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
// main 可执行文件使用 LC_MAIN,我们需要使用 libdyld 中的 helper 来调用 main() 
                if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
                    *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
                else
                    halt("libdyld.dylib support not present for LC_MAIN");
            }
            else {
                // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
                result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
                *startGlue = 0;
            }
        }
    }
  • 执行初始化方法initializeMainExecutable
  • notifyMonitoringDyldMain通知任何监控进程,此进程即将进入main()
  • 通过if判断result寻找主程序的入口点
  • result != 0 时:使用LC_MAIN,我们需要使用 libdyld 中的 helper来调用 main()
  • result == 0: 使用 LC_UNIXTHREADdyld 需要让程序中的“start”main()设置

3.2 initializeMainExecutable

3.2.1 runInitializers

拿到镜像文件的个数,循环开始对镜像进行初始化

// run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up 
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
    

初始化进行前期的相关准备 runInitializers -> processInitializers

3.2.2 processInitializers

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                     InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    uint32_t maxImageCount = context.imageCount()+2;
    ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
    ImageLoader::UninitedUpwards& ups = upsBuffer[0];
    ups.count = 0;
    // Calling recursive init on all images in images list, building a new list of
    // uninitialized upward dependencies.
    for (uintptr_t i=0; i < images.count; ++i) {
        images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}

通过recursiveInitialization递归初始化,下面是核心代码部分

3.2.3 recursiveInitialization

// let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this, NULL);
            

3.2.4 notifySingle

recursiveInitialization可以找到notifySingle

notifySingle
  • notifySingle重点代码

路径加载和镜像文件加载是重点

notifySingle重点代码

全局搜索sNotifyObjCInit,发现是_dyld_objc_notify_init类型

sNotifyObjCInit

3.2.5 objc_init和dyld的联系

registerObjCNotifiers方法的第二个参数赋值给了sNotifyObjCInit

registerObjCNotifiers

全局搜索registerObjCNotifiers看看哪里调用了

发现,在dyldAPIs.cpp文件中找到了registerObjCNotifiers调用

// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

这个_dyld_objc_notify_register看着好眼熟啊!似曾相识燕归来啊!
这不就是libobjc.dylib源码里面_objc_init方法调用了啊!如下图:

_objc_init

在调用_dyld_objc_notify_register函数时,传入了三个参数(map_images的函数地址 、load_images函数unmap_image函数)

那么回到dyld源码的工程,registerObjCNotifiers里面是这样的

sNotifyObjCMapped   = mapped = &map_images
sNotifyObjCInit    = init   = load_images
sNotifyObjCUnmapped = unmapped = unmap_image

到此也就发现,objc_initdyld关联起来了

objc_init()dyld中注册了三个函数,在dyld进行动静态库加载过程时,当特定环境满足的条件下,这三个函数会调用执行。

实在是妙啊!

之前一直在dyld的源码里面,现在我们回到_objc_init里面在进行正向的猜测探索。
_objc_init函数里面打上断点,查看堆栈信息,发现_oc_object_init是在libdispatch的源码里面。

在这里插入图片描述

那么现在就去libdispatch的源码里面看看

libdispatch-1271.120.2源码

_oc_object_init

libdispatch的源码里面发现了_oc_object_init的调用是在libdispatch_init,那么libdispatch_init是由谁来发起的呢?
从堆栈信息发现,是libSystem_initializer

堆栈信息

那么现在又得去LibSystem源码里面看看,发现libdispatch_init确实调用了

Libsystem-1292.120.1源码

LibSystem

那么libSystem_initializer又是谁来发起的呢?
通过堆栈信息可以发现是doModInitFunctions

在这里插入图片描述

doModInitFunctions又回到了dyld了,这就是反向推导到正向的验证过程。

doModInitFunctionsImageLoaderMachO::doInitialization里面被调用了,如下代码:

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
    CRSetCrashLogMessage2(this->getPath());

    // mach-o has -init and static initializers
    doImageInit(context);
    doModInitFunctions(context);
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}

ImageLoader::recursiveInitialization里面又调用了doInitialization,递归初始化镜像文件

    // let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            bool hasInitializers = this->doInitialization(context);

总结一下:objc_init()调用流程如下:

_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

_dyld_objc_notify_register 里面的方法是在什么时候调用的呢?

3.3 map_images和load_images

3.3.1 map_images

回到libObjc.dylib也就是objc的源码工程,在map_imagesload_images的方法处分别打上断点,发现是先走到了map_images处,再控制台bt打印堆栈信息

map_images

map_images方法首先执行的,再查看运行堆栈,其流程为: _dyld_objc_notify_register --> registerObjCNotifiers --> notifyBatchPartial --> map_images
dyld源码中也可以验证,如下:

在这里插入图片描述

3.3.2 load_images

点击继续运行到load_images断点处,再打印堆栈信息

load_images

由此可以知道load_images调用时机:
_dyld_objc_notify_register --> registerObjCNotifiers --> load_images

3.4 main调用时机探索

在上一篇博文iOS底层探索之dyld(上)中,我们有一个测试案例,执行顺序是+ load --> c++ --> main函数
那么就去源码里面探索下,到底是怎么走到main函数的。

3.4.1 load方法调用过程

load_images(const char *path __unused, const struct mach_header *mh)
{
   ....省略代码....

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

这里对所有类的load方法prepare,那么进入prepare_load_methods方法,主要是对所有镜像的懒加载的类、分类prepare,说白了就是找load methods

classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);

schedule_class_load为 类 和任何未加载的superclasses类递归调度 +load

// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

add_class_to_loadable_list把所有的+load收集到一起,先是类,然后再是分类的。

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

递归获取方法收集在loadable_classes[loadable_classes_used].method,是从通过getLoadMethod方法获取的。

/***********************************************************************
* objc_class::getLoadMethod
* fixme
* Called only from add_class_to_loadable_list.
* Locking: runtimeLock must be read- or write-locked by the caller.
**********************************************************************/
IMP 
objc_class::getLoadMethod()
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    ASSERT(isRealized());
    ASSERT(ISA()->isRealized());
    ASSERT(!isMetaClass());
    ASSERT(ISA()->isMetaClass());

    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}

递归所有的baseMethods(),通过strcmp匹配"load",这就是load方法的调用过程

3.4.2 C++函数调用时机

C++函数开始处打上断点,然后再bt打印调用堆栈信息查看

bt查看堆栈信息

从堆栈信息可知是按doInitialization -->doModInitFunctions --> C++(JPFunc)调用顺序,然后我们回到dyld源码搜索doInitialization

doInitialization

在上面已经验证过了notifySingle的里面的流程是load_images方法的调用最后再到load,所以上图也验证了load方法是在C++之前的调用的。

那么再根据堆栈的调用进入到doModInitFunctions方法里面。

doModInitFunctions

这个doModInitFunctions里面就是对于macho_header 的处理,包括Load commandsmacho_segment_commandfor循环遍历macho_section里面的函数指针,也就是包括了C++方法。

3.4.3 dyld如何到main.m函数

C++函数后,dyldbootstrap::start后,会寻找main函数。从堆栈可以知道是从dyld_dyld_start开始

__dyld_start

_dyld_start是汇编写的,从汇编可以知道main函数存在rax寄存器里面,最后会jmp跳转到rax执行main函数。
那么现在去工程代码里面验证一下。

代码验证

通过代码的汇编跟踪调试,也可以发现是和dyld源码里面的汇编执行是一模模一样样。

在这里插入图片描述

通过register read打印寄存器信息也可以发现,rax里面存的就是main,最后通过register read rax读取rax验证了rax = main函数。这就是dyldmain的过程。

这就是dyld的探索分析过程,试问还有谁能,把这么复杂的东西,分析的这么有条有理,我这该死的无处安放的魅力啊!哈哈😁

厉害

4. 总结

  • dyld分析反推到正向验证

  • notifySingle单个通知注入

  • _dyld_objc_notify_register注册回调函数,下句柄,类似blockdyld里面实现了map_imagesload_imagesunmap_image之后回调给_objc_init就可以开始正常调用了。

  • libSystem库是最先开始初始化调用的,在ImageLoaderMachO::doModInitFunctionsImageLoaderMachO::doImageInit里面可以验证,因为objc的相关操作依赖系统,dyld也等系统相关库初始化完成,才对镜像进行初始化、映射等操作。

    doModInitFunctions

  • main 、+ load 、C++执行顺序是+ load --> c++ --> main函数

  • 最后奉上dyld流程分析图

dyld流程分析

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容