通过runtime源码完整分析消息机制

[TOC]

一、前言

  1. 本文主要分析当我们调用[p test1]的过程中,runtime是如何调用的。

  2. 本文的调试代码地址

  3. 由于runtime源码无法正常跑在真机上,本文是通过断点x86代码来类比分析arm64

  4. 本文的代码是objc-750和之前的480有些不一样的地方;

二、缓存查找

先添加如下测试代码

15454413434857.jpg

23行添加断点

15454419571462.jpg

点击运行程序,程序将断点在23

2.1、计算方法test1的索引

15454455754798.jpg
  1. 计算方法test1的缓存索引 ,4294971225 & 3 == 1
  2. 先打印索引1的地方有没有占用,可以看到索引为1init方法占用了,由于是x86架构索引将42949712251然后再& 3 == 2,输出为2的位置的_key == 0,所有方法test1索引是2(要是arm64架构的话就是-1了),关于如何缓存的请看这篇
#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

2.2、首次调用test(没有缓存的汇编代码)

objc-msg-x86_64.s中打下断点

15454460158277.jpg

继续调试会到宏CacheLookup代码处,由于此时传的是NORMAL,由于我们的代码最终是跑在真机上的,所以我这里还是分析arm64的汇编吧,x86汇编留给你自己分析吧,读懂arm64的汇编代码,x86汇编不在话下区别就是使用的汇编写法不同(x86是AT&T汇编)

2.2.1、objc-msg-arm64.s汇编代码如下

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

如果你觉得一头雾水的话我带你一步一步看汇编,首先初步看到这个汇编代码会发现出现很多不知道的寄存器比如p是什么鬼,PTRSHIFT又是什么鬼,原来在objc-750中,苹果使用宏重定义了,其实就是x,可以查看arm64-asm.h头文件可以看到,由于我们是arm64的所以也就是下面的代码

#if __arm64__

#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
#define p0  x0
#define p1  x1
#define p2  x2
#define p3  x3
#define p4  x4
#define p5  x5
#define p6  x6
#define p7  x7
#define p8  x8
#define p9  x9
#define p10 x10
#define p11 x11
#define p12 x12
#define p13 x13
#define p14 x14
#define p15 x15
#define p16 x16
#define p17 x17

// true arm64
#else

这里还是不厌其烦的我还是把宏替换成习惯的形式来方便理解,替换后的结果如

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #16]    // x10 = buckets, x11 = occupied|mask

    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #(1+3)
                     // x12 = buckets + ((_cmd & mask) << (1+3))

    ldp x17, x9, [x12]      // {imp, sel} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x17, x9, [x12, #-16]!   // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #(1+3)
                                // x12 = buckets + (mask << 1+3)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x17, x9, [x12]      // {imp, sel} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x17, x9, [x12, #-16]!   // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

2.2.2、汇编代码分析

下图表示person类对象的内存所占的内存大小图

15454726370834.jpg
2.2.2.1、ldp x10, x11, [x16, #16

_objc_msgSend 调用CacheLookup传的是NORMAL,入参是放在x1 = SEL, x16 = isa中的,最先执行的是ldp x10, x11, [x16, #16],这行命令的意思是从x16往下16个字节长度开始,连续读取8 + 8个字节的长度分别赋值给x10x11

  1. 由于x16 == isa ,那么 x16 + 16 就是 _buckets的地址,赋值给x10
  2. 由于 x10所占的字节长度是8,那么x11就是接下来的8个字节,也就是读取到了_mask_occupied,所以x11 == _mask + _occupied,由于是小端存储模式,_mask被存放在低16位,可以通过w11取到。

所以执行完ldp x10, x11, [x16, #16]代码,x10x11内容如下

15454735233773.jpg
2.2.2.2、查找缓存列表

2.2.2.1已经找到了缓存_buckets的地址,下面就开始查找缓存了,_buckets的数据结构以及每个元素的地址可以通过下面的方式计算得出如下

15454822875161.jpg
and w12, w1, w11        // x12 = _cmd & mask
add x12, x10, x12, LSL #(1+3) // x12 = buckets + ((_cmd & mask) << (1+3))
ldp x17, x9, [x12]      // {imp, sel} = *bucket

既然已经找到了存放缓存的地址,接下来只要找到调用的方法在缓存中的索引值就可以了,我们需要找的方法是test1,他的SEL4294971226

  1. and w12, w1, w11 初步计算出方法的索引值,4294971226 & 3 == 2
  2. add x12, x10, x12, LSL #(1+3),步骤1计算出了&的结果,接下来就把指针移动到,序号为2的地方,左移4位正好是16个字节大小,要想跳到哪个序号直接序号左移4位即可,再加上初始的地址x10,就是序号的地址。
  3. ldp x17, x9, [x12],分别取出序号2_imp_key赋值给寄存器x17x9

知道每个元素的地址计算方式了,那么就是挨个判断是否是我们要找的方法了,我们初始值是序号2,接下来进入判断逻辑,流程图如下。

untitled.png

由于我们是第一次调用是没有缓存的,所以即将进入方法__objc_msgSend_uncached,不幸的是这个方法还是汇编代码。

三、方法列表查找 & 动态方法解析

如果缓存中没有我们要找的方法,那么就会进入__objc_msgSend_uncached,汇编代码如下

  STATIC_ENTRY __objc_msgSend_uncached
    
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    

实际上这个就是调用的宏MethodTableLookup如果宏没有跳转就会调用TailCallFunctionPointer x17了,我们先看MethodTableLookup

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

如果懂函数栈帧的话,其实这个段汇编代码很简单,头和尾其实是常规操作,前面是开辟栈空间,保护寄存器的值,尾部恢复寄存器的值,以及恢复栈平衡,关键代码其实是bl __class_lookupMethodAndLoadCache3,这个__class_lookupMethodAndLoadCache3方法是一个我们熟悉的高级语言方法了,其实就是_class_lookupMethodAndLoadCache3

在文件objc-runtime-new.m

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}


lookUpImpOrForward的代码如下(有删减)

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    
 retry:    

    // 1.查找缓存 
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 2.查找自己的方法列表并存入缓存
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 3.递归查找父类的缓存或者方法列表
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            
           // 3.1 查找父类的缓存
            imp = cache_getImp(curClass, sel);
             // 如果父类缓存中有 !!!存入的是自己的缓存列表,并不是存到父类的缓存列表!!!
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                // 不在缓存的话存入自己的缓存
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // 3.2 父类缓存中没有 , 就查找父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
             // 如果父类方法列表中有 !!!存入的是自己的缓存列表,并不是存到父类的缓存列表!!!
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 以上情况都没有 就会进入动态方法解析

    if (resolver  &&  !triedResolver) {
    // 也就是我们熟悉的 【+resolveClassMethod or +resolveInstanceMethod.】
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        goto retry;
    }

    // 以上还没有找到 就会进入消息转发阶段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

注释都写在后面了,流程图如下:

lookUpImpOrForward.png

四、消息转发(_objc_msgForward_impcache)

这个方法是汇编实现的,objc-msg-arm64.s中的汇编代码如下

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache


其实它就是对__objc_msgForward的一个封装而已

    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

__objc_forward_handlerruntime的一个默认实现,代码在objc-runtime.m

__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

需要借助一个demo来继续下面的探索,选择以下Forwarding_demo工程

15456305553025.jpg
15456308945541.jpg

4.1、查看调用堆栈-【X86架构】

运行以后会闪退,函数堆栈如下

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000107ca2705 libobjc.A.dylib`objc_exception_throw
    frame #1: 0x0000000108bfdf44 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #2: 0x0000000108be3ed6 CoreFoundation`___forwarding___ + 1446
    frame #3: 0x0000000108be5da8 CoreFoundation`__forwarding_prep_0___ + 120
  * frame #4: 0x000000010738572c Forwarding_demo`-[ViewController viewDidLoad]

【...】

从函数堆栈可以看出调用完performSelector:方法以后,代码就进入了CoreFoundation框架了,然后就到了__forwarding_prep_0___ -> ___forwarding___CoreFoundation代码是开源的可以去这里下载,我下载的是CF-1153.18 2,遗憾的是虽然CoreFoundation代码是开源的,但是苹果没有给出以上方法的实现。

4.1.1、断点查看方法实现

运行程序,分别增加2个断点

(lldb) breakpoint set -n '__forwarding_prep_0___'
Breakpoint 3: where = CoreFoundation`__forwarding_prep_0___, address = 0x00000001079efd30
(lldb) breakpoint set -n '___forwarding___'
Breakpoint 4: where = CoreFoundation`___forwarding___, address = 0x00000001079ed930
(lldb) 

运行程序,程序首先进入__forwarding_prep_0___

CoreFoundation`__forwarding_prep_0___:
->  0x1079efd30 <+0>:   pushq  %rbp
    [...]
    0x1079efda3 <+115>: callq  0x1079ed930               ; ___forwarding___
    [...]
    

过掉这个断点程序会进入___forwarding___

CoreFoundation`___forwarding___:
->  0x1079ed930 <+0>:    pushq  %rbp
    0x1079ed931 <+1>:    movq   %rsp, %rbp
    0x1079ed934 <+4>:    pushq  %r15
    0x1079ed936 <+6>:    pushq  %r14
    0x1079ed938 <+8>:    pushq  %r13
    0x1079ed93a <+10>:   pushq  %r12
    0x1079ed93c <+12>:   pushq  %rbx
    0x1079ed93d <+13>:   subq   $0x28, %rsp
    0x1079ed941 <+17>:   movq   0x26c798(%rip), %rax      ; (void *)0x000000010907b070: __stack_chk_guard
    0x1079ed948 <+24>:   movq   (%rax), %rax
    0x1079ed94b <+27>:   movq   %rax, -0x30(%rbp)
[...]

代码很长,4.1.3会专门分析这个方法

4.1.2、逆向CoreFoundation.framework查看方法实现

除了直接添加断点的方式,还可以通过逆向CoreFoundation.framework来窥探实现,这样更直观,还能知道函数调用关系,CoreFoundation.framework放在/System/Library/Frameworks/CoreFoundation.framework

15456324177613.jpg

找到了___forwarding___实现,并且知道是谁调用的,分别是框出来的部分__forwarding_prep_0_____forwarding_prep_1___,点击进入__forwarding_prep_0___,同样的方式最后定位到了___CFInitialize

15456325212259.jpg

通过汇编确实看出___CFInitialize调用了___forwarding___方法,但是开源的代码根本没有这个相关的影子。

4.1.3、分析forwarding实现

我们已经逆向出来了__forwarding__的汇编代码,但是可读性还是太差了,网上有人已经将汇编代码转成熟悉的样子,这个方法就是消息转发的全部了。

void __forwarding__(BOOL isStret, void *frameStackPointer, ...) {
  id receiver = *(id *)frameStackPointer;
  SEL sel = *(SEL *)(frameStackPointer + 4);

  Class receiverClass = object_getClass(receiver);

  if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
    id forwardingTarget = [receiver forwardingTargetForSelector:sel];
    if (forwardingTarget) {
      return objc_msgSend(forwardingTarget, sel, ...);
    }
  }

  const char *className = class_getName(object_getClass(receiver));
  const char *zombiePrefix = "_NSZombie_";
  size_t prefixLen = strlen(zombiePrefix);
  if (strncmp(className, zombiePrefix, prefixLen) == 0) {
    CFLog(kCFLogLevelError,
          @"-[%s %s]: message sent to deallocated instance %p",
          className + prefixLen,
          sel_getName(sel),
          receiver);
    <breakpoint-interrupt>
  }

  if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
    NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
    if (methodSignature) {
      BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
      if (signatureIsStret != isStret) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
              sel_getName(sel),
              signatureIsStret ? "" : not,
              isStret ? "" : not);
      }
      if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
        NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature
                                                                          frame:frameStackPointer];
        [receiver forwardInvocation:invocation];

        void *returnValue = NULL;
        [invocation getReturnValue:&value];
        return returnValue;
      } else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
              receiver,
              className);
        return 0;
      }
    }
  }

  const char *selName = sel_getName(sel);
  SEL *registeredSel = sel_getUid(selName);

  if (sel != registeredSel) {
    CFLog(kCFLogLevelWarning ,
          @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
          sel,
          selName,
          registeredSel);
  } else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) {
    [receiver doesNotRecognizeSelector:sel];
  } else {
    CFLog(kCFLogLevelWarning ,
          @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
          receiver,
          className);
  }

  // The point of no return.
  kill(getpid(), 9);
}

涉及到的顺序:

  1. forwardingTargetForSelector:
  2. methodSignatureForSelector:
  3. forwardInvocation:;
  4. doesNotRecognizeSelector:

五、objc_msgSend完整流程

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

推荐阅读更多精彩内容