iOS App启动优化《Clang插桩》

前言

iOS App启动优化《二进制重排》我们讲述了App的pre-main阶段的流程以及二进制重排的原理,接着我们就用这篇文章来实现二进制重排。

1 配置Clang插桩

我们打开Clang的官方文档# Clang 13 documentation
在这里有一个Tracing PCs,PC指的是PC寄存器,CPU在读取代码的指针即读取虚拟内存的那一行代码。
所以Tracing PCs跟踪的是CPU执行到的代码。
如何使用的呢,官方文档有详细介绍,我们来配置下,我们先添加一个标记

-fsanitize-coverage=trace-pc-guard 

如图


1

我们编译一下,如图


2

这里报链接错误,找不到符号,这是因为还需要实现两个回调函数
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

我们再次编译,编译成功。
这里有一行printf("INIT: %p %p\n", start, stop);代码,我们看下这个start和stop是什么,我们运行一下,看下效果,如图

3

这里打了两个地址,我们来分析下这些是什么。
startstop都是uint32_tunsigned int类型的指针,这说明上面打印的地址存放的是unsigned int类型数据,这些unsigned int类型数据到底是什么,我们看下,如图
4

这里的startstop代表符号个数,我们看下最后个数据,如图
5

其中11000000是最后一个数据,stop往上走4个字节读取最后一个数据。
*for (uint32_t *x = start; x < stop; x++)
x = ++N;
这里就是从start位置到stop这个位置读取符号个数,这里是11个符号,我们来验证下

void test() {
    
}

我们在ViewController.m加入

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
}

再次运行,如下所示


6

这里变成了13,我们刚才增加了两个方法,加进去了,这说明我们的方法和函数都拦截到了。
我们再来尝试下block,如下

void (^block)(void) = ^(void) {
    
};

我们再来看,如图


7

这说明我们的block也拦截住了。
这个Clang的Trace是全局的,其它的文件一样可以拦截。

+ (void)load {
    
}

+ (void)initialize {
    
}

我们再加上这两个函数,调试如下图

8

说明loadinitialize方法可以拦住的。
__sanitizer_cov_trace_pc_guard我们来调试下这个函数,打个断点,如图
9

运行项目,点击屏幕,看下堆栈,如图
10

我们可以看到touchesBegan这个方法调起了__sanitizer_cov_trace_pc_guard这个函数,我们改下touchesBegan的代码,如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"屏幕点击了");
}

再次运行,如图


11

经过测试发现,__sanitizer_cov_trace_pc_guard这个函数在NSLog之前调用了,我们在block,test()函数加上打印,发现都在NSLog之前调用了__sanitizer_cov_trace_pc_guard这个函数,说明它可以拦截当前项目中的所有符号,系统库和三方库不会拦截。
我们重新排列的是代码的实现的二进制,系统库和三方库不在我们
项目中生成Mach-o文件。
我们自定义的属性生成的get和set方法是可以拦截到的。

2 Clang的原理分析

__sanitizer_cov_trace_pc_guard这个函数是HOOK一切的回调函数,我们来分析下它是如何做到的。
我们在这个函数打个断点,然后运行,过掉所有断点,再次打上断点,点击屏幕,断住之后,我们来分析下它的汇编,如图

12

我们看下touchBegan,如图
13

这里可以看出当touchBegan被调起的时候,立马进入了__sanitizer_cov_trace_pc_guard这个回调函数,
我们看到汇编bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29,这是bl到了__sanitizer_cov_trace_pc_guard这个函数,这说明我们只要添加了Clang插桩的标记,编译器就会在所有的方法,函数,block的代码实现的边缘的加上一句bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard代码,在实现函数的代码之前加上了这句代码,同样在函数和block中都有这样的代码。
这里相当于修改了二进制文件,如何修改的,我们分析下。
在所有的方法前面插入一行代码,只有编译器能做到,编译器在读到我们的方法,函数,block时就会插入这行代码。
我们是通过Other c Flags 添加的标记,所以肯定是在编译期做这个插入代码的动作。

3 获取到符号

我们的目标是最终要生成order文件,那就需要获取到符号名称和顺序,怎么才能获取到呢,我们分析看看。
我们先打开void *PC = __builtin_return_address(0);这个函数,__builtin_return_address这个函数返回的是上一个函数的地址,也就是调用者,这个PC就是上一个函数的地址,也就是函数的第一行代码的地址,第0行插入了bl 0x1000359e4 ; __sanitizer_cov_trace_pc_guard at ClangTrace.m:29代码
我们可以通过个地址获取到符号的名字,代码如下

 Dl_info info;
 dladdr(PC, &info);

这里需要导入#import <dlfcn.h>这个头文件,函数的信息会存在info这个结构体中,我们看下这个结构体的定义,如下

/*
 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;
  • dli_fname m文件名字
  • dli_fbase m文件的地址
  • dli_sname 函数或方法的名字
  • dli_saddr 地址

我们来验证下,代码如下

printf("fname:%s\nfbase:%p\nsname:%s\nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname, info.dli_saddr);

运行,效果如下

fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:+[ClangTrace load]
saddr:0x100029a30
INIT: 0x10002d788 0x10002d7e0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:main
saddr:0x100029e5c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate setWindow:]
saddr:0x100029d7c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr:0x100029ad0
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[ViewController viewDidLoad]
saddr:0x1000297a4
fname:/var/mobile/Containers/Bundle/Application/CDD47581-31B2-48C9-930D-B6D683798DB3/ClangTrace.app/ClangTrace
fbase:0x100024000
sname:-[AppDelegate window]
saddr:0x100029d2c

这里就获取了符号名称以及调用顺序。

4 利用原子队列保存符号

我们在__sanitizer_cov_trace_pc_guard加入

NSLog(@"%@", [NSThread currentThread]);

然后顺ViewController.m文件加入代码

+ (void)load {
    
}

+ (void)initialize {
    
}

void test() {
    NSLog(@"test函数执行");
    
}

void (^block)(void) = ^(void) {
    NSLog(@"block函数执行");
};

- (void)sleepT{
    sleep(3);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelectorInBackground:@selector(sleepT) withObject:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    NSLog(@"屏幕点击了");
    test();
}

运行项目,看下效果,如下所示

2021-09-02 13:40:26.424 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController load]
2021-09-02 13:40:26.425 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
main
2021-09-02 13:40:27.311 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
+[ViewController initialize]
2021-09-02 13:40:27.312 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.318 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate setWindow:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate application:didFinishLaunchingWithOptions:]
2021-09-02 13:40:27.324 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.325 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]
2021-09-02 13:40:27.330 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[ViewController viewDidLoad]
2021-09-02 13:40:27.336 ClangTrace[2817:871748] <NSThread: 0x12ed20eb0>{number = 2, name = (null)}
-[ViewController sleepT]
2021-09-02 13:40:27.346 ClangTrace[2817:871645] <NSThread: 0x12ee2be50>{number = 1, name = main}
-[AppDelegate window]

这里打印了线程的信息,其中<NSThread: 0x12ed20eb0>{number = 2,name = (null)}这里说明了在子线程也是可以获取的。
所以__sanitizer_cov_trace_pc_guard这个回调也是多线程的,我们的方法在子线程执行的话,这个回调函数也是在子线程执行的。在这里存储数据的话,就有多线程的访问,就会造成线程不安全。
我们就用线程安全的队列OSAtomic来处理,代码如下

// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

// 定义符号的结构
typedef struct {
    void * pc; // 函数地址
    void * next; // 下一个函数节点
}SymboNode;

我们在修改__sanitizer_cov_trace_pc_guard这个函数的代码,如下

/// HOOK一切的回调函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    // 创建结构体
    SymboNode *node = malloc(sizeof(SymboNode));
    // 先给node赋值,下个节点暂时先为空
    *node = (SymboNode){PC, NULL};
    // 结构体入栈,node存入symbolList,并把下一个地址给到node的next属性
    OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}

我们在touchesBegan加入代码

 while (YES) {
        SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
        if (node == NULL) {
            break;
        }
        //获取符号信息
        Dl_info info;
        dladdr(node->pc, &info);
        printf("%s\n",info.dli_sname);

    }

运行看下效果,如下

[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]
-[ViewController touchesBegan:withEvent:]

发现死循环了,这是为什么呢?
__sanitizer_cov_trace_pc_guard这个函数把我们的循环也给拦截了,如何解决呢,我们需要修改Other C Flags的标记,如下

-fsanitize-coverage=func,trace-pc-guard

我们再运行一下项目看下结果,如下

-[ViewController touchesBegan:withEvent:]
-[SceneDelegate sceneDidBecomeActive:]
-[SceneDelegate sceneWillEnterForeground:]
-[ViewController sleepT]
-[ViewController viewDidLoad]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate window]
+[ViewController initialize]
-[AppDelegate application:didFinishLaunchingWithOptions:]
main
+[ViewController load]

结果正常了,所以我们应该只拦截方法。

5 方法顺序调整和去除重复的符号

从上面的运行结果可以看出,这个顺序是反的,并且这里还有很多重复,需要我们处理一下,代码如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 定义数组
    NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
    
    while (YES) {
        SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
        if (node == NULL) {
            break;
        }
        //获取符号信息
        Dl_info info;
        dladdr(node->pc, &info);
        // 转字符串
        NSString *name = @(info.dli_sname);
        // 区分函数,block和OC方法的符号,函数与block是一样的
        NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
        [sybleNames addObject:symbolName];
    }
    //反向遍历数组
    NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
    NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
    // 遍历去除重复的符号
    NSString *name;
    while (name = [enumerator nextObject]) {
        if (![funArray containsObject:name]) {
            [funArray addObject:name];
        }
    }
    NSLog(@"%@",funArray);
}

6 生成order文件

最后一步,就把这些拦截到的符号写入到文件中,代码如下

    // 定义数组
    NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
    
    while (YES) {
        SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
        if (node == NULL) {
            break;
        }
        //获取符号信息
        Dl_info info;
        dladdr(node->pc, &info);
        // 转字符串
        NSString *name = @(info.dli_sname);
        // 区分函数,block和OC方法的符号,函数与block是一样的
        NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
        [sybleNames addObject:symbolName];
    }
    //反向遍历数组
    NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
    NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
    // 遍历去除重复的符号
    NSString *name;
    while (name = [enumerator nextObject]) {
        if (![funArray containsObject:name]) {
            [funArray addObject:name];
        }
    }
    NSLog(@"%@",funArray);
    //去掉自己
    [funArray removeObject:[NSString stringWithFormat:@"%s", __func__]];
    // 写入order文件
    // 变成字符串
    NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
    
    // 存储路径
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
    // 文件
    NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
    // 创建文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
    NSLog(@"%@", funcStr);

运行之后, 我们把真机上的文件download下来,找到这个clangTrace.order文件,如图


13

这样就保存下来了,我们可以使用下order文件,如图


14

运行,我们看下map文件的内容,如图
15

跟我们order文件的顺序一模一样的。

7 Swift符号覆盖

我们创建一个swift文件,如下

import UIKit

class SwiftTest: NSObject {
    
    @objc class public func swifttest () {
        print("swifttest......")
    }
}

然后在ViewController.m文件中的load方法加入

+ (void)load {
    
    [SwiftTest swifttest];
}

运行下,如图

16

这是没有拦截到swift的方法,这个时候需要怎么解决呢?
我们需要加一下配置,如图
17

sanitize-coverage=func和-sanitize=undefined参数,运行,如图
18

这时候可以看到swift的方法也拦截住了,这里的swift符号是经过混淆的,这是编译器自动添加的。

总结

这篇文章我们通过Clang的插桩实现了二进制重排,并在此过程解决了很多坑,本人在这个过程中也学习到了很多知识,文章有很多不足之处,不过还是希望可以给大家带来知识。

附完整的代码

// OC插桩标记
-fsanitize-coverage=func,trace-pc-guard
// swift插桩标记
sanitize-coverage=func
-sanitize=undefined

ClangTrace.h代码如下

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ClangTrace : NSObject

/// 生成order文件
/// @param filePath 文件路径
void generateOrderFile(NSString *filePath);
@end

NS_ASSUME_NONNULL_END

ClangTrace.m代码如下

#import "ClangTrace.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>

// 定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

// 定义符号的结构
typedef struct {
    void * pc; // 函数地址
    void * next; // 下一个函数节点
}SymboNode;


@implementation ClangTrace

/// 生成order文件
/// @param filePath 文件路径
void generateOrderFile(NSString *filePath) {
    // 定义数组
    NSMutableArray<NSString *> *sybleNames = [NSMutableArray array];
    while (YES) {
        SymboNode *node =OSAtomicDequeue(&symbolList, offsetof(SymboNode, next));
        if (node == NULL) {
            break;
        }
        //获取符号信息
        Dl_info info;
        dladdr(node->pc, &info);
        // 转字符串
        NSString *name = @(info.dli_sname);
        // 区分函数,block和OC方法的符号,函数与block是一样的
        NSString *symbolName = ([name hasPrefix:@"+["] || [name hasPrefix:@"-["])? name: [@"_" stringByAppendingString:name];
        [sybleNames addObject:symbolName];
    }
    //反向遍历数组
    NSEnumerator *enumerator = [sybleNames reverseObjectEnumerator];
    NSMutableArray *funArray = [NSMutableArray arrayWithCapacity:sizeof(sybleNames.count)];
    // 遍历去除重复的符号
    NSString *name;
    while (name = [enumerator nextObject]) {
        if (![funArray containsObject:name]) {
            [funArray addObject:name];
        }
    }
    //去掉自己
    [funArray removeObject:[@"_" stringByAppendingFormat:@"%s", __func__]];
    // 写入order文件
    // 变成字符串
    NSString *funcStr = [funArray componentsJoinedByString:@"\n"];
    
    if ([ClangTrace isBlankString:filePath]) {
        // 存储路径
        filePath = [NSTemporaryDirectory() stringByAppendingString:@"/clangTrace.order"];
    }
    // 文件
    NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
    // 创建文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    NSLog(@"\n%@", funcStr);
}


/// 项目中的符号个数
/// @param start 起始位置
/// @param stop 结束位置
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
//    static uint64_t N;  // Counter for the guards.
//    if (start == stop || *start) return;  // Initialize only once.
//    printf("INIT: %p %p\n", start, stop);
//    for (uint32_t *x = start; x < stop; x++)
//       *x = ++N;  // Guards should start from 1.
}


/// HOOK一切的回调函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    // 创建结构体
    SymboNode *node = malloc(sizeof(SymboNode));
    // 先给node赋值,下个节点暂时先为空
    *node = (SymboNode){PC, NULL};
    // 结构体入栈,node存入symbolList,并把下一个地址给到node的next属性
    OSAtomicEnqueue(&symbolList, node, offsetof(SymboNode, next));
}


/// 判断字符串是否为空,返回YES字符串为空,NO相反
/// @param str 字符串
+ (BOOL)isBlankString:(NSString *)str {
    NSString *string = str;
    if (string == nil || string == NULL) {
        return YES;
    }
    if ([string isKindOfClass:[NSNull class]]) {
        return YES;
    }
    if ([[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length]==0) {
        return YES;
    }
    return NO;
}

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

推荐阅读更多精彩内容