DYLD

一、dyld

dyld (动态库加载器),负责加载程序和程序所以依赖的动态库。内核读取 Mach-O 文件后将读取的内容交给 dyld 进行加载,dyld 加载完毕后才会执行 main 函数。

下面下载一下 dyld 源码,看一下 dyldmain 函数之前做了哪些操作。dyld源码下载地址,这里使用的源码版本是433.6。

首先我们要找到 dyld 的入口,我们尝试在 main 函数打断点看调用栈,结果发现没有有用信息。这时想到我们经常使用 load 方法,它是在 main 之前调用的,我们随便找一个类写一个 load 方法并设置断点,结果如下图:

程序的真正入口.png

** _dyld_start** 是 dyld 启动函数,我们 dyld 调用了 dyldbootstrap::start() , dyldbootstrap::start() 中又调用了 dyld::_main() 函数,我们打开 dyld.cpp 文件,找到 _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)
{
    uintptr_t result = 0;
    // 保存执行文件头部
    sMainExecutableMachHeader = mainExecutableMH;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
    // if this is host dyld, check to see if iOS simulator is being run
    const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
    if ( rootPath != NULL ) {
        // Add dyld to the kernel image info before we jump to the sim
        notifyKernelAboutDyld();

        // look to see if simulator has its own dyld
        char simDyldPath[PATH_MAX]; 
        strlcpy(simDyldPath, rootPath, PATH_MAX);
        strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
        int fd = my_open(simDyldPath, O_RDONLY, 0);
        if ( fd != -1 ) {
            const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
            if ( errMessage != NULL )
                halt(errMessage);
            return result;
        }
    }
#endif

    CRSetCrashLogMessage("dyld: launch started");

    // 设置上下文信息
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // 获取可执行文件路径
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];
    
    // 将相对路径转换成绝对路径
    if ( sExecPath[0] != '/' ) {
        
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }
    
    // 获取文件的名字
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;
    // 配置进程是否受限
    configureProcessRestrictions(mainExecutableMH);

#if __MAC_OS_X_VERSION_MIN_REQUIRED
    if ( gLinkContext.processIsRestricted ) {
        pruneEnvironmentVariables(envp, &apple);
        // set again because envp and apple may have changed or moved
        setContext(mainExecutableMH, argc, argv, envp, apple);
    }
    else
#endif
    {
        // 检查环境变量
        checkEnvironmentVariables(envp);
        // 如果DYLD_FALLBACK为nil,则将其设置为默认值
        defaultUninitializedFallbackPaths(envp);
    }
    
    // 如果设置了DYLD_PRINT_OPTS环境变量,则打印参数
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    // 如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    
    // 读取Mach-O文件的header,获取当前运行架构信息
    getHostInfo(mainExecutableMH, mainExecutableSlide);
    // install gdb notifier
    stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
    stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
    // make initial allocations large enough that it is unlikely to need to be re-alloced
    sImageRoots.reserve(16);
    sAddImageCallbacks.reserve(4);
    sRemoveImageCallbacks.reserve(4);
    sImageFilesNeedingTermination.reserve(16);
    sImageFilesNeedingDOFUnregistration.reserve(8);

#if !TARGET_IPHONE_SIMULATOR
#ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
    // <rdar://problem/6849505> Add gating mechanism to dyld support system order file generation process
    WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
#endif
#endif


    try {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();
        notifyKernelAboutDyld();

#if SUPPORT_ACCELERATE_TABLES
        bool mainExcutableAlreadyRebased = false;

reloadAllImages:
#endif

        CRSetCrashLogMessage(sLoadingCrashMessage);
        
        // 加载可执行文件并生成一个ImageLoader实例对象
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

#if TARGET_IPHONE_SIMULATOR
        // check main executable is not too new for this OS
        {
            if ( ! isSimulatorBinary((uint8_t*)mainExecutableMH, sExecPath) ) {
                throwf("program was built for a platform that is not supported by this runtime");
            }
            uint32_t mainMinOS = sMainExecutable->minOSVersion();

            // dyld is always built for the current OS, so we can get the current OS version
            // from the load command in dyld itself.
            uint32_t dyldMinOS = ImageLoaderMachO::minOSVersion((const mach_header*)&__dso_handle);
            if ( mainMinOS > dyldMinOS ) {
    #if TARGET_OS_WATCH
                throwf("app was built for watchOS %d.%d which is newer than this simulator %d.%d",
                        mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
                        dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
    #elif TARGET_OS_TV
                throwf("app was built for tvOS %d.%d which is newer than this simulator %d.%d",
                        mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
                        dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
    #else
                throwf("app was built for iOS %d.%d which is newer than this simulator %d.%d",
                        mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
                        dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
    #endif
            }
        }
#endif


    #if __MAC_OS_X_VERSION_MIN_REQUIRED
        // <rdar://problem/22805519> be less strict about old mach-o binaries
        uint32_t mainSDK = sMainExecutable->sdkVersion();
        gLinkContext.strictMachORequired = (mainSDK >= DYLD_MACOSX_VERSION_10_12) || gLinkContext.processUsingLibraryValidation;
    #else
        // simulators, iOS, tvOS, and watchOS are always strict
        gLinkContext.strictMachORequired = true;
    #endif

        // 检查共享缓存是否开启,iOS中必须开启
        checkSharedRegionDisable();
    #if DYLD_SHARED_CACHE_SUPPORT
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
            // 将共享缓存映射到共享区域
            mapSharedCache();
        } else {
            dyld_kernel_image_info_t kernelCacheInfo;
            bzero(&kernelCacheInfo.uuid[0], sizeof(uuid_t));
            kernelCacheInfo.load_addr = 0;
            kernelCacheInfo.fsobjid.fid_objno = 0;
            kernelCacheInfo.fsobjid.fid_generation = 0;
            kernelCacheInfo.fsid.val[0] = 0;
            kernelCacheInfo.fsid.val[0] = 0;
            task_register_dyld_shared_cache_image_info(mach_task_self(), kernelCacheInfo, true, false);
        }
    #endif

    #if SUPPORT_ACCELERATE_TABLES
        sAllImages.reserve((sAllCacheImagesProxy != NULL) ? 16 : INITIAL_IMAGE_COUNT);
    #else
        sAllImages.reserve(INITIAL_IMAGE_COUNT);
    #endif

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        // 检查库的版本是否有更新,如果有则覆盖原有的,如两个应用依赖不同版本的动态库,后面加载的动态库需要覆盖前面的版本
        checkVersionedPaths();
    #endif


        // dyld_all_image_infos image list does not contain dyld
        // add it as dyldPath field in dyld_all_image_infos
        // for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_IPHONE_SIMULATOR
        // get path of host dyld from table of syscall vectors in host dyld
        void* addressInDyld = gSyscallHelpers;
#else
        // get path of dyld itself
        void*  addressInDyld = (void*)&__dso_handle;
#endif
        char dyldPathBuffer[MAXPATHLEN+1];
        int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
        if ( len > 0 ) {
            dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
            if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
                gProcessInfo->dyldPath = strdup(dyldPathBuffer);
        }

        // 加载 DYLD_INSERT_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
        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;
        }

        // 链接插入动态库
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
                image->setNeverUnloadRecursive();
            }
            // only INSERTED libraries can interpose
            // register interposing info after all inserted libraries are bound so chaining works
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->registerInterposing();
            }
        }

        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            ImageLoader* image = sAllImages[i];
            if ( image->inSharedCache() )
                continue;
            image->registerInterposing();
        }
    #if SUPPORT_ACCELERATE_TABLES
        if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
            // Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
            ImageLoader::clearInterposingTuples();
            // unmap all loaded dylibs (but not main executable)
            for (long i=1; i < sAllImages.size(); ++i) {
                ImageLoader* image = sAllImages[i];
                if ( image == sMainExecutable )
                    continue;
                if ( image == sAllCacheImagesProxy )
                    continue;
                image->setCanUnload();
                ImageLoader::deleteImage(image);
            }
            // note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
            sAllImages.clear();
            sImageRoots.clear();
            sImageFilesNeedingTermination.clear();
            sImageFilesNeedingDOFUnregistration.clear();
            sAddImageCallbacks.clear();
            sRemoveImageCallbacks.clear();
            sDisableAcceleratorTables = true;
            sAllCacheImagesProxy = NULL;
            sMappedRangesStart = NULL;
            mainExcutableAlreadyRebased = true;
            gLinkContext.linkingMainExecutable = false;
            resetAllImages();
            goto reloadAllImages;
        }
    #endif

        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.linkingMainExecutable = false;
        
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        // 若符号绑定
        sMainExecutable->weakBind(gLinkContext);

    #if DYLD_SHARED_CACHE_SUPPORT
        // If cache has branch island dylibs, tell debugger about them
        if ( (sSharedCache != NULL) && (sSharedCache->mappingOffset >= 0x78) && (sSharedCache->branchPoolsOffset != 0) ) {
            uint32_t count = sSharedCache->branchPoolsCount;
            dyld_image_info info[count];
            const uint64_t* poolAddress = (uint64_t*)((char*)sSharedCache + sSharedCache->branchPoolsOffset);
            // <rdar://problem/20799203> empty branch pools can be in development cache
            if ( ((mach_header*)poolAddress)->magic == sMainExecutableMachHeader->magic ) {
                for (int poolIndex=0; poolIndex < count; ++poolIndex) {
                    uint64_t poolAddr = poolAddress[poolIndex] + sSharedCacheSlide;
                    info[poolIndex].imageLoadAddress = (mach_header*)(long)poolAddr;
                    info[poolIndex].imageFilePath = "dyld_shared_cache_branch_islands";
                    info[poolIndex].imageFileModDate = 0;
                }
                // add to all_images list
                addImagesToAllImages(count, info);
                // tell gdb about new branch island images
                gProcessInfo->notification(dyld_image_adding, count, info);
            }
        }
    #endif

        CRSetCrashLogMessage("dyld: launch, running initializers");
    #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();

        // 寻找入口并执行,main函数
        result = (uintptr_t)sMainExecutable->getThreadPC();
        if ( result != 0 ) {
            // main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
            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->getMain();
            *startGlue = 0;
        }
    }
    catch(const char* message) {
        syncAllImages();
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failed\n");
    }

    CRSetCrashLogMessage(NULL);
    
    return result;
}

从代码中可以看到 dyld 的加载流程主要包括如下 9 个步骤:

  • 设置上下文信息,配置进程是否受限
  • 配置环境变量,获取当前运行架构
  • 加载可执行文件,生成一个 ImageLoader 实例对象
  • 检查共享缓存是否映射到共享区域
  • 加载所有的插入动态库
  • 链接主程序
  • 链接所有插入动态库,执行符号替换
  • 执行初始化方法
  • 寻找主程序入口

二、懒加载与非懒加载

1.懒加载绑定

懒加载的符号在第一次调用之前的符号地址不是真实的符号地址,只有在第一次调用时才会绑定真实的符号地址,第二次就会直接调用真实的符号地址。
通过一个例子来看懒加载绑定的过程,新建一个 Test 工程,写 2 句输出语句,如下:


Demo.png

读取 __stubs 中存放函数地址的地址

在两处代码设置断点,运行程序,运行到断点出点击菜单栏 Debug -> Debug Workflow -> Always Show Disassembly 查看汇编代码。

汇编代码.png

可以看到第一处 printf 调用地址 0x100cf2584 ,该地址是当前 printf 加载到内存中的地址,而且也可以看出该地址是在 Mach-O 文件中的 __stubs 节中,而该节又在 __TEXT 段中。

我们要在 Mach-O 中查看该地址对应的内容就需要计算出内容在 Mach-O 文件中的偏移地址。

在计算偏移地址之前需要介绍一下 ALSR(地址空间配置随机加载)ALSR 是一种防范内存损坏漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。

由于 ALSR 技术的存在,应用每次加载到内存中的其实地址是不同,我们可以在 LLDB 调试窗口通过 image list | grep <Mach-O name> 获取当前应用的初始地址。

查看 Mach-O 起始地址.png

这时候我们就可以计算偏移地址了,偏移地址 = 目标内存地址 - Mach-O起始内存地址 + 当前架构起始地址,我们可以用 LLDB 进行计算偏移大小:

偏移大小.png

0x6584 就是我们要查看内容在 Mach-O 文件在当前架构内容的偏移大小,然后通过 MachOView 软件查看 Mach-O 文件发现,改可执行文件只有一个架构,所以当前架构起始地址为 0x0 ,最终我们得出我们要找的 printf 在 **Mach-O 文件中的偏移地址就是 0x6584

printf.png

读取 __la_symobol_ptr 中函数地址

接下来我们通过 si 命令跟进查看读取地址指令

读取地址.png

由于还未绑定此时读取的并非是函数真实地址,而是函数绑定指令

此处 ldr 指令的意思就是取 pc + 0x1c40 的值

计算绑定指令地址.png

0x100cf41c8 在文件中的偏移地址是 0x81c8 ,然后通过 Mach-O 查看,可以看出上面其实取的是 __la_symbol_ptr__printf 的值:

此时该位置存储并不是函数真实地址.png

从动态库中获取函数真实地址

继续分析汇编指令,通过命令 p/xg 0x100cf41c8 可以查看 0x100cf41c8 地址的值是 0x0000000100cf28000x0000000100cf2800 对应文件中的 0x6800__stub_helper 节中。

函数绑定辅助指令.png

使用 dis -s 0x0000000100cf2800 命令反汇编查看

image.png

可以看到该指令在 0x100cf2808 取值 0x0000040c,这是一个偏移值,是距离 Dynamic Loader InfoLazy Binding Info 起始位置的偏移。
从文件中可以看到起始地址是 0xc368,加上 0x0000040c 最终结果是 0x0000c774,通过 Mach-O 应用查看 0x0000c774 处:
偏移值.png

分析一下上图的内容, dylib(2) 表示该符号要从当前文件的第 2 个 LC_LOAD_DYLIB 中寻找,也就是在 libSystem.B.dylib 中寻找 printf 的真实地址:segment(2)offset(456) 表示将找到的真实地址写入到当前架构第 2 个 segment(不包括 __PAGEZERO) 偏移 456 的地方。
第 2 个 segment__DATA 段,其起始地址为 0x8000,偏移 456 的位置就是 0x8000 + 456 = 0x81c8,而 0x81c8 就是 __la_symbol_ptrprintf 符号的指针位置,也就是将找到的真实地址写到 __la_symbol_ptr 中。

LC_LOAD_DYLIB.png

执行完 0x100cf2808 处代码,执行 0x100cf2810 处的代码,这里跳转到 0x100cf25f0,此处对应与文件中 __stub_helper 节起始位置, 反汇编查看此处代码:

image.png

此处可以看到调用了 dyld_stub_binder, 使用 dis -a 0x00000001acf3cb94 继续跟进 dyld_stub_binder
image.png

0x1acf3cbcc 处下断点,程序断下后 x1 寄存器的值是 0x0000040c,就是上面所说的偏移值。此处调用了 _dyld_fast_stub_entry 函数,该函数根据偏移值进行真正的绑定。

动态绑定到此结束,继续指定到了我们第二次的 printf 函数断点处,通过 si 跟进发现我们再次从 __la_symbol_ptr 去除的 printf 符号的指针地址是真实的 printf 符号地址。

image.png

懒加载函数绑定流程

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

推荐阅读更多精彩内容