golang中的panic,recover执行过程?

上篇文章golang中defer的执行过程是怎样的?介绍了一下defer的执行过程,本篇是上一篇的引申,主要介绍panic、recover的底层分析,如果没有读过上一篇文章,可以先去读一下在看这篇。
总共分3部分讲解:

1 panic

2 defer panic

3 defer panic recover

=======================

环境:go version go1.12.5 linux/amd64

1 panic

golang中的异常总共分为4中:

  • 编译器捕获的
  • 直接手动panic
  • golang捕获的
  • 系统捕获的
编译器捕获的

1/0 我们知道被除数是不能等于0的,所以这种错误是编译不过去的,会提示:
./main.go:7:8: division by zero

直接手动panic

示例代码:

package main
func main() {
    panic("panic error!!")
}

编译成汇编代码看panic函数会指向底层哪个函数:
go tool compile -S main.go > main.s

0x0034 00052 (main.go:4)    CALL    runtime.gopanic(SB)

查看gopanic(SB)实现,先粗略看一下代码的含义一些解释在代码中已经注解:

func gopanic(e interface{}) {
    gp := getg() //获取当前的g

        ....省略不重要的

    var p _panic //_panic原型
    p.arg = e //将panic参数存入arg参数
    p.link = gp._panic  //将p.link绑定到当前的g的_panic上。
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) //将p绑定到g的链表头。
      
    atomic.Xadd(&runningPanicDefers, 1)

    for {
        d := gp._defer
        if d == nil {
            break
        }

        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
            }
            d._panic = nil
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
            continue
        }

        d.started = true

    
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))//将p绑定到g的链表头。

        p.argp = unsafe.Pointer(getargp(0))
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) //调用g上的defer(源程序中如果没有defer函数,编译器会生成一个并绑定到g._defer上)
        p.argp = nil

        if gp._defer != d {
            throw("bad defer entry in panic")
        }
              //脱链
        d._panic = nil
        d.fn = nil
        gp._defer = d.link 

        pc := d.pc
        sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
        freedefer(d)
        if p.recovered { //先忽略讲到recover时候在说
            .....
        }
    }

    preprintpanics(gp._panic)
        //循环打印panic
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

我们发现panic的原型是_panic,去看一下定义:

type _panic struct {
    argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
    arg       interface{}    // argument to panic
    link      *_panic        // link to earlier panic
    recovered bool           // whether this panic is over
    aborted   bool           // the panic was aborted
}

发现是个结构体类型,里面的类型我们在调试代码的时候在去探究具体的含义。
接下来我们就用gdb跟踪一下上面的源码示例。

go build -o main
gdb main

进入gdb界面并断点到panic函数行见下图:


图1

按s进入到gopanic(interface)中。
发现这条语句:

gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

原来当前的gp定义(由于不是讲goroutine 这里就不贴gp的原型了)中有_panc字段作为链表头,而_panic结构体中有link字段。不难看出和defer同理:从goroutine._panic作为头,然后用_painc.link作为链接组成了一个链表的数据结构。之所以是链表是因为recover到panic时候,recover中也有可能有panic,例如见下方代码:

if err := recover(); err != nil {
  panic("go on panic xitehip")
}

deferd函数也会继续有panic。下方讲到recover时候详细讲解。
执行上面的语句此时的链表示意结构见下方:
gp._panic => p.link => gp._panic(之前的链表头)
继续往下走:

图2

运行到reflectcall()函数,发现这个函数总共有5个参数:

func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)

从第二个参数可知这个是函数指针,猜测这个reflectcall是调用我们实参unsafe.Point(d.fn)的。根据源码中的定义 d := gp._defer可知变量d就是上文我们说的g._defer。那马上有疑问了,这个例子里根本没有用到defer关键字,就不会调用deferproc(SB)生成defer。那只有一种可能就是编译器帮我们做了生成了一个defer函数然后绑定到了g._defer的链表头上。
继续看reflectcall函数见下图x:

图x

用disass命令查看一下汇编代码,绿线处的是即将调用的reflectcall函数。红线处是它的下一条指令,记住它的地址0x0000000000423025,我们去看一下reflectcall函数执行完的返回值是如何指向到红线处的指令的。
见下方汇编代码:

//runtime/asm_amd64.s

TEXT ·reflectcall(SB), NOSPLIT, $0-32
    MOVLQZX argsize+24(FP), CX
    DISPATCH(runtime·call32, 32)
    DISPATCH(runtime·call64, 64)
        .....
    MOVQ    $runtime·badreflectcall(SB), AX
    JMP AX
//runtime/asm_amd64.s
#define DISPATCH(NAME,MAXSIZE)      \
    CMPQ    CX, $MAXSIZE;       \
    JA  3(PC);          \
    MOVQ    $NAME(SB), AX;      \
    JMP AX
//runtime/asm_amd64.s
#define CALLFN(NAME,MAXSIZE)            \
TEXT NAME(SB), WRAPPER, $MAXSIZE-32;        \
    NO_LOCAL_POINTERS;          \
    /* copy arguments to stack */       \
    MOVQ    argptr+16(FP), SI;      \
    MOVLQZX argsize+24(FP), CX;     \
    MOVQ    SP, DI;             \
    REP;MOVSB;              \
    /* call function */         \
    MOVQ    f+8(FP), DX;            \
    PCDATA  $PCDATA_StackMapIndex, $0;  \
    CALL    (DX);               \
    /* copy return values back */       \
    MOVQ    argtype+0(FP), DX;      \
    MOVQ    argptr+16(FP), DI;      \
    MOVLQZX argsize+24(FP), CX;     \
    MOVLQZX retoffset+28(FP), BX;       \
    MOVQ    SP, SI;             \
    ADDQ    BX, DI;             \
    ADDQ    BX, SI;             \
    SUBQ    BX, CX;             \
    CALL    callRet<>(SB);          \
    RET

是不是很乱,这些是啥??看不懂。用gdb跟踪一下到:
运行到下图:


图片.png

disass一下看一下CALLFN(. call32, 32)所指向的指令:


图片.png

绿框处所对应的的就是源文件中的代码:
TEXT callRet<>(SB), NOSPLIT, $32-0

那红框ret处就是reflectcall的返回。打到断点到ret处。
执行到这里见下图:

图片.png

ret的作用是pop 栈顶到rip,我们看一下rsp中的内容是啥?
图片.png

0x423025 所指向的内容:
图y
图y和上面的图x的地址一样的,就是reflectcall指令的下条指令。再看一下源文件下行代码是啥?p.argp = nil
翻译成汇编代码就是图y中的 mov QWROD PTR [rsp+0x58],0x0,就是变量赋值会把值存入栈中而不是寄存器中。
图片.png

执行完d.fn,将d脱链:

d._panic = nil
d.fn = nil
gp._defer = d.link

运行到:

func fatalpanic(msgs *_panic)

进行打印输出,看一下实现:

func fatalpanic(msgs *_panic) {
    pc := getcallerpc()
    sp := getcallersp()
    gp := getg()
    var docrash bool

    systemstack(func() {
        if startpanic_m() && msgs != nil {
            atomic.Xadd(&runningPanicDefers, -1)
                        
            printpanics(msgs)
        }

        docrash = dopanic_m(gp, pc, sp)
    })

    if docrash {
        crash()
    }

    systemstack(func() {
        exit(2)
    })

    *(*int)(nil) = 0 // not reached
}

重点看如下函数:

 printpanics(msgs)

实现:

func printpanics(p *_panic) {
    if p.link != nil {
        printpanics(p.link)
        print("\t")
    }
    print("panic: ")
    printany(p.arg)
    if p.recovered {
        print(" [recovered]")
    }
    print("\n")
}

发现这个是个递归调用,从g._panic链表头开始直到链表结束然后打印出panic信息。

golang捕获的

例如slice越界,见下方代码:

package main
import "fmt"
func main() {
    arr := []int{1, 2}
    arr[2] = 3
    fmt.Println(arr)
}

会panic:
panic: runtime error: index out of range
编译成汇编代码:go tool compile -S main.go > main.s

0x003c 00060 (main.go:7)    CALL    runtime.panicindex(SB)

可知调用了panicindex(SB)
去看一下它的实现:

func panicindex() {
    if hasPrefix(funcname(findfunc(getcallerpc())), "runtime.") {
        throw(string(indexError.(errorString)))
    }
    panicCheckMalloc(indexError)
    panic(indexError)
}

发现最终还是会调用panic(interface{})这个函数,然后就是上面所说的手动panic的执行流程,在这里不在重复赘述。

系统捕获的

比如对只读内存区赋值操作会引起panic

package main

import "fmt"

func main() {
    var pi *int
    *pi = 100
    fmt.Printf("%v", *pi)
}

会报如下错误:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x488a53]
goroutine 1 [running]:
main.main()
/server/gotest/src/hello/defer/main.go:7 +0x3a

编译成汇编代码没有发现gopanic入口。因为最终输出panic栈的信息,所以肯定调用了gopanic,给gopanic()打上断点直接运行到这里见下图:

图片.png

确实拦截到了gopanic,看一下它的调用链:
main.main => runtime.sigpanic() => runtime.panicmem() => gopanic()。
那为什么汇编中没有sigpanic()入口还能调用这个函数呢?
看一下*pi = 100生成的汇编代码:
图片.png

划红线处:test BYTE PTR [ax], al 由于ax=0x0所以BYTE PTR [ax]是获取不到0x0的内存的。这样cpu执行这条语句的时候会进入内核态保存0x488b1a到寄存器,内核态发送消息给go进程,go处理函数将0x488b1a所指向的内容换成go启动时事先注册号的函数作为指令入口,回到内核态执行0x488b1a -> 注册函数的指令。具体的调用链在这里就不深究了重点还是panic,recover。

2 defer panic

2.1示例:
package main
import "fmt"
func main() {
    defer fmt.Println("d1")
    defer fmt.Println("d2")
    panic("panic error")
}

输出:
d2
d1
panic error
如下核心代码:

//runtime/panic.go
func gopanic(e interface{}) {
    for {
        ...//获取goroutine表头deferd          
           //执行表头的deferd
           reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        ...//将表头的deferd拖链,将下一个deferd绑定到表头
     }
     ...
     fatalpanic(gp._panic) // 运行递归调用gp._panic链表上的panic
     ...
}

从上面代码可知,gopanic先遍历deferd链在遍历panic链,所以panic error最后输出。

2.2示例:
图片.png

输出:
d2
d1
panic: panic error
panic: panic error2
根据示例2.1 函数gopanic()可知函数的调用链见下面调用关系:

第14行panic -> gopanic() -> reflectcall -> 第12行defer -> reflectcall -> 第8行defer -> 第9行panic -> gopanic -> reflectcall -> 继续执行deferd链上的也就是第6行defer -> fatalpanic(里面子函数printpanics()递归调用g._panic链)。

3 defer panic recover

下面介绍的是recover的执行过程,先看下方示例代码:

package main

import "fmt"

func main() {
    re()
    fmt.Println("After recovery!")
}
func re() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("err:", err)
        }
    }()
    panic("panic error1")
}

输出:
err: panic error1
After recovery!

recover()的作用是捕获异常之后让程序正常往下执行而不会退出。这个例子里re()函数里有了异常,并且被捕获然后执行了re()下面的代码输出'After recovery'。

那为什么执行完recover()之后会跳转到输出行执行呢?

从汇编角度考虑:执行完re()之后要想保证继续往下执行,首先要把下一行的入口地址存起来,然后recover()之后再去取回来,放到rip指令寄存器中这样才可以向下执行。

在re()里除了deferd函数还有有panic()这行,那很明显它的内部实现里会有相关实现,继续分析recover的实现和panic内部的相关实现。

汇编查看recover():go tool compile -S main.go
发现gorecover(SB),猜测是recover()的实现:

    0x002a 00042 (main.go:13)   CALL    runtime.gorecover(SB)

在recover()行打断点,发现确实执行了gorecover(SB)函数,实现如下:

func gorecover(argp uintptr) interface{} {
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

从以上代码可知gorecover(uintptr)只是把当前goroutine的_panic.recovered 设置为true,然后返回之前panic函数设置的参数(err)给调用方。其实就是将当前的g._panic设置个标致,告诉以后的程序说我已经被捕获到了。

这个有recover()的deferd函数执行完之后会返回到上面提到的gopanic(interface{})函数中的reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))下一行继续往下执行。
见下方代码:

func gopanic(e interfac{}) {
.......
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
//往下看:
        p.argp = nil


        if gp._defer != d {
            throw("bad defer entry in panic")
        }
        //执行完defered函数之后脱链
        d._panic = nil
        d.fn = nil
        gp._defer = d.link

        pc := d.pc //deferproc()函数中存入的放回值地址
        sp := unsafe.Pointer(d.sp) //
        freedefer(d)
        if p.recovered {//执行了gorecover()函数之后p.recovered == true
            atomic.Xadd(&runningPanicDefers, -1)

            gp._panic = p.link

            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil { // must be done with signal
                gp.sig = 0
            }

            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc //pc恢复栈作用。
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
......
}

看一下这行代码:

pc := d.pc 

pc是什么呢?它是上篇文章中提到的deferproc()函数中存入的,见下方代码:

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
        ...
    callerpc := getcallerpc()
    d := newdefer(siz)
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }
    d.fn = fn
    d.pc = callerpc
       ....

我们在下方截图的第12行打一断点来看一下pc中到底是啥。看一下绿框中的指令:

图片.png

defer关键字会翻译成call runtime.deferproc那它下方绿框中的是runtime.deferproc后面的指令是编译器生成的(也可以这么理解,defer关键字会让编译器生成deferproc函数指令及后面一堆指令)第一行:test eax, eax的地址是0x4872d5稍后会再次说到这个指令及地址。

继续断点执行到d.pc = callerpc之后,我们看一下d.pc到底是什么值,见下图:

图片.png

0x4872d5这不是刚刚说的上图绿框处 test eax, eax的指令地址吗。带着疑问继续往下看。

从上面gorecover(uintptr)函数代码可知 p.recoverd == true 所以gopanic()中会执行到if p.recovered {里,我们着重看两行代码:

 gp.sigcode1 = pc

将pc就是deferproc()函数的返回值赋值给gp.sigcode1,为返回到正常流程做准备。

mcall(recovery)

其中的mcall先不看,先看recovery函数作用,见下方实现:

func recovery(gp *g) {
    // Info about defer passed in G struct.
    sp := gp.sigcode0
    pc := gp.sigcode1

    // d's arguments need to be in the stack.
    if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
        print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
        throw("bad recovery")
    }

    // Make the deferproc for this d return again,
    // this time returning 1.  The calling function will
    // jump to the standard return epilogue.
    gp.sched.sp = sp
    gp.sched.pc = pc
    gp.sched.lr = 0
    gp.sched.ret = 1
    gogo(&gp.sched)
}

recovery(*g) 主要是gp.sched赋值。其中pc是当前deferproc函数的返回地址。我们再看一下gogo(&gp.sched)函数实现,因为gogo函数是用汇编实现的所以用gdb跟踪是最方便的见下方代码:

TEXT runtime·gogo(SB), NOSPLIT, $16-8
    MOVQ    buf+0(FP), BX       // gobuf
    MOVQ    gobuf_g(BX), DX
    MOVQ    0(DX), CX       // make sure g != nil
    get_tls(CX)
    MOVQ    DX, g(CX)
    MOVQ    gobuf_sp(BX), SP    // restore SP
    MOVQ    gobuf_ret(BX), AX
    MOVQ    gobuf_ctxt(BX), DX
    MOVQ    gobuf_bp(BX), BP
    MOVQ    $0, gobuf_sp(BX)    // clear to help garbage collector
    MOVQ    $0, gobuf_ret(BX)
    MOVQ    $0, gobuf_ctxt(BX)
    MOVQ    $0, gobuf_bp(BX)
    MOVQ    gobuf_pc(BX), BX
    JMP BX

着重看2行代码:

MOVQ    gobuf_ret(BX), AX

AX从某个值变成了1,这个指令的偏移数量是gobuf_ret,其中的ret不就是返回的意思吗,见下图。


图片.png

再看最后一条指令:

JMP BX

看一下BX到底是啥:

图片.png

绿框处就是BX的值,也就是要jmp到这个地址处执行,这个地址眼熟吗,不就是刚提到的0x4872d5吗,对应的指令是test eax,eax。再重看一下这个图:
图片.png
其中绿框第一行就是要跳转的地址。刚才说了AX已经变成了1。那下方的两行指令

test eax, eax 
jne 0x4872f9

的意思是如果eax不等于0就跳转到这个地址否则就去执行绿框处第三行的正常流程。因为eax已经不等0了,所以就会跳转到0x4872f9这个地址处,跟踪一下这个地址指向的是哪里,见下图:

图片.png

原来它调用了runtime.deferreturn()函数,见下图。
图片.png

执行到这里。

sp := getcallersp()
sp是调用者的sp。就是即将调用defer func() {时的sp。
d.sp 是调用链上第二个defer,因为第一个deferd已经脱链。
显然这两个不相等,所以return了,具体return底层到底是如何将re()的返回地址返回的就不在跟踪了。然后执行到了下放的入口地址处:

fmt.Println("After recovery!")

整个流程,参看下图代码然后解释:


图片.png

call re() => 将re()返回值压栈到栈顶 => 执行12行defer函数 => 执行deferproc():将deferproc返回值存入pc,调用者(re())栈顶存入到sp,将defered函数加入到链表头,返回0(return0函数作用是将ax设为0) => 返回到下方代码test eax eax处 => 由于ax=0继续运行到17行的panic() =>gopanic() => 调用reflectcall():执行deferd函数 => 执行recovery():将recoverd标志位设为1 => mcall(recovery) => gogo():ax设为1,跳转到pc处 => 再一次跳转到test eax, eax :由于ax=1 => 跳转到deferreturn()函数:callersp !=d.sp,这里的d.sp中的d其实已经是是g上面默认带的_defer了,所以不等 => return 获取re()的返回地址pop到rip处 => cpu执行其返回值 => 输出'After recovery'

...
//defer函数 =>deferproc
0x00000000004872d0 <+48>:   call   0x426c00 <runtime.deferproc>
0x00000000004872d5 <+53>:   test   eax,eax
0x00000000004872d7 <+55>:   jne    0x4872f9 <main.re+89>
0x00000000004872d9 <+57>:   jmp    0x4872db <main.re+59>
0x00000000004872db <+59>:   lea    rax,[rip+0x111be]        # 0x4984a0
0x00000000004872e2 <+66>:   mov    QWORD PTR [rsp],rax
0x00000000004872e6 <+70>:   lea    rax,[rip+0x48643]     
0x00000000004872ed <+77>:   mov    QWORD PTR [rsp+0x8],rax

//panic() => gopanic
0x00000000004872f2 <+82>:   call   0x427880 <runtime.gopanic>
...

recover()的核心其实就是defer函数生成的汇编指令:判断跳转区分正常流程还是获取返回值流程。见上方汇编代码。
机器指令是从上往下执行,正常流程是执行完deferproc之后再执行panic()生成的gopanic()。获取返回值流程必然需要跳转到某处获取,而golang的设计者放到了deferreturn()函数中所以最终要跳到这里来。

留个疑问下方代码如何输出,为什么?

package main

import "fmt"

func main() {
    re()
    fmt.Println("After recovery!")
}

func re() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Recover again:", err)
        }
    }()
    defer func() {
        if err := recover(); err != nil {
            switch v := err.(type) {
            case string:
                panic(string(v))
            }
        }
    }()
    panic("start panic")
}

参考:
Go语言panic/recover的实现

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