記boost協(xié)程切換bug發(fā)現(xiàn)和分析

在分析了各大開源協(xié)程庫實現(xiàn)后翰意,最終選擇參考boost.context的匯編實現(xiàn)约郁,來寫tbox的切換內(nèi)核。

在這過程中流昏,我對boost各個架構(gòu)平臺下的context切換扎即,都進(jìn)行了分析和測試吞获。

在macosx i386和mips平臺上實現(xiàn)協(xié)程切換時,發(fā)現(xiàn)boost那套匯編實現(xiàn)是有問題的铺遂,如果放到tbox切換demo上運行衫哥,會直接掛掉茎刚。

在分析這兩個架構(gòu)上襟锐,boost.context切換實現(xiàn)問題,這邊先貼下tbox上的context切換demo膛锭,方便之后的講解:

static tb_void_t func1(tb_context_from_t from)
{
    // check
    tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
    tb_assert_and_check_return(contexts);

    // 先保存下主函數(shù)入口context粮坞,方便之后切換回去
    contexts[0] = from.context;

    // 初始化切換到func2
    from.context = contexts[2];

    // loop
    tb_size_t count = 10;
    while (count--)
    {
        // trace
        tb_trace_i("func1: %lu", count);

        // 切換到func2,返回后更新from中的context地址
        from = tb_context_jump(from.context, contexts);
    }

    // 切換回主入口函數(shù)
    tb_context_jump(contexts[0], tb_null);
}
static tb_void_t func2(tb_context_from_t from)
{
    // check
    tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
    tb_assert_and_check_return(contexts);

    // loop
    tb_size_t count = 10;
    while (count--)
    {
        // trace
        tb_trace_i("func2: %lu", count);

        // 切換到func1初狰,返回后更新from中的context地址
        from = tb_context_jump(from.context, contexts);
    }

    // 切換回主入口函數(shù)
    tb_context_jump(contexts[0], tb_null);
}
static tb_void_t test()
{ 
    // 定義全局堆棧
    static tb_context_ref_t contexts[3];
    static tb_byte_t        stacks1[8192];
    static tb_byte_t        stacks2[8192];

    // 生成兩個context上下文莫杈,綁定對應(yīng)函數(shù)和堆棧
    contexts[1] = tb_context_make(stacks1, sizeof(stacks1), func1);
    contexts[2] = tb_context_make(stacks2, sizeof(stacks2), func2);

    // 切換到func1并傳遞私有參數(shù):context數(shù)組
    tb_context_jump(contexts[1], contexts);
}

這里為了測試context切換,直接使用的底層切換接口tb_context_maketb_context_jump奢入,所以代碼使用上筝闹,比較原始。

這兩個接口相當(dāng)于boost的make_fcontextjump_fcontext腥光,當(dāng)然實際應(yīng)用中关顷,tbox的協(xié)程庫提供了更上層的封裝,并不會直接使用這兩個接口武福。

這個demo很簡單议双,就是創(chuàng)建兩個context,來回切換捉片,最后結(jié)束返回到主函數(shù)平痰。

然后再直接嘗試使用boost的實現(xiàn)時,出現(xiàn)了兩個不同現(xiàn)象的crash

  1. macosx i386下伍纫,從func2切換回到func1時發(fā)生了崩潰
  2. mips32下宗雇,在執(zhí)行完10次來回切換后,切回主函數(shù)是莹规,發(fā)生了崩潰

macosx i386下的問題分析

我們先來分析下macosx i386的這個問題赔蒲,由于之前tbox已經(jīng)參考了boost的linux i386下的實現(xiàn),完成了上下文切換访惜,是能正常運行的嘹履。

因此,可以在這兩個平臺下做下對比债热,結(jié)果發(fā)現(xiàn)砾嫉,boost幾乎是直接照搬了linux下那套實現(xiàn),那么問題來了窒篱,為甚了linux下ok焕刮,macosx上就有問題呢舶沿。

大體可以猜到,應(yīng)該是調(diào)用棧布局的不同導(dǎo)致的問題配并,因此我們看下macosx上的boost jump實現(xiàn):

.text
.globl _jump_fcontext
.align 2
_jump_fcontext:
    pushl  %ebp  /* save EBP */
    pushl  %ebx  /* save EBX */
    pushl  %esi  /* save ESI */
    pushl  %edi  /* save EDI */

    /* store fcontext_t in ECX */
    movl  %esp, %ecx

    /* first arg of jump_fcontext() == context jumping to */
    movl  0x18(%esp), %eax

    /* second arg of jump_fcontext() == data to be transferred */
    movl  0x1c(%esp), %edx

    /* restore ESP (pointing to context-data) from EAX */
    movl  %eax, %esp

    /* address of returned transport_t */
    movl 0x14(%esp), %eax
    /* return parent fcontext_t */
    movl  %ecx, (%eax)
    /* return data */
    movl %edx, 0x4(%eax)

    popl  %edi  /* restore EDI */
    popl  %esi  /* restore ESI */
    popl  %ebx  /* restore EBX */
    popl  %ebp  /* restore EBP */

    /* jump to context */
    ret $4

jump_fcontext的參數(shù)原型是:struct(context, data) = jump_fcontext(context, data)括荡,跟tboxtb_context_jump差不多

都是傳入一個struct,相當(dāng)于傳入了兩個參數(shù)溉旋,一個context畸冲,一個data,返回結(jié)果也是一個類似struct

而從上面的代碼中可以看到观腊,從esp + 0x18處取了第一個參數(shù)context邑闲,esp + 0x1c取得是第二個參數(shù)data,換算到_jump_fcontext的入口處

可以確定出_jump_fcontext入口處大體的棧布局:

esp + 12: data參數(shù)
esp + 8:  context參數(shù)
esp + 4:  ??
esp    :  _jump_fcontext的返回地址

按照i386的調(diào)用棧布局梧油,函數(shù)入口處第一個參數(shù)苫耸,應(yīng)該是通過 esp + 4 訪問的,那為什么context參數(shù)卻是在esp + 8處呢儡陨,esp + 4指向的內(nèi)容又是什么褪子?

我們可以看下,_jump_fcontext調(diào)用處的匯編偽代碼:

pushl data
pushl context 
pushl hidden 
call _jump_fcontext
addl $12, %esp

其實編譯器在調(diào)用_jump_fcontext處骗村,實際壓入了三個參數(shù)嫌褪,這個esp + 4指向的hidden數(shù)據(jù),這個是_jump_fcontext返回的struct數(shù)據(jù)的椥鹕恚空間地址

用于在_jump_fcontext內(nèi)部渔扎,設(shè)置返回struct(context, data)的數(shù)據(jù),也就是:

/* address of returned transport_t */
movl 0x14(%esp), %eax
/* return parent fcontext_t */
movl %ecx, (%eax)
/* return data */
movl %edx, 0x4(%eax)

說白了信轿,linux i386上返回struct數(shù)據(jù)晃痴,是通過傳入一個指向棧空間的變量指針财忽,作為隱藏的第一個參數(shù)倘核,用于設(shè)置struct數(shù)據(jù)返回。

而boost在macosx i386上即彪,也直接照搬了這種布局來實現(xiàn)紧唱,那macosx上是否真的也是這么做的呢?

我們來寫個測試程序驗證下:

static tb_context_from_t test()
{
    tb_context_from_t from = {0};
    return from;
}

反匯編后的結(jié)果如下:

__text:00051BD0 _test           proc near               
__text:00051BD0
__text:00051BD0 var_10          = dword ptr -10h
__text:00051BD0 var_C           = dword ptr -0Ch
__text:00051BD0 var_8           = dword ptr -8
__text:00051BD0 var_4           = dword ptr -4
__text:00051BD0
__text:00051BD0                 push    ebp
__text:00051BD1                 mov     ebp, esp
__text:00051BD3                 sub     esp, 10h
__text:00051BD6                 mov     [ebp+var_C], 0
__text:00051BDD                 mov     [ebp+var_10], 0
__text:00051BE4                 mov     [ebp+var_4], 0
__text:00051BEB                 mov     [ebp+var_8], 0
__text:00051BF2                 mov     eax, [ebp+var_8]
__text:00051BF5                 mov     edx, [ebp+var_4]
__text:00051BF8                 add     esp, 10h
__text:00051BFB                 pop     ebp
__text:00051BFC                 retn
__text:00051BFC _test           endp

可以看到隶校,實際上并沒有像linux上那樣通過一個struct指針來返回漏益,而是直接將struct(context, data),通過 eax, edx 進(jìn)行返回深胳。

到這里绰疤,我們大概可以猜到,macosx上舞终,對這種小的struct結(jié)構(gòu)體返回做了優(yōu)化轻庆,直接放置在了eax癣猾,edx中,而我們的from結(jié)構(gòu)體只有兩個pointer余爆,正好滿足這種方式纷宇。

因此,為了修復(fù)macosx上的問題蛾方,tbox在實現(xiàn)上像捶,對棧布局做了調(diào)整,并且做了些額外的優(yōu)化:

1. 調(diào)整jump實現(xiàn)转捕,改用eax作岖,edx直接返回from結(jié)構(gòu)體
2. 由于不再像linux那樣通過保留一個額外的椝衾空間返回struct五芝,可以把linux那種跳板實現(xiàn)去掉,改為直接jump到實際位置(提升切換效率)

mips32下的問題分析

mips下這個問題辕万,我之前也是調(diào)試了很久枢步,在每次切換完成后,打算切換回主函數(shù)時渐尿,就會發(fā)生crash醉途,也就是下面這個位置:

static tb_void_t func1(tb_context_from_t from)
{
    // check
    tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv;
    tb_assert_and_check_return(contexts);

    // 先保存下主函數(shù)入口context,方便之后切換回去
    contexts[0] = from.context;

    // 初始化切換到func2
    from.context = contexts[2];

    // loop
    tb_size_t count = 10;
    while (count--)
    {
        // trace
        tb_trace_i("func1: %lu", count);

        // 切換到func2砖茸,返回后更新from中的context地址
        from = tb_context_jump(from.context, contexts);
    }

    // 切換回主入口函數(shù)
    tb_context_jump(contexts[0], tb_null);   <-----  此處發(fā)生崩潰
}

我們先來初步分析下隘擎,既然之前的來回切換都是ok的,只有在最后這個切換發(fā)生問題凉夯,那么可以確定jump的大體實現(xiàn)應(yīng)該還是ok的

可能是傳入jump的參數(shù)不對導(dǎo)致的問題货葬,最有可能的是 contexts[0] 指向的主函數(shù)上下文地址已經(jīng)不對了。

通過printf確認(rèn)劲够,確實值不對了震桶,那么在func1入口處這個contexts[0],是否正確呢征绎,我又繼續(xù)printf了下蹲姐,居然還是不對。 = =

然后人柿,我又繼續(xù)打印contexts[0], contexts[1], contexts[2]這三個在func1入口處的值柴墩,發(fā)現(xiàn)只有contexts[2]是對的

前兩處都不對了,而且值得注意的是凫岖,這兩個的值江咳,正好是from.context和from.data的值。

由此隘截,可以得出一個初步結(jié)論:

1. contexts這塊buffer的前兩處數(shù)據(jù)扎阶,在jump切換到func1的時候被自動改寫了
2. 而且改寫后的數(shù)據(jù)值汹胃,正好是from里面的context和data

說白了,也就是發(fā)生越界了东臀。着饥。

那什么情況下, contexts指向的數(shù)據(jù)會發(fā)生越界呢,可以先看下contexts的定義:

static tb_void_t test()
{ 
    // 定義全局堆棧
    static tb_context_ref_t contexts[3];
    static tb_byte_t        stacks1[8192];
    static tb_byte_t        stacks2[8192];

    // 生成兩個context上下文惰赋,綁定對應(yīng)函數(shù)和堆棧
    contexts[1] = tb_context_make(stacks1, sizeof(stacks1), func1);
    contexts[2] = tb_context_make(stacks2, sizeof(stacks2), func2);

    // 切換到func1并傳遞私有參數(shù):context數(shù)組
    tb_context_jump(contexts[1], contexts);
}

contexts[3]的數(shù)據(jù)定義宰掉,正好在stacks1的上面,而stacks1是作為func1的堆棧傳入的赁濒,也就是說轨奄,如果func1的堆棧發(fā)生上溢,就會擦掉contexts里面的數(shù)據(jù)拒炎。

我們接著來看下挪拟,boost的實現(xiàn),看看是否有地方會發(fā)生這種情況:

.text
.globl make_fcontext
.align 2
.type make_fcontext,@function
.ent make_fcontext
make_fcontext:
#ifdef __PIC__
.set    noreorder
.cpload $t9
.set    reorder
#endif
    # first arg of make_fcontext() == top address of context-stack
    move $v0, $a0

    # shift address in A0 to lower 16 byte boundary
    move $v1, $v0
    li $v0, -16 # 0xfffffffffffffff0
    and $v0, $v1, $v0

    # reserve space for context-data on context-stack
    # including 48 byte of shadow space (sp % 16 == 0)
    addiu $v0, $v0, -112

    # third arg of make_fcontext() == address of context-function
    sw  $a2, 44($v0)
    # save global pointer in context-data
    sw  $gp, 48($v0)

    # compute address of returned transfer_t
    addiu $t0, $v0, 52
    sw  $t0, 36($v0)

    # compute abs address of label finish
    la  $t9, finish
    # save address of finish as return-address for context-function
    # will be entered after context-function returns
    sw  $t9, 40($v0)

    jr  $ra # return pointer to context-data

finish:
    lw $gp, 0($sp)
    # allocate stack space (contains shadow space for subroutines)
    addiu  $sp, $sp, -32
    # save return address
    sw  $ra, 28($sp)

    # restore GP (global pointer)
#    move  $gp, $s1
    # exit code is zero
    move  $a0, $zero
    # address of exit
    lw  $t9, %call16(_exit)($gp)
    # exit application
    jalr  $t9
.end make_fcontext
.size make_fcontext, .-make_fcontext

.text
.globl jump_fcontext
.align 2
.type jump_fcontext,@function
.ent jump_fcontext
jump_fcontext:
    # reserve space on stack
    addiu $sp, $sp, -112

    sw  $s0, ($sp)  # save S0
    sw  $s1, 4($sp)  # save S1
    sw  $s2, 8($sp)  # save S2
    sw  $s3, 12($sp)  # save S3
    sw  $s4, 16($sp)  # save S4
    sw  $s5, 20($sp)  # save S5
    sw  $s6, 24($sp)  # save S6
    sw  $s7, 28($sp)  # save S7
    sw  $fp, 32($sp)  # save FP
    sw  $a0, 36($sp)  # save hidden, address of returned transfer_t
    sw  $ra, 40($sp)  # save RA
    sw  $ra, 44($sp)  # save RA as PC

    # store SP (pointing to context-data) in A0
    move  $a0, $sp

    # restore SP (pointing to context-data) from A1
    move  $sp, $a1

    lw  $s0, ($sp)  # restore S0
    lw  $s1, 4($sp)  # restore S1
    lw  $s2, 8($sp)  # restore S2
    lw  $s3, 12($sp)  # restore S3
    lw  $s4, 16($sp)  # restore S4
    lw  $s5, 20($sp)  # restore S5
    lw  $s6, 24($sp)  # restore S6
    lw  $s7, 28($sp)  # restore S7
    lw  $fp, 32($sp)  # restore FP
    lw  $t0, 36($sp)  # restore hidden, address of returned transfer_t
    lw  $ra, 40($sp)  # restore RA

    # load PC
    lw  $t9, 44($sp)

    # adjust stack
    addiu $sp, $sp, 112
    
    # return transfer_t from jump
    sw  $a0, ($t0)  # fctx of transfer_t
    sw  $a1, 4($t0) # data of transfer_t
    # pass transfer_t as first arg in context function
    # A0 == fctx, A1 == data
    move  $a1, $a2 

    # jump to context
    jr  $t9
.end jump_fcontext
.size jump_fcontext, .-jump_fcontext

可以看到击你,boost在make_fcontext的時候玉组,先對傳入的棧頂做了16字節(jié)的對齊,然后保留了112字節(jié)的空間丁侄,用于保存寄存器數(shù)據(jù)惯雳。

然后再jump切換到新context的時候,恢復(fù)了新context所需的寄存器鸿摇,并把新的sp指針+112石景,把保留的棧空間給pop了拙吉。

也就是說潮孽,在第一次切換到實際func1函數(shù)入口時,這個時候的棧指針指向棧頂?shù)穆洌偻隙魃蹋呀?jīng)沒有多少空間了(也就只有為了16字節(jié)對齊,有可能保留的少部分空間)必逆。

換一句話說怠堪,如果傳入的stack1的棧頂本身就是16字節(jié)對齊的,那么func1的入口處sp指向的就是stack1的棧頂

如果在func1的入口處名眉,有超過stack1棧頂范圍的寫操作粟矿,就有可能會擦掉contexts的數(shù)據(jù),因為contexts緊靠著stack1的棧頂位置损拢。

那是否會出現(xiàn)這種情況陌粹,我們通過反匯編func1的入口處代碼,實際看下:

.text:00453F04 func1:     
.text:00453F04
.text:00453F04 var_30          = -0x30
.text:00453F04 var_2C          = -0x2C
.text:00453F04 var_28          = -0x28
.text:00453F04 var_20          = -0x20
.text:00453F04 var_18          = -0x18
.text:00453F04 var_14          = -0x14
.text:00453F04 var_10          = -0x10
.text:00453F04 var_8           = -8
.text:00453F04 var_4           = -4
.text:00453F04 arg_0           =  0
.text:00453F04 arg_4           =  4
.text:00453F04
.text:00453F04                 addiu   $sp, -0x40
.text:00453F08                 sw      $ra, 0x40+var_4($sp)
.text:00453F0C                 sw      $fp, 0x40+var_8($sp)
.text:00453F10                 move    $fp, $sp
.text:00453F14                 la      $gp, unk_5706A0
.text:00453F1C                 sw      $gp, 0x40+var_20($sp)
.text:00453F20                 sw      $a0, 0x40+arg_0($fp)    <------------ 此處發(fā)生越界福压,改寫了contexts[0] = from.context
.text:00453F24                 sw      $a1, 0x40+arg_4($fp)    <------------ 此處發(fā)生越界掏秩,改寫了contexts[1] = from.data
.text:00453F28                 lw      $v0, 0x40+arg_4($fp)
.text:00453F2C                 sw      $v0, 0x40+var_14($fp)
.text:00453F30                 lw      $v0, 0x40+var_14($fp)
.text:00453F34                 sltu    $v0, $zero, $v0
.text:00453F38                 andi    $v0, 0xFF
.text:00453F3C                 move    $v1, $v0

可以看到或舞,確實發(fā)生了越界行為,那為什么在函數(shù)內(nèi)部蒙幻,還會去寫當(dāng)前棧幀外的數(shù)據(jù)呢映凳,這個要從mips的調(diào)用棧布局上說起了。

簡單來說邮破,mips在調(diào)用某個函數(shù)時诈豌,會把a(bǔ)0-a3作為參數(shù)寄存器,其他參數(shù)放置在堆棧中抒和,但是與其他架構(gòu)有點不同的是:

mips還會去為a0-a3這前四個參數(shù)矫渔,保留棧空間

調(diào)用棧如下:

 ------------
| other args |
|------------|
|   a0-a3    | <- 參數(shù)傳遞使用a0-a3摧莽,但是還是會為這四個參數(shù)保留椕硗荩空間出來
|------------|
|     ra     | <- 返回地址
|------------|
| fp gp s0-7 | <- 保存的一些其他寄存器
|------------|
|   locals   |
 ------------

而剛剛在func1內(nèi),就是回寫了a0-a3處保留的椃吨觯空間送膳,導(dǎo)致了越界,因為boost的實現(xiàn)在jump后丑蛤,棧空間已經(jīng)到棧頂了撕阎,空間不夠了受裹。脯厨。

因此智哀,為了修復(fù)這個問題镶摘,只需要在make_fcontext里面遇西,多保留a0-a3這32字節(jié)的空間就行了洛退,也就是:

.globl make_fcontext

    # reserve space for context-data on context-stack
    # including 48 byte of shadow space (sp % 16 == 0)
#    addiu $v0, $v0, -112
    addiu $v0, $v0, -146

而在tbox內(nèi)干奢,除了對此處的額外的椊染椋空間保留彼水,來修復(fù)此問題汗侵,還對棧數(shù)據(jù)進(jìn)行了更加合理的分配利用幸缕,不再需要保留146這么多字節(jié)數(shù)

只需要保留96字節(jié),就夠用了晰韵,節(jié)省了50個字節(jié)发乔,如果同時存在1024個協(xié)程的話,相當(dāng)于節(jié)省了50K的內(nèi)存數(shù)據(jù)雪猪。

并且boost的jump實現(xiàn)上栏尚,還有其他兩處問題,tbox里面一并修復(fù)了:

jump_fcontext:
    # reserve space on stack
    addiu $sp, $sp, -112

    sw  $s0, ($sp)  # save S0
    sw  $s1, 4($sp)  # save S1
    sw  $s2, 8($sp)  # save S2
    sw  $s3, 12($sp)  # save S3
    sw  $s4, 16($sp)  # save S4
    sw  $s5, 20($sp)  # save S5
    sw  $s6, 24($sp)  # save S6
    sw  $s7, 28($sp)  # save S7
    sw  $fp, 32($sp)  # save FP
    sw  $a0, 36($sp)  # save hidden, address of returned transfer_t
    sw  $ra, 40($sp)  # save RA
    sw  $ra, 44($sp)  # save RA as PC
                      <-------------------- 此處boost雖然為gp保留了48($sp)空間只恨,但是確沒去保存gp寄存器

    # store SP (pointing to context-data) in A0
    move  $a0, $sp

    # restore SP (pointing to context-data) from A1
    move  $sp, $a1

    lw  $s0, ($sp)  # restore S0
    lw  $s1, 4($sp)  # restore S1
    lw  $s2, 8($sp)  # restore S2
    lw  $s3, 12($sp)  # restore S3
    lw  $s4, 16($sp)  # restore S4
    lw  $s5, 20($sp)  # restore S5
    lw  $s6, 24($sp)  # restore S6
    lw  $s7, 28($sp)  # restore S7
    lw  $fp, 32($sp)  # restore FP
    lw  $t0, 36($sp)  # restore hidden, address of returned transfer_t
    lw  $ra, 40($sp)  # restore RA
                      <-------------------- 此處boost也沒去恢復(fù)gp寄存器

    # load PC
    lw  $t9, 44($sp)

    # adjust stack
    addiu $sp, $sp, 112
    
    # return transfer_t from jump
    sw  $a0, ($t0)  # fctx of transfer_t
    sw  $a1, 4($t0) # data of transfer_t  <------------- 此處應(yīng)該使用 a2 而不是 a1 
    # pass transfer_t as first arg in context function
    # A0 == fctx, A1 == data
    move  $a1, $a2 

    # jump to context
    jr  $t9
.end jump_fcontext

最后說一下译仗,本文是針對boost 1.62.0 版本做的分析抬虽,如有不對之處,歡迎指正哈纵菌。斥赋。


個人主頁:TBOOX開源工程
原文出處:http://tboox.org/cn/2016/11/13/boost-context-bug/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市产艾,隨后出現(xiàn)的幾起案子疤剑,更是在濱河造成了極大的恐慌,老刑警劉巖闷堡,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隘膘,死亡現(xiàn)場離奇詭異,居然都是意外死亡杠览,警方通過查閱死者的電腦和手機(jī)弯菊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踱阿,“玉大人管钳,你說我怎么就攤上這事∪砩啵” “怎么了才漆?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長佛点。 經(jīng)常有香客問我醇滥,道長,這世上最難降的妖魔是什么超营? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任鸳玩,我火速辦了婚禮,結(jié)果婚禮上演闭,老公的妹妹穿的比我還像新娘不跟。我一直安慰自己,他們只是感情好米碰,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布窝革。 她就那樣靜靜地躺著,像睡著了一般见间。 火紅的嫁衣襯著肌膚如雪聊闯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天米诉,我揣著相機(jī)與錄音菱蔬,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛拴泌,可吹牛的內(nèi)容都是我干的魏身。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚪腐,長吁一口氣:“原來是場噩夢啊……” “哼箭昵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起回季,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤家制,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后泡一,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颤殴,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年鼻忠,在試婚紗的時候發(fā)現(xiàn)自己被綠了涵但。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡帖蔓,死狀恐怖矮瘟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塑娇,我是刑警寧澤澈侠,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站钝吮,受9級特大地震影響埋涧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奇瘦,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劲弦。 院中可真熱鬧耳标,春花似錦、人聲如沸邑跪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽画畅。三九已至砸琅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轴踱,已是汗流浹背症脂。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诱篷。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓壶唤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棕所。 傳聞我的和親對象是個殘疾皇子闸盔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容