再探 go 匯編

五一假期在家沒事逛論壇的時候淘这,發(fā)現(xiàn)了一個寶藏網(wǎng)站另锋,傳送門 這個網(wǎng)站可以在線生成多種語言的匯編代碼,有這個好東西,那必須拿go實驗一番埠况。

很久之前我寫過一篇go通過go匯編看多返回值實現(xiàn)的文章傳送門。當時寫的時候比較早棵癣,后來 go 1.17 對函數(shù)調(diào)用時辕翰,傳遞參數(shù)做了修改,簡單說就是go1.17之前浙巫,函數(shù)參數(shù)是通過椊鹗瘢空間來傳遞的,在go1.17時做出了改變的畴,在一些平臺上(AMD64)可以像C,C++那樣使用寄存器傳遞參數(shù)和函數(shù)返回值了渊抄。為什么做出這個改變呢,原因就是寄存器更快丧裁。雖然內(nèi)存已經(jīng)很快了护桦,但是還是沒法和寄存器相比。之前為啥不用寄存器煎娇,用椂郑空間,原因是實現(xiàn)簡單缓呛,不用考慮不同平臺催享,不用架構(gòu)的區(qū)別。

簡單總結(jié)一下兩種方式

  1. 椨窗恚空間:
  • 優(yōu)點:實現(xiàn)簡單因妙,不用區(qū)分不同的平臺,通用性強
  • 缺點:效率低
  1. 寄存器:
  • 優(yōu)點:速度快
  • 缺點:通用性差,不同的平臺需要單獨處理
    當然攀涵,這里說的通用性差是對于編譯器來說的

go匯編基礎(chǔ)知識

再來總結(jié)一次go匯編的基礎(chǔ)知識吧铣耘,現(xiàn)在回頭看之前總結(jié)的還是不全面的
go使用的 plan9 匯編,這個和 AT&T 的匯編差別還是有點大的以故,我個人感覺plan9匯編比較重要的就是四個寄存器蜗细,只要理解了這四個寄存器,匯編就理解了一半了

匯編中個幾個術(shù)語:

  • 棧:進程怒详、線程炉媒、goroutine 都有自己的調(diào)用棧,先進后出(FILO)
  • 棧幀:可以理解是函數(shù)調(diào)用時棘利,在棧上為函數(shù)所分配的內(nèi)存區(qū)域
  • 調(diào)用者:caller橱野,比如:A 函數(shù)調(diào)用了 B 函數(shù),那么 A 就是調(diào)用者
  • 被調(diào)者:callee善玫,比如:A 函數(shù)調(diào)用了 B 函數(shù)水援,那么 B 就是被調(diào)者
寄存器 說明
SB(Static base pointer) global symbols 全局靜態(tài)指針
FP(Frame pointer) arguments and locals 指向棧幀的開始
SP(Stack pointer) top of stack 指向棧頂
PC(Program counter) jumps and branches 簡單說程序計數(shù)器

簡單展開說一下幾個寄存器吧

  • SB:全局靜態(tài)指針,即程序地址空間的開始地址茅郎。一般用在聲明函數(shù)蜗元、全局變量中。
  • FP:指向的是 caller 調(diào)用 callee 時傳遞的第一個參數(shù)的位置系冗,可以看作是指向兩個函數(shù)棧的分割位置奕扣;但是FP指向的位置不在 callee 的 stack frame 之內(nèi)。而是在 caller 的 stack frame 上掌敬,指向調(diào)用 add 函數(shù)時傳遞的第一個參數(shù)的位置惯豆;可以在 callee 中用 symbol+offset(FP) 來獲取入?yún)⒌膮?shù)值,比如 a+8(FP)奔害。雖然 symbol 沒有什么具體意義楷兽,但是不加編譯器會報錯。
  • SP:這個是最常用的寄存器了华临,同樣也是最復(fù)雜的寄存器了芯杀。不同的引用方式,代表不同的位置雅潭。SP寄存器 分為偽 SP 寄存器和硬件 SP 寄存器揭厚。symbol+offset(SP) 形式,則表示偽寄存器 SP (這個也簡稱為 BP)扶供。如果是 offset(SP) 則表示硬件寄存器 SP筛圆。偽 SP 寄存器指向當前棧幀第一個局部變量的結(jié)束位置;硬件SP指向的是整個函數(shù)棧結(jié)束的位置椿浓。有個比較坑的地方:對于編譯輸出(go tool compile -S / go tool objdump)的代碼來講顽染,所有的 SP 都是硬件 SP 寄存器漾岳,無論是否帶 symbol(這一點非常具有迷惑性,需要慢慢理解粉寞。往往在分析編譯輸出的匯編時,看到的就是硬件 SP 寄存器)左腔。
  • PC:這個就是計算機常見的 pc 寄存器唧垦,在 x86 平臺下對應(yīng) ip 寄存器,amd64 上則是 rip液样。這個很少有用到振亮。

通過一個棧幀的圖來理解一下這幾個寄存器

棧幀.jpg

大體的棧幀就是圖中的這樣,圖中標注的寄存器都是以 callee 函數(shù)為基準的

通過圖中可知鞭莽,如果callee函數(shù)中沒有局部變量的話坊秸,SP硬寄存器和SP偽寄存器指向的是同一個地方

偽 FP 寄存器對應(yīng)的是 caller 函數(shù)的幀指針,一般用來訪問 callee 函數(shù)的入?yún)?shù)和返回值澎怒。偽 SP 棧指針對應(yīng)的是當前 callee 函數(shù)棧幀的底部(不包括參數(shù)和返回值部分)褒搔,一般用于定位局部變量。硬件 SP 是一個比較特殊的寄存器喷面,因為還存在一個同名的 SP 真寄存器星瘾,硬件 SP 寄存器對應(yīng)的是棧的頂部。

在編寫 Go 匯編時惧辈,當需要區(qū)分偽寄存器和真寄存器的時候只需要記住一點:偽寄存器一般需要一個標識符和偏移量為前綴琳状,如果沒有標識符前綴則是真寄存器。比如(SP)盒齿、+8(SP)沒有標識符前綴為真 SP 寄存器念逞,而 a(SP)、b+8(SP)有標識符為前綴表示偽寄存器边翁。

還有一點

如果callee的楐岢校空間大小是0的話, caller BP 是不會被壓入棧中的倒彰,此時的SP硬件寄存器和偽FP寄存器指向的是同一個位置审洞。

在匯編中,函數(shù)的定義

TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-24

TEXT 是一個特殊的指令待讳,定義一個函數(shù)

.add 是函數(shù)的名

NOSPLIT 向編譯器表明不應(yīng)該插入 stack-split 的用來檢查棧需要擴張的前導(dǎo)指令

$0-24 這兩個參數(shù)芒澜,0是聲明這個函數(shù)需要的棧空間的大小创淡,一般來說就是局部變量需要的空間痴晦,單位是位。
24是聲明函數(shù)傳入?yún)?shù)和返回值需要的椓詹剩空間的大小誊酌,單位也是位部凑。

生成匯編

了解了這些基本概念后,上一段代碼碧浊,通過匯編看一下不同版本go是如何處理函數(shù)傳遞參數(shù)的

先通過一段簡單的代碼看一下區(qū)別

package main

func main(){
    add(10,20)
}

//go:noinline
func add(a,b int) int{
    return a+b
}

//go:noinline 這個是告訴編譯器不要對這個函數(shù)內(nèi)聯(lián)涂邀,這個東西叫g(shù)o的編譯指令,go還有你很多別的指令箱锐,這里就不展開了比勉。想想go也挺有意思的,C++是通過 inline 顯式的指定要內(nèi)聯(lián)驹止,go是告訴編譯器不要內(nèi)聯(lián)浩聋。

先看一下在1.16下的匯編代碼

main_pc0:
        .file 1 "<source>"
        .loc 1 5 0
        TEXT    "".main(SB), ABIInternal, $32-0
        MOVQ    (TLS), CX
        CMPQ    SP, 16(CX)
        PCDATA  $0, $-2
        JLS     main_pc64
        PCDATA  $0, $-1
        SUBQ    $32, SP
        MOVQ    BP, 24(SP)
        LEAQ    24(SP), BP
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        .loc 1 11 0
        MOVQ    $10, (SP)
        MOVQ    $20, 8(SP)
        PCDATA  $1, $0
        CALL    "".add(SB)
        .loc 1 12 0
        MOVQ    24(SP), BP
        ADDQ    $32, SP
        RET
        NOP
        .loc 1 5 0
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        NOP
main_pc64:
        CALL    runtime.morestack_noctxt(SB)
        PCDATA  $0, $-1
        JMP     main_pc0
        .loc 1 21 0
        TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-24
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        .loc 1 22 0
        MOVQ    "".b+16(SP), AX
        MOVQ    "".a+8(SP), CX
        ADDQ    CX, AX
        MOVQ    AX, "".~r2+24(SP)
        RET

代碼有很多,只關(guān)注下面圖中標注的


MOVQ "".b+16(SP), AXMOVQ "".a+8(SP), CX 可以得知臊恋,函數(shù)是通過SP寄存器偏移完成傳遞參數(shù)的衣洁。
這里要注意,.b+16(SP) 這種寫法看著像是使用的是偽SP寄存器抖仅,實際上用的是硬件SP寄存器

同樣的坊夫,在函數(shù)調(diào)用之前,也會把數(shù)值放到棧的指定位置
MOVQ $10, (SP) , MOVQ $20, 8(SP)

把編譯器換成最新的 1.18看一下

main_pc0:
        .file 1 "<source>"
        .loc 1 5 0
        TEXT    "".main(SB), ABIInternal, $24-0
        CMPQ    SP, 16(R14)
        PCDATA  $0, $-2
        JLS     main_pc47
        PCDATA  $0, $-1
        SUBQ    $24, SP
        MOVQ    BP, 16(SP)
        LEAQ    16(SP), BP
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        .loc 1 11 0
        MOVL    $10, AX
        MOVL    $20, BX
        PCDATA  $1, $0
        NOP
        CALL    "".add(SB)
        .loc 1 12 0
        MOVQ    16(SP), BP
        ADDQ    $24, SP
        RET
main_pc47:
        NOP
        .loc 1 5 0
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        CALL    runtime.morestack_noctxt(SB)
        PCDATA  $0, $-1
        JMP     main_pc0
        .loc 1 21 0
        TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-16
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $5, "".add.arginfo1(SB)
        FUNCDATA        $6, "".add.argliveinfo(SB)
        PCDATA  $3, $1
        .loc 1 22 0
        ADDQ    BX, AX
        RET

在1.18的匯編代碼中就沒有通過棸妒郏空間來傳遞參數(shù)了践樱,而是直接通過寄存器完成操作,
ADDQ BX, AX凸丸,并且返回值直接放到寄存器中拷邢。

寄存器數(shù)量是有上限的,如果傳遞的參數(shù)個數(shù)超過了寄存器的上限屎慢,又會怎樣處理呢

package main

func main(){
    add(1,2,3,4,5,6,7,8,9,10,11,12)
}

//go:noinline
func add(a,b,c,d,e,f,g,h,i,j,k,l int) int{
    return a+b+c+d+e+f+g+h+i+j+k+l
}

對應(yīng)的匯編

main_pc0:
        .file 1 "<source>"
        .loc 1 5 0
        TEXT    "".main(SB), ABIInternal, $104-0
        CMPQ    SP, 16(R14)
        PCDATA  $0, $-2
        JLS     main_pc111
        PCDATA  $0, $-1
        SUBQ    $104, SP
        MOVQ    BP, 96(SP)
        LEAQ    96(SP), BP
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        .loc 1 11 0
        MOVQ    $10, (SP)
        MOVQ    $11, 8(SP)
        MOVQ    $12, 16(SP)
        MOVL    $1, AX
        MOVL    $2, BX
        MOVL    $3, CX
        MOVL    $4, DI
        MOVL    $5, SI
        MOVL    $6, R8
        MOVL    $7, R9
        MOVL    $8, R10
        MOVL    $9, R11
        PCDATA  $1, $0
        NOP
        CALL    "".add(SB)
        .loc 1 12 0
        MOVQ    96(SP), BP
        ADDQ    $104, SP
        RET
main_pc111:
        NOP
        .loc 1 5 0
        PCDATA  $1, $-1
        PCDATA  $0, $-2
        CALL    runtime.morestack_noctxt(SB)
        PCDATA  $0, $-1
        JMP     main_pc0
        .loc 1 21 0
        TEXT    "".add(SB), NOSPLIT|ABIInternal, $0-96
        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        FUNCDATA        $5, "".add.arginfo1(SB)
        FUNCDATA        $6, "".add.argliveinfo(SB)
        PCDATA  $3, $1
        .loc 1 22 0
        LEAQ    (BX)(AX*1), DX
        ADDQ    DX, CX
        ADDQ    DI, CX
        ADDQ    SI, CX
        ADDQ    R8, CX
        ADDQ    R9, CX
        ADDQ    R10, CX
        ADDQ    R11, CX
        MOVQ    "".j+8(SP), DX
        ADDQ    DX, CX
        MOVQ    "".k+16(SP), DX
        ADDQ    DX, CX
        MOVQ    "".l+24(SP), DX
        LEAQ    (DX)(CX*1), AX
        RET

通過匯編可以看到瞭稼,會先使用寄存器,當寄存器不夠時腻惠,會使用椈分猓空間。

手寫匯編

通過手寫一段匯編代碼集灌,驗證一下各個寄存器的位置悔雹,我用的go版本是 1.14.13,所以傳參數(shù)用的是椥佬空間腌零。
main.go 文件中

package main

func add(int, int) int

func main() {
    print(add(10, 20))
}

定義一個 main 函數(shù)作為整個程序的入口,聲明一個 add(int, int) int 函數(shù)唆阿,add 函數(shù)的具體實現(xiàn)是用匯編寫的,
main.go 同級目錄下創(chuàng)建一個 add_amd64.s 的文件益涧。

使用硬BP寄存器
TEXT ·add(SB), $0-24
    MOVQ 8(SP), AX
    MOVQ 16(SP), BX
    ADDQ BX, AX
    MOVQ AX, 24(SP)
    RET

使用 go run . 命令,看一下程序執(zhí)行的結(jié)果驯鳖。

這時候的棧幀如圖所示



因為add函數(shù)椣醒空間是0久免,所以偽SP寄存器沒有被壓入棧中扭弧,偽SP寄存器和硬件SP寄存器指向的是同一個位置。

使用偽SP寄存器
TEXT ·add(SB), $16-24
    MOVQ a+16(SP), AX
    MOVQ b+24(SP), BX
    ADDQ BX, AX
    MOVQ AX, ret+32(SP)
    RET

為了區(qū)分對比丁寄,這時候把add函數(shù)的棽蠢ⅲ空間設(shè)置為16盛正,然后使用偽SP寄存器來獲取值。
執(zhí)行結(jié)果如下:


這時候的椞底蹋空間如圖


使用FP寄存器

代碼如下:

TEXT ·add(SB), $16-24
    MOVQ a+0(FP), AX
    MOVQ b+8(FP), BX
    ADDQ BX, AX
    MOVQ AX, ret+16(FP)
    RET

執(zhí)行結(jié)果:


說明還是正確的续崖,此時的棧空間沒有變化多艇,和上面是一樣的像吻。

到這,應(yīng)該能理解各個寄存器的相對位置了吧:
在callee棽Υ遥空間不為0的時候,

FP = 硬件SP + framsize + 16
SP = 硬件SP + framsize

在callee椆嵌觯空間為0的時候台腥,

FP = 硬件SP + 8
SP = 硬件SP

匯編簡單分析

通過上面的代碼會發(fā)現(xiàn),手寫匯編的代碼和反匯編的代碼有些不同览爵。反匯編得到的代碼,在函數(shù)前面會有一段
CALL runtime.morestack_noctxt(SB)
其實這個是編譯器自動插入的一段函數(shù)箕母,這段指令會調(diào)用一次 runtime.morestack_noctxt 這個函數(shù)具體的作用是挺復(fù)雜的,主要有 檢查是否需要擴張棧嘶是,go的棧空間是可以動態(tài)擴充的辖源,所以在調(diào)用函數(shù)前會檢查當前的椣L空間是否需要擴充。還有一個功能就是檢查當前協(xié)程需要搶占誊辉。go在1.14之前goroutine的搶占是協(xié)作式搶占模式,怎么判斷一個協(xié)程是否需要搶占呢堕澄?后臺協(xié)程會定時掃描當前運行中的協(xié)程,如果發(fā)現(xiàn)一個協(xié)程運行比較久拍屑,會將其標記為搶占狀態(tài)坑傅。這個掃描的時間點就是函數(shù)調(diào)用期間完成的。

不同類型參數(shù)傳遞

傳結(jié)構(gòu)體

package main

type One struct {
    a int
    b int
}

func main(){
    o := One {
        a:10,
        b.20,
    }
    f1(o)
}

//go:noinline
 func f1(o One) int {
     return o.a + o.b
 }

只貼 關(guān)鍵的匯編代碼吧

    MOVQ    $10, (SP)
        MOVQ    $0, 8(SP)
        PCDATA  $1, $0
        CALL    "".f1(SB)

..........................

        TEXT    "".f1(SB), NOSPLIT|ABIInternal, $0-24
        MOVQ    "".o+16(SP), AX
        MOVQ    "".o+8(SP), CX
        ADDQ    CX, AX
        MOVQ    AX, "".~r1+24(SP)
        RET

在傳結(jié)構(gòu)體的時候矢渊,只把結(jié)構(gòu)體的內(nèi)容傳進去了枉证。

傳結(jié)構(gòu)體指針
package main

type One struct {
    a int
    b int
}

func main(){
    o := &One{
        a:10,
        b:20,
    }
    f1(o)
}

//go:noinline
 func f1(o *One) int {
     return o.a + o.b
 }

匯編

        LEAQ    ""..autotmp_2+16(SP), AX
        MOVQ    AX, (SP)
        PCDATA  $1, $0
        CALL    "".f1(SB)

.......................

        TEXT    "".f1(SB), NOSPLIT|ABIInternal, $0-16
        MOVQ    "".o+8(SP), AX
        MOVQ    (AX), CX
        ADDQ    8(AX), CX
        MOVQ    CX, "".~r1+16(SP)
        RET

可以看到,傳遞指針的時候毡鉴,只是把結(jié)構(gòu)體第一個元素的地址傳遞進去了。

如果是傳一個空的結(jié)構(gòu)體

package main

type One struct {
}

func main(){
    o := One{}
    f1(o,10,20)
}

//go:noinline
 func f1(o One, a,b int) int {
     return a + b
 }

匯編

        TEXT    "".f1(SB), NOSPLIT|ABIInternal, $0-24
        .loc 1 15 0
        MOVQ    "".b+16(SP), AX
        MOVQ    "".a+8(SP), CX
        ADDQ    CX, AX
        MOVQ    AX, "".~r3+24(SP)
        RET

可以看到猪瞬,當傳遞一個空結(jié)構(gòu)體時入篮,相當于沒有傳遞,因為空結(jié)構(gòu)體的空間大小是0潮售,所以編譯器就給忽略了锅风。

最后

暫時就想到這么多鞍泉,先寫這些吧,后面想起別的再補充吧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末边器,一起剝皮案震驚了整個濱河市托修,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌睦刃,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吃环,居然都是意外死亡,警方通過查閱死者的電腦和手機郁轻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門文留,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人骑篙,你說我怎么就攤上這事森书。” “怎么了凛膏?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長猖毫。 經(jīng)常有香客問我,道長趁蕊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任介衔,我火速辦了婚禮恨胚,結(jié)果婚禮上炎咖,老公的妹妹穿的比我還像新娘乘盼。我一直安慰自己,他們只是感情好绸栅,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著粹胯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪况鸣。 梳的紋絲不亂的頭發(fā)上竹观,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天镐捧,我揣著相機與錄音臭增,去河邊找鬼。 笑死列牺,一個胖子當著我的面吹牛拗窃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播并炮,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荤西!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勉躺,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤觅丰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后妇萄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冠句,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年唇牧,在試婚紗的時候發(fā)現(xiàn)自己被綠了聚唐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杆查。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掺栅,到底是詐尸還是另有隱情氧卧,我是刑警寧澤搏明,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布星著,位于F島的核電站粗悯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏铺遂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捞蚂。 院中可真熱鬧姓迅,春花似錦丁存、人聲如沸柴我。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逾礁。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焰枢,已是汗流浹背暑椰。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人监憎。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像迄委,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子渔扎,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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