通過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架構索引將4294971225?1然后再& 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

繼續(xù)調試會到宏CacheLookup代碼處,由于此時傳的是NORMAL,由于我們的代碼最終是跑在真機上的懦傍,所以我這里還是分析arm64的匯編吧雹舀,x86匯編留給你自己分析吧,讀懂arm64的匯編代碼粗俱,x86匯編不在話下區(qū)別就是使用的匯編寫法不同(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

如果你覺得一頭霧水的話我?guī)阋徊揭徊娇磪R編,首先初步看到這個匯編代碼會發(fā)現(xiàn)出現(xiàn)很多不知道的寄存器比如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怜姿,入?yún)⑹欠旁?code>x1 = SEL, x16 = isa中的慎冤,最先執(zhí)行的是ldp x10, x11, [x16, #16],這行命令的意思是從x16往下16個字節(jié)長度開始沧卢,連續(xù)讀取8 + 8個字節(jié)的長度分別賦值給x10x11蚁堤。

  1. 由于x16 == isa ,那么 x16 + 16 就是 _buckets的地址但狭,賦值給x10披诗;
  2. 由于 x10所占的字節(jié)長度是8,那么x11就是接下來的8個字節(jié)立磁,也就是讀取到了_mask_occupied呈队,所以x11 == _mask + _occupied,由于是小端存儲模式,_mask被存放在低16位唱歧,可以通過w11取到宪摧。

所以執(zhí)行完ldp x10, x11, [x16, #16]代碼粒竖,x10x11內容如下

15454735233773.jpg
2.2.2.2、查找緩存列表

2.2.2.1已經(jīng)找到了緩存_buckets的地址几于,下面就開始查找緩存了,_buckets的數(shù)據(jù)結構以及每個元素的地址可以通過下面的方式計算得出如下

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

既然已經(jīng)找到了存放緩存的地址蕊苗,接下來只要找到調用的方法在緩存中的索引值就可以了,我們需要找的方法是test1孩革,他的SEL4294971226

  1. and w12, w1, w11 初步計算出方法的索引值得运,4294971226 & 3 == 2 膝蜈;
  2. add x12, x10, x12, LSL #(1+3),步驟1計算出了&的結果熔掺,接下來就把指針移動到饱搏,序號為2的地方,左移4位正好是16個字節(jié)大小置逻,要想跳到哪個序號直接序號左移4位即可推沸,再加上初始的地址x10,就是序號的地址券坞。
  3. ldp x17, x9, [x12]鬓催,分別取出序號2_imp_key賦值給寄存器x17x9

知道每個元素的地址計算方式了恨锚,那么就是挨個判斷是否是我們要找的方法了宇驾,我們初始值是序號2,接下來進入判斷邏輯,流程圖如下猴伶。

untitled.png

由于我們是第一次調用是沒有緩存的课舍,所以即將進入方法__objc_msgSend_uncached,不幸的是這個方法還是匯編代碼他挎。

三筝尾、方法列表查找 & 動態(tài)方法解析

如果緩存中沒有我們要找的方法,那么就會進入__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

如果懂函數(shù)棧幀的話,其實這個段匯編代碼很簡單呢撞,頭和尾其實是常規(guī)操作贸街,前面是開辟棧空間狸相,保護寄存器的值薛匪,尾部恢復寄存器的值,以及恢復棧平衡脓鹃,關鍵代碼其實是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);
             // 如果父類緩存中有 !!娇跟!存入的是自己的緩存列表岩齿,并不是存到父類的緩存列表!0盹沈!
            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) {
             // 如果父類方法列表中有 3砸ァF蚍狻!存入的是自己的緩存列表岗憋,并不是存到父類的緩存列表K嗤怼!仔戈!
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 以上情況都沒有 就會進入動態(tài)方法解析

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

    // 以上還沒有找到 就會進入消息轉發(fā)階段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

注釋都寫在后面了关串,流程圖如下:

lookUpImpOrForward.png

四、消息轉發(fā)(_objc_msgForward_impcache)

這個方法是匯編實現(xiàn)的监徘,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的一個默認實現(xiàn)晋修,代碼在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來繼續(xù)下面的探索,選擇以下Forwarding_demo工程

15456305553025.jpg
15456308945541.jpg

4.1、查看調用堆棧-【X86架構】

運行以后會閃退凰盔,函數(shù)堆棧如下

(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]

【...】

從函數(shù)堆椃沈荆可以看出調用完performSelector:方法以后,代碼就進入了CoreFoundation框架了廊蜒,然后就到了__forwarding_prep_0___ -> ___forwarding___趴拧,CoreFoundation代碼是開源的可以去這里下載,我下載的是CF-1153.18 2,遺憾的是雖然CoreFoundation代碼是開源的,但是蘋果沒有給出以上方法的實現(xiàn)山叮。

4.1.1著榴、斷點查看方法實現(xiàn)

運行程序,分別增加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查看方法實現(xiàn)

除了直接添加斷點的方式锐借,還可以通過逆向CoreFoundation.framework來窺探實現(xiàn)问麸,這樣更直觀,還能知道函數(shù)調用關系钞翔,CoreFoundation.framework放在/System/Library/Frameworks/CoreFoundation.framework

15456324177613.jpg

找到了___forwarding___實現(xiàn)严卖,并且知道是誰調用的,分別是框出來的部分__forwarding_prep_0_____forwarding_prep_1___布轿,點擊進入__forwarding_prep_0___哮笆,同樣的方式最后定位到了___CFInitialize

15456325212259.jpg

通過匯編確實看出___CFInitialize調用了___forwarding___方法来颤,但是開源的代碼根本沒有這個相關的影子。

4.1.3稠肘、分析forwarding實現(xiàn)

我們已經(jīng)逆向出來了__forwarding__的匯編代碼福铅,但是可讀性還是太差了,網(wǎng)上有人已經(jīng)將匯編代碼轉成熟悉的樣子项阴,這個方法就是消息轉發(fā)的全部了滑黔。

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
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末薯演,一起剝皮案震驚了整個濱河市撞芍,隨后出現(xiàn)的幾起案子秧了,更是在濱河造成了極大的恐慌跨扮,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件验毡,死亡現(xiàn)場離奇詭異衡创,居然都是意外死亡,警方通過查閱死者的電腦和手機晶通,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門璃氢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狮辽,你說我怎么就攤上這事一也。” “怎么了喉脖?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵椰苟,是天一觀的道長。 經(jīng)常有香客問我树叽,道長舆蝴,這世上最難降的妖魔是什么获洲? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任蹦肴,我火速辦了婚禮恢准,結果婚禮上丢烘,老公的妹妹穿的比我還像新娘糠悯。我一直安慰自己列牺,他們只是感情好鄙漏,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布招驴。 她就那樣靜靜地躺著草冈,像睡著了一般祭椰。 火紅的嫁衣襯著肌膚如雪臭家。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天方淤,我揣著相機與錄音钉赁,去河邊找鬼。 笑死携茂,一個胖子當著我的面吹牛你踩,可吹牛的內容都是我干的。 我是一名探鬼主播讳苦,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼带膜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鸳谜?” 一聲冷哼從身側響起膝藕,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咐扭,沒想到半個月后芭挽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡蝗肪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年袜爪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薛闪。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡辛馆,死狀恐怖,靈堂內的尸體忽然破棺而出豁延,到底是詐尸還是另有隱情昙篙,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布诱咏,位于F島的核電站苔可,受9級特大地震影響,放射性物質發(fā)生泄漏胰苏。R本人自食惡果不足惜硕蛹,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硕并。 院中可真熱鬧法焰,春花似錦、人聲如沸倔毙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陕赃。三九已至卵蛉,卻和暖如春颁股,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背傻丝。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工甘有, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人葡缰。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓亏掀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親泛释。 傳聞我的和親對象是個殘疾皇子滤愕,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容