Clang插桩

配置Clang插桩

LLVM内置了一个简单的代码覆盖率检测工具(SanitizerCoverage)。它在函数级基本块级边缘级上插入对用户定义函数的调用,通过这种方式可以顺利对OC方法C函数BlockSwift的方法/函数进行全面HOOK

Clang13的文档 关于 Tracing PCs (跟踪CPU执行到的代码),通过Clang插桩我们可以跟踪到所有函数的执行,包括APP启动时刻所调用的

Clang 插桩
  • 搭建测试项目,在Build Setting -> Other C Flags中,增加-fsanitize-coverage=trace-pc-guard的配置
image.png
  • 编译工程有如下报错
image.png

说明__sanitizer_cov_trace_pc_guard_init__sanitizer_cov_trace_pc_guard方法需要我们实现,Clang13的官方文档内容如下

image.png
  • 按照文档,在项目中加入如下代码
#import "ViewController.h" 
#include <stdint.h> 
#include <stdio.h> 
#include <sanitizer/coverage_interface.h>
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 }

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { 
    static uint64_t N; 
    if (start == stop || *start) return; 
    printf("INIT: %p %p\n", start, stop); 
    for (uint32_t *x = start; x < stop; x++) 
        *x = ++N; 
}

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); 
}

@end
__sanitizer_cov_trace_pc_guard_init函数

参数一 start是一个指针,指向无符号int类型4个字节,相当于一个数组的起始位置,即符号的起始位置(是从高位往低位读)
参数二 stop由于数据的地址是往下读的(即从高往低读,所以此时获取的地址并不是stop真正的地址,而是标记的最后的地址,读取stop时由于stop占4个字节,stop真实地址 = stop打印的地址-0x4

// 运行项目,打印以下内容:
INIT: 0x10e838a0c 0x10e838aa0
  • 打印来自__sanitizer_cov_trace_pc_guard_init函数
  • 通过for循环代码,发现从startstop的地址中,存储的是uint32_t类型的值
  • 循环中xuint32_t指针类型,x++表示指针运算,步长+1会增加数据类型的长度
  • uint32_t占4字节,所以循环中的代码含义,每四字节记录一个++N的值
lldb验证

// 读取start 
(lldb) x 0x10e838a0c
0x10e838a0c: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10e838a1c: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
// 读取stop 
(lldb) x 0x10e838aa0-4
0x10e838a9c: 25 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  %...............
0x10e838aac: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  • 读取最后一个值,要在stop地址的基础上减去4字节
  • startstop,读出值为25(注意是16进制)表示当前项目中方法/函数/Block的符号个数。
__sanitizer_cov_trace_pc_guard函数

参数guard是一个哨兵,告诉我们是第几个方法被调用的

  • __sanitizer_cov_trace_pc_guard函数中设置断点,运行项目查看函数调用栈,由main函数调用
image.png
  • 继续调试,进入该函数的断点,由didFinishLaunchingWithOptions函数调用
image.png

我们发现项目中每一个方法和函数的调用,都会触发__sanitizer_cov_trace_pc_guard的断点,并且由当前执行的方法/函数调用

测试__sanitizer_cov_trace_pc_guard方法

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { 
    NSLog(@"__sanitizer_cov_trace_pc_guard");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    NSLog(@"touchesBegan方法执行"); 
    test(); 
}

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

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

// 控制台打印
2021-10-20 20:57:20.938427+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940051+0800 TraceDemo[16078:2734157] touchesBegan方法执行
2021-10-20 20:57:20.940299+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940499+0800 TraceDemo[16078:2734157] test函数执行
2021-10-20 20:57:20.940675+0800 TraceDemo[16078:2734157] __sanitizer_cov_trace_pc_guard
2021-10-20 20:57:20.940861+0800 TraceDemo[16078:2734157] Block执行
  • 从运行结果来看,方法和函数全部被HOOK
  • 被拦截的方法和函数,仅限当前项目中的符号。例如:NSLog等外部符号不会被HOOK
  • 二进制重排的本意,就是将代码实现的二进制中方法/函数符号在启动时刻按照顺序排列在前面。外部符号的方法/函数实现,并不在当前项目中,所以它们的符号也不在重排的范围之内。

测试set方法能否HOOK到?

@interface ViewController ()
@property(nonatomic, assign) int age;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.age = 10;
}
image.png

set方法成功HOOK

Clang插桩的原理

__sanitizer_cov_trace_pc_guard方法添加断点,点击屏幕触发touchesBegan方法进行调试

image.png

在每一个方法/函数/Block内容执行前,都调用了__sanitizer_cov_trace_pc_guard函数

Clang插桩的实现原理:

  • 只要添加了Clang插桩的标记,编译器就会在当前项目中所有方法函数Block的代码实现边缘,插入一句__sanitizer_cov_trace_pc_guard函数的调用代码,达到方法/函数/Block的100%覆盖
  • 相当于编译器在编译时期,修改了当前的二进制文件
  • 修改时机:有可能是语法分析之后生成IR中间代码时进行修改(未验证)

获取符号名称

我们现在已经能HOOK到所有的方法/函数/Block,现在要怎么获取它们的符号,写入order文件?

  • 查看__builtin_return_address方法
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    //获取上一个函数的地址,通过这个地址就能拿到函数的符号名称
    /*
     - PC 当前函数返回上一个调用的地址
     - 0 当前这个函数地址,即当前函数的返回地址
     - 1 当前函数调用者的地址,即上一个函数的返回地址
    */
    void *PC = __builtin_return_address(0); 
    char PcDescr[1024]; 
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); 
}
  • 得到调用者的函数地址获取符号名称
#include <dlfcn.h> 

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { 
    if (!*guard) return; 
    
    void *PC = __builtin_return_address(0);
    Dl_info info; 
    dladdr(PC, &info);
    
    NSLog(@"%s", info.dli_fname); 
    NSLog(@"%p", info.dli_fbase);
    NSLog(@"%s", info.dli_sname);
    NSLog(@"%p", info.dli_saddr); 
}

使用dladdr函数传入函数地址获取基本信息,存入Dl_info结构体

  • Dl_info结构体的定义
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:当前MachO路径(文件的名字)
  • dli_fbase:当前MachO起始地址(文件的地址)
  • dli_sname:函数名称
  • dli_saddr:函数地址

运行项目查看打印结果,发现可以通过dli_sname得到函数名称

/Users/wn/Library/Developer/CoreSimulator/Devices/C53887CF-B3AC-4677-B6FD-DD090CC6D346/data/Containers/Bundle/Application/E211EC24-FFD6-4745-8DFE-345A1DDDC07C/TraceDemo.app/TraceDemo
0x10fb8a000
-[ViewController touchesBegan:withEvent:]
0x10fb8dd20
  • 修改测试代码运行项目
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>

@interface ViewController ()
@end
@implementation ViewController

+ (void)load {
    // NSLog(@"load函数");
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    NSLog(@"%s", info.dli_sname);
}

@end

// 控制台打印
 +[ViewController load]
 main
 -[AppDelegate application:didFinishLaunchingWithOptions:]
 -[SceneDelegate window]
 -[SceneDelegate setWindow:]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[SceneDelegate scene:willConnectToSession:options:]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[ViewController viewDidLoad]
 -[SceneDelegate sceneWillEnterForeground:]
 -[SceneDelegate sceneDidBecomeActive:]

获取到启动时刻所有被调用的方法、函数、Block的函数名称。其中部分函数多次调用,出现了重复符号,还需要对其排重。

通过原子队列保存符号

修改代码,测试能否获取到子线程的符号

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

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

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    void *PC = __builtin_return_address(0);
    NSLog(@"%@", [NSThread currentThread]);
    Dl_info info;
    dladdr(PC, &info);
    NSLog(@"%s", info.dli_sname);
}

// 控制台打印
 -[SceneDelegate sceneDidBecomeActive:]
 <NSThread: 0x6000038a7540>{number = 8, name = (null)}

通过日志可以确定能够获取子线程的符号,同时说明__sanitizer_cov_trace_pc_guard的回调是多线程的。所以当我们通过回调收集函数名称时也要保证线程安全。

  • 以下案例我们使用线程相对安全原子队列进行返回地址的收集
#import <libkern/OSAtomic.h>

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

//定义结构体 
typedef struct { 
    void *pc; 
    void *next; 
} SYNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard)  {
    void *PC = __builtin_return_address(0);
    //创建结构体
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC, NULL};
    //结构体入栈
    //offsetof:参数1传入类型,将下一个节点的地址返回给参数
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}

// 生成order文件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); 
        //取空则停止循环 
        if(node == NULL){ 
            break; 
        } 
        Dl_info info; 
        dladdr(node->pc, &info); 
        NSLog(@"%s", info.dli_sname);
    }
}

原子队列保存符号步骤

  • 定义: 定义原子队列结构体,pc存储当前返回地址,next存储下一个节点地址
  • 收集:
    创建结构体,对pc赋值,next设置为NULL
    结构体入栈
    offsetof:宏,参数1传入类型,将下一个节点的地址返回给参数2
  • 生成order文件
    循环读取node,取空则停止循环
    将返回地址写入Dl_info结构体
    打印符号名称

运行工程,点击屏幕触发touchesBegan方法产生死循环

// 控制台打印
 -[ViewController touchesBegan:withEvent:]
 -[ViewController touchesBegan:withEvent:]
 -[ViewController touchesBegan:withEvent:]
 -[ViewController touchesBegan:withEvent:]
 -[ViewController touchesBegan:withEvent:]
......

解决循环引发的天坑

上面运行工程产生了死循环,下面进行调试

  • touchesBegan方法中设置断点,运行项目查看汇编代码,发现touchesBegan方法中插入了三次__sanitizer_cov_trace_pc_guard函数的调用
image.png

这就是循环引发的天坑,SanitizerCoverage不但拦截方法函数Block,还会对循环进行HOOK。
案例中while循环被HOOK,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan的函数地址,这会导致队列中永远存在一个到两个touchesBegan,next永远获取不完。

解决办法:
Build Setting -> Other C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard对其增加func参数

image.png
  • 再次运行项目,点击屏幕,控制台打印如下
 -[ViewController touchesBegan:withEvent:]
 -[SceneDelegate sceneDidBecomeActive:]
 -[SceneDelegate sceneWillEnterForeground:]
 -[ViewController viewDidLoad]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[SceneDelegate scene:willConnectToSession:options:]
 -[SceneDelegate window]
 -[SceneDelegate window]
 -[SceneDelegate setWindow:]
 -[SceneDelegate window]
 -[AppDelegate application:didFinishLaunchingWithOptions:]
 main
 +[ViewController load]

修改配置项仅拦截方法的调用,成功解决循环引发的天坑。

取反&去重

还有几个问题需要解决?

  • 过滤掉自身touchesBegan的函数名称
  • 不是OC的函数Block等符号,需要在符号名称之前增加_
  • 相同的函数符号,需要进行去重
  • 队列原则先进后出,所以我们需要的符号顺序需要反转

修改touchesBegan方法,解决上述问题

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 定义数组
    NSMutableArray<NSString *> *symbolNames = [NSMutableArray array]; 
    while (YES) { 
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); 
        if(node == NULL){
             break; 
        } 
        Dl_info info; 
        dladdr(node->pc, &info); 
        // 转字符串
        NSString *name = @(info.dli_sname); 
        // 不是OC函数名称添加_,获取符号名称,如果不是+[和-[开头,视为函数或Block,前面加_
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
    } 
    // 反向遍历数组
    symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects]; 
    NSLog(@"%@",symbleNames); 
}

// 运行工程,控制台打印
 TraceDemo[47155:640473] (
    "+[ViewController load]",
    "_main",
    "-[AppDelegate application:didFinishLaunchingWithOptions:]",
    "-[SceneDelegate window]",
    "-[SceneDelegate setWindow:]",
    "-[SceneDelegate window]",
    "-[SceneDelegate window]",
    "-[SceneDelegate scene:willConnectToSession:options:]",
    "-[SceneDelegate window]",
    "-[SceneDelegate window]",
    "-[SceneDelegate window]",
    "-[ViewController viewDidLoad]",
    "-[ViewController setAge:]",
    "-[SceneDelegate sceneWillEnterForeground:]",
    "-[SceneDelegate sceneDidBecomeActive:]",
    "-[ViewController touchesBegan:withEvent:]"
)

相同符号去重

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 定义数组
    NSMutableArray<NSString *> *symbolNames = [NSMutableArray array]; 
    while (YES) { 
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); 
        if(node == NULL){
             break; 
        } 
        Dl_info info; 
        dladdr(node->pc, &info); 
        // 转字符串
        NSString *name = @(info.dli_sname); 
        // 给OC函数名称添加_
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
    } 
    // 反向遍历数组
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    while (name = [em nextObject]) {
        // 如果符号名称不在数组中,添加到数组
        if (![funcs containsObject:name]) {//去重:数组没有name
            [funcs addObject:name];
        }
    }
    //去掉当前函数名称touchesBegan
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
    NSLog(@"%@",funcs);
}

// 运行工程,控制台打印
TraceDemo[47196:643780] (
    "+[ViewController load]",
    "_main",
    "-[AppDelegate application:didFinishLaunchingWithOptions:]",
    "-[SceneDelegate window]",
    "-[SceneDelegate setWindow:]",
    "-[SceneDelegate scene:willConnectToSession:options:]",
    "-[ViewController viewDidLoad]",
    "-[ViewController setAge:]",
    "-[SceneDelegate sceneWillEnterForeground:]",
    "-[SceneDelegate sceneDidBecomeActive:]"
)

生成order文件

  • 修改touchesBegan方法,将符号列表写入.order文件
// 添加load方法与block
+(void)load {
    block();
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 定义数组
    NSMutableArray<NSString *> *symbolNames = [NSMutableArray array]; 
    while (YES) { 
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); 
        if(node == NULL){
             break; 
        } 
        Dl_info info; 
        dladdr(node->pc, &info); 
        // 转字符串
        NSString *name = @(info.dli_sname); 
        // 给OC函数名称添加_
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbolName];
    } 
    // 反向遍历数组
    NSEnumerator * em = [symbleNames reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString * name;
    while (name = [em nextObject]) {
        // 如果符号名称不在数组中,添加到数组
        if (![funcs containsObject:name]) {//去重:数组没有name
            [funcs addObject:name];
        }
    }
    //去掉当前函数名称touchesBegan
    [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
    
    //写入文件
    //1.编程字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
    NSLog(@"%@",funcStr);
}

// 运行工程,控制台打印
+[ViewController load]
_block_block_invoke
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
-[ViewController setAge:]
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
  • 拿到.order文件选择Add Additional Simulators...
image.png
  • 选中案例App点击Downlad Container...,如下图
image.png
  • 选择存放路径下载.xcappdata文件,右键显示包内容,在AppData/tmp目录下找到.order文件
  • .order文件拷贝到工程根目录,在Build Setting -> Order File进行配置
image.png
  • Build Settings -> Write Link Map File设置为YES
image.png
  • 编译项目打开LinkMap文件查看,发现配置生效二进制重排成功
image.png

Swift符号覆盖

  • 创建SwiftTest.swift文件代码如下
import UIKit

class SwiftTest: NSObject {
    @objc class public func swiftTest(){
        print("Swift Test ...")
    }
}
  • ViewControllerload方法中分别调用BlockswiftTest方法
+(void)load
{
    [SwiftTest swiftTest];
    block();
}

void(^block)(void) = ^(void){
    NSLog(@"block函数执行!");
};
  • Other C Flags中的配置仅对Clang编译器生效。而Swift使用swiftc编译器,要想获得swift函数符号,需要对Other Swift Flags添加-sanitize-coverage=func-sanitize=undefined两项
image.png
  • 运行项目,点击屏幕,查看控制台输出内容
+[ViewController load]
_$s9TraceDemo9SwiftTestC05swiftD0yyFZTo
_$s9TraceDemo9SwiftTestC05swiftD0yyFZ
_$ss5print_9separator10terminatoryypd_S2StFfA0_
_$ss5print_9separator10terminatoryypd_S2StFfA1_
_block_block_invoke
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
-[ViewController setAge:]
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]

OC和Swift的混编工程中,成功得到Swift函数符号

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

推荐阅读更多精彩内容