深入理解 函數(shù)調(diào)用: 函數(shù)調(diào)用棧 / 寄存器傳遞 / 匯編

caller: 主調(diào)函數(shù)
callee: 被調(diào)函數(shù)

1 x86-64 / Inter 處理器: 1 個 CPU16 個 64 位 通用目的 寄存器, 存 整數(shù)指針

1.1 運(yùn)行時棧: 3 類 寄存器

為確保 callee `不會覆蓋 caller 稍后要用的 寄存器值`,
x86-64 有一組 `規(guī)范:` 何時 use 哪個 寄存器 

(1) callee 保存 寄存器

%bp

可作 幀指針, 存儲 `當(dāng)前棧幀 的 棧底; 
%bp 入棧`, 通常是為了 `保存 前 1 棧幀 的 棧底`**

%bx

%r12-15

(2) caller 保存 寄存器

%ax : 返回值
%di %si %dx %cx %r8 %r9 : 第 1-6 個參數(shù)

//前1-4個分別是 
d s d c //連接
i i x x 

(3) 棧指針

%sp

1.2 callee / caller 保存 寄存器

(1) callee 保存 寄存器
`P 作為` main 的 `被調(diào)用者`

callee 保存, 保證 其 ( callee~器 ) 值callee 返回 時 與 callee 被調(diào)用相同

(2) caller 保存 寄存器

保存 caller~器 是 caller 的責(zé)任

<=
`P 作為` Q 的 `調(diào)用者`
1) caller 在 caller~器 中 store 舊值
2) caller 調(diào)用 callee
3) callee 可隨意修改 之
4) 調(diào)完 后, caller 還要用 stored 舊值

1.3 如何保證 函數(shù)調(diào)用 正確進(jìn)行

答:只需讓 

正在運(yùn)行的過程需要時 ( 什么情況下需要 ? ) 正確保存 ( 如何保存 ? ) callee / caller ~器

(1) 若 calleecallee~器 ( %rbx ), 由 callee 通過 將 callee~器 ( %rbx ) 入/出 ( callee ) 棧保存 callee~器

// main 調(diào) P
1) callee 將 %rbx (舊值) `push 到 callee 棧`  
2) callee 中 mod %rbp 
3) callee 返回前, `pop %rbp (舊值)` from callee 棧

(2) 若 calleecaller~器 ( %rdi )caller 調(diào)用 callee 后 還要用 caller~器, 由 caller 通過 將 caller~器 ( %rdi ) 保存到 callee~器 ( %rbx )保存 caller~器

P ( caller ) 調(diào) Q (callee)
`Q 會 改 %rdi` + Q 返回后, `P 還要用 %rdi (舊值)`
=>

1) `P 保存 %rdi (舊值) 到 %rbx`

    => `P 改 %rbx 
    => P 要先保存 %rbx ( 給 P 的 caller 用, 此時 P 又作為 callee ):
        將 %rbx push 到 P 棧幀, P 返回前 pop %rbx`

2) P 調(diào) Q

3) Q 改 %rdi 

4) `P 用 %rbx 恢復(fù) %rdi ( 舊值 ), 再 use %rdi ( 舊值 )`
##1.4 eg
// eg1: 函數(shù)調(diào)用 底層機(jī)制
// 函數(shù)調(diào)用 過程中 callee / caller 保存寄存器 如何 被 callee / caller 保存?

// long P(long x):
//     return x + Q(0); 

//.c
#include <stdio.h>

long Q(long x)
{
    x = 2;
    return x;
}
long P(long x)
{
    // 2-1 %rdi: 舊值 x
    
    int y;
    
    // 2-2 %rdi 被 Q (作 P 的 callee) 修改: 新值為0
    y = Q(0); 
    
    // 2-3 P 調(diào) Q 之后, P (作 Q 的 caller) 還要用 %rdi 舊值x
    return x + y; 
}

int main()
{
    long x = 10;
    long y = P(x);
    printf("%ld", y);
}

//.s
Q:
    movl    $2, %eax
    ret
P:
    pushq   %rbx       // 1-2 `由 P (作 main 的 callee) 保存 %rbx`: 通過 入/出 P 棧幀
    movq    %rdi, %rbx // 1-1 %rbx 被 P (作 main 的 callee) 修改 / 2-4 `由 P ( 作 Q 的 caller ) 保存 %rdi` ( 舊值 ) 到 %rbx => 1
    movl    $0, %edi
    call    Q
    cltq
    addq    %rbx, %rax
    popq    %rbx
    ret
// eg2: caller 保存寄存器 無需保存 的 case
//.c
long P(long x)
{
    // 1. %rdi: 舊值 x
    
    // 2. %rdi 被改: 新值 x + 1
    return Q(x+1); 
     
    // 3. 之后, P ( caller ) 不再用 %rdi 舊值 x => P ( caller ) 不用保存 %rdi
}

//.s
P:  // P 不用保存 %rdi
    addq    $1, %rdi  
    call    Q
    ret

1.5 為什么 caller~器 要由 caller ( 而不是 callee ) 保存

答: 

eg: 
`P 調(diào) Q + Q 改 %rdi + Q 返回后, P 還要用 %rdi (舊值):`

long P(long x):
     return x + Q(0); 

`若 由 callee 保存`

Q 保存 %rdi 到 %rbx -> until Q 返回 P 時, 才能用 %rbx 恢復(fù) %rdi ( 舊值 )

=> `Q (作為 P 的 callee) 不能 通過 入/出 Q 棧 來 保存 %rbx`
=> Q 返回 P 時, %rbx 的 值被改了

保存了 %rdi, 卻把 %rbp 值丟了

2 匯編:試圖最大化一段代碼的性能

PC: 程序計數(shù)器, 存 下一條指令(在內(nèi)存中)的地址

linux下 由 .c 文件 得到匯編文件 .s:

gcc -Og -S hello.c

2.1 三種操作數(shù)

立即數(shù) immediate
寄存器 register
內(nèi)存引用

2.2 數(shù)據(jù)傳送指令 : 將數(shù)據(jù)從一個位置復(fù)制到另一個位置

1. 指令結(jié)構(gòu)
最后2個字符:分別是 源和目的的大小
倒數(shù)第3個字符:z - 零擴(kuò)展,s-符號擴(kuò)展

2. MOV 系列指令的 5種情形:src -> dst

//Immediate -> Register : 
movl $0x4050, %eax     // 0x4050 -> %eax

//Immediate -> Memeory  : 
movb $-17, (%rsp)      // -17 -> * %rsp

//Register  -> Register : 
movw %bp, %sp          // %bp -> %sp

//Register  -> Memeory  :
movq %rax, -12(%rbp)   // %rax -> *(%rsp - 12)

//Memory    -> Register : 
movb (%rdi, %rcx), %al // *(%rdi + %rcx) -> %al

(1) dst 必須是 Register/Memory
(2) Memory 不能到 Memory
(3) 1方 非 內(nèi)存 時,傳送的是 Immediate/Register 本身的值
(4) 1方 為 內(nèi)存, 即 用 offset( ... ) 形式時译荞,這1方交互的是 () 中的 值 和 偏移 形成的 內(nèi)存地址 上的值

3. 指針的 間接引用 pointer dereferencing**
x = *xp; // `讀` sp 所指內(nèi)存中的值 到 x
*xp = y; // `寫` y 的值 到 sp 所指內(nèi)存

2.3 壓入 和 彈出 棧數(shù)據(jù)

棧指針 %rsp: 保存 棧頂 元素地址

入棧: 棧指針 減8, 值 寫 到棧頂

pushq %rbp
<=>
subq $8, %rsp
movq %rbp, (%rsp) // write %rbp on stack

出棧:讀 棧頂數(shù)據(jù), 棧指針加8

popq %rax
<=>
movq (%rsp), %rax  // read %rax from stack
addq $8, %rsp

2.4 算術(shù)

1. 加載有效地址

leaq: 將 有效地址 寫到 目的操作數(shù)

note: 形式 是 從內(nèi)存 `讀數(shù)據(jù)` 到寄存器

leaq 7(%rdx, %rdx, 4), %rax

將 %rax 的值 設(shè)為 5x + 7, 假定 %rdx 值為 x
2. 二元操作

`第2操作數(shù): 既是源又是目的`
必須是寄存器或內(nèi)存
當(dāng)為內(nèi)存地址時, 
CPU 從內(nèi)存中讀出值, 
執(zhí)行操作, 
結(jié)果寫入內(nèi)存

2.5 跳轉(zhuǎn)

jmp *%rax     // 新地址為 %rax 
jmp *(%rax)   // 新地址為 以 %rax 的值為地址的內(nèi)存中的值

3 過程/函數(shù)調(diào)用 的機(jī)制

函數(shù) 如何 保存現(xiàn)場 并 返回

`過程`: 抽象機(jī)制
函數(shù) function, 方法method, 子例程 subroutine, 處理函數(shù) handler

P 調(diào)用 Q, Q 執(zhí)行后 返回到 P:
1)傳遞控制

1) `進(jìn)入 Q 時`, PC 設(shè)為 `Q 第1條指令的地址`
2) `Q 返回 時`, PC 設(shè)為 `Q 的 返回地址`:
    P 中 調(diào)用 Q 指令 后面 那條指令的地址

2)傳遞數(shù)據(jù)

P 給 Q 提供 參數(shù), Q 向 P 返回1個值

3)分配 和 釋放內(nèi)存

Q 開始時 要為 `loacl 變量 分配空間`,
Q 返回前 必須釋放這些空間

3.1 運(yùn)行時棧

棧幀結(jié)構(gòu)被保存的 register: push 到 棧幀 的 register

C 語言 過程調(diào)用機(jī)制 的關(guān)鍵:使用 提供的 后進(jìn)先出內(nèi)存管理

棧幀: 棧上分配的空間

例: P 調(diào) Q, Q 正在執(zhí)行
1) `P 及 P 的 調(diào)用鏈 中 過程`, 都暫時被 `掛起`
2) 系統(tǒng)分配  P 的棧幀:

將 棧指針減小/增加 可以 在棧上 分配/釋放 空間

1. 何時必須 用棧傳遞诫欠?

3種情形之一:

(1)需保存 被保存的寄存器 : 即 被調(diào)用者保存寄存器

(2)存在 loacl variable必須通過 棧 傳遞

1)寄存器 不夠存 所有 local variable
2)local variable 用 取址運(yùn)算符 &: 
因?yàn)?`只能對 內(nèi)存 取地址`
3)local variable 為 數(shù)組或結(jié)構(gòu):
數(shù)組和結(jié)構(gòu)占用的空間是 一段連續(xù)的內(nèi)存

(3) 該過程 又調(diào)用新過程, 而 寄存器 不夠存 調(diào)用 新過程 所用的所有 arg(7-n個arg)

P 調(diào) Q, Q 正在執(zhí)行.png

2. P 調(diào) Q, Q 的 返回地址 作為 P 的 棧幀 的一部分, 因?yàn)樗娣?與 P相關(guān)的狀態(tài)

  1. P 調(diào) Q 時的 參數(shù)傳遞:

每個棧幀基于一個函數(shù), 棧幀 隨著 函數(shù)的生命周期產(chǎn)生抖拴、發(fā)展和消亡

(1)`寄存器傳遞:` 最多傳 `6個整數(shù)值(指針 和 整數(shù))`

(2)`棧傳遞: 第7-n個參數(shù), 參數(shù) 7->n 反順序壓棧 
=> 參數(shù) 7 在棧頂`

棧幀結(jié)構(gòu)中用到2個寄存器來定位 當(dāng)前幀的空間, 
實(shí)際上 未必一定需要幀指針:
`%ebp/%esp : 幀/棧`指針, 
    總是指向當(dāng)前幀的底/頂部

`編譯器 根據(jù) 匯編指令集` 規(guī)則 `小心調(diào)整 %ebp %esp 的值`

3.2 轉(zhuǎn)移控制

Q 的返回地址 : P 中 call Q 指令的 下一條指令的地址

1. 如何將 控制 從 P 轉(zhuǎn)到 Q ?

(1) call Q
1) Q 返回地址 壓棧
2) 設(shè) PCQ 第1條指令 地址

(2)去執(zhí)行: 執(zhí)行 下一條指令 / PC 所指 指令

2. 如何將 控制 從 Q 返回到 P ?

(1) ret
1) 彈棧 出 Q 返回地址
2) 設(shè)給 PC

(2)去執(zhí)行

image.png
image.png

3.3 數(shù)據(jù)傳送

1. P 調(diào)用 Q

(1)參數(shù)傳遞 : P 把 
`參數(shù) 1-6 復(fù)制到 適當(dāng)?shù)募拇嫫鱜; 
`參數(shù) 7-n 用 棧傳遞, 數(shù)據(jù)大小 都向 指針大小`
    = 8 (64位系統(tǒng)) 的 倍數(shù)對齊
(2) call Q: 控制轉(zhuǎn)移到 Q 

2. Q 返回 P: P 可訪問 寄存器 %rax 中 返回值

3.4 函數(shù)調(diào)用時 參數(shù)的傳遞: 寄存器傳遞/棧傳遞

1. 棧 上 局部存儲

(1) `棧就是一段內(nèi)存, 用來進(jìn)行 內(nèi)存管理``

棧 的 意義: 函數(shù)調(diào)用 中 保存/恢復(fù) 被保存的寄存器 / local variable / 實(shí)參argument / 返回地址 等

(2) 不需要 出棧/入棧 就能 讀取/寫入 棧中任何位置的內(nèi)存值

, 通過 `棧指針 加 偏移 加 讀/寫 操作` 即可
// eg3
// long call_proc(): 
//     proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);

//主調(diào)函數(shù) .c
long call_proc()
{
    long x1 = 1;
    int x2 = 2;
    short x3 = 3;
    char x4 = 4;
    proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
    return (x1+x2)*(x3-x4);
}
//.s
call_proc
    // set up arguments to proc: 為調(diào)用 proc 作準(zhǔn)備
    // alloc 32-byte stack frame, 不算 call_proc 中 被調(diào)函數(shù) proc 返回地址 所占空間, 
    // 因 call proc 里包含 proc 返回地址 壓棧
    subq    $32, %rsp       
       
    // 1. local variable space in stack
    movq    $1, 24(%rsp)    // store 1 in  &x1
    movl    $2, 20(%rsp)
    movw    $3, 18(%rsp)
    movb    $4, 17(%rsp)

    // 2. argument 7-8 : pass by stack
    leaq    17(%rsp), %rax  // create &x4, %rax 只用作普通寄存器, 作存儲用
    movq    %rax, 8(%rsp)   // store &x4 as argunent 8
    movl    $4, (%rsp)      // store 4 as argunent 7

    // 3. argument1-6 : pass by register
    leaq    18(%rsp), %r9   // pass &x3 as argunent 6
    movl    $3, %r8d        // pass 3 as argunent 5
    leaq    20(%rsp), %rcx
    movl    $2, %edx
    leaq    24(%rsp), %rsi
    movl    $1, %edi
        
    // 4. call proc()
    call    proc

    // 5. Retrive changes to memory, %rdx %eax %ecx 這里只用作普通存儲器, 作存儲用
    movslq  20(%rsp), %rdx  // get x2 and convert to long
    addq    24(%rsp), %rdx  // compute x1+x2
    movswl  18(%rsp), %eax
    movsbq  17(%rsp), %ecx
    subl    %ecx, %eax
    cltq                   //convert to long
    imulq   %rdx, %rax
    addq    $32, %rsp
    ret
圖中的數(shù)字為 `字節(jié)序號`, 
64 位OS, 
`指針大小 = 地址總線 = 64位 = 8 Byte`
image.png
(3) call_proc 匯編 大部分是為 調(diào)用 proc 作準(zhǔn)備:

1) 棧上為 局部變量 x1 - x4 建立棧幀, 即 分配存儲空間
2) `leaq 指令 生成到 到這些變量 內(nèi)存的 指針`
3) 為 參數(shù)8-7 建立棧幀
4) 參數(shù) 1-6 加載至 寄存器

call_proc:
    執(zhí)行 call proc 指令: 
        proc 返回地址 入棧, 
        subq $8, %rsp // 在棧上 分配空間

proc: 
    執(zhí)行 ret 指令:
        proc 返回地址 出棧, 
        addq $8, %rsp // 在棧上 釋放空間

note: 書中畫成下圖, 應(yīng)該不對
image.png
//note: 1個函數(shù)也可以直接在 linux 下 進(jìn)行匯編,得到.s文件
//被調(diào)函數(shù) .c
#include <stdio.h>
void proc(long a1,  long *a1p,
          int a2,   int *a2p,
          short a3, short *a3p,
          char a4,  char *a4p)
{
    *a1p += a1;
    *a2p += a2;
    *a3p += a3;
    *a4p += a4;
}

//.s
proc: // 函數(shù) proc 沒有棧幀
    movq    16(%rsp), %rax
    addq    %rdi, (%rsi)
    addl    %edx, (%rcx)
    addw    %r8w, (%r9)
    movl    8(%rsp), %edx
    addb    %dl, (%rax)
    ret
image.png
image.png
// eg4
// caller():
//     long sum = swap_add(&arg1, &arg2);
image.png
image.png
image.png

64位系統(tǒng), 指針 為 8 Byte

2. 寄存器 中 局部存儲

//eg5
// long P(long x, long y):
//     return Q(x) + Q(y);    

#include <stdio.h>
long Q(long x)
{
    return 2*x;
}
long P(long x, long y)
{
    long u = Q(y);
    long v = Q(x);
    return u + v;
}

int main()
{
    long x = 10;
    long y = 20;
    long z = P(x, y);
    printf("%ld", z);
}
Q: // note:Q 無棧幀, Q 參數(shù)個數(shù)<7, 所有 參數(shù)都通過 寄存器傳遞了
    leaq    (%rdi,%rdi), %rax
    ret
P:
// x in %rdi
// y in %rsi
    pushq   %rbp       // save %rbp
    pushq   %rbx       // save %rbx
    movq    %rdi, %rbp // save x(%rdi) to %rbp
    movq    %rsi, %rdi // move y(%rsi) to first argument %rdi
    call    Q          // call Q(y)
    movq    %rax, %rbx // Save Q(y)(saved in %rax) result to %rbx
    movq    %rbp, %rdi // move x(saved in %rbp) to first argument %rdi
    call    Q          // call Q(x)
    addq    %rbx, %rax // add Q(y)(saved in %rbx) to Q(x) (saved in %rax)
    popq    %rbx       // restore %rbx
    popq    %rbp       // restore %rbp
    ret
這里, 只著重分析 P的 匯編 和 棧幀變化:

(1) P的參數(shù)
x : %rdi
y : %rsi

(2) P 中 local 變量

P正在運(yùn)行 行為和棧幀變化:

1)`P 保存 調(diào)~器 %rdi / %rax 到 被~器 %rbp / %rbx`

1> %rdi: 
先存舊值x 
-> P調(diào)Q(y)時, 被P修改為新值y 
-> P調(diào)Q(x)時, P又要用其舊值x

note: `P不修改 %rsi => P 不用保存 %rsi`

2> %rax: 
P 調(diào) Q(y)時,  先存 %rax 舊值 Q(y) 
-> P 調(diào) Q(x) 時, %rax 被 P 修改為新值Q(x) 
-> 返回 u+v 時, P又要用其舊值 u=Q(y)

2)`P 保存 被~器 %rbp  %rbx 到 P 的 棧幀`
P 保存 %rdi / %rax 到 %rbp / %rbx,
即 P 會修改 被~器 %rbp  %rbx
=> P 要先保存 %rbp  %rbx

(3) P的棧幀變化:
image.png

4. 數(shù)據(jù)對齊

簡化 CPU 與 內(nèi)存系統(tǒng)接口 硬件設(shè)計

1. Intel x86-64 建議的 `對齊原則:`

(1) K 字節(jié) 基本對象 的 地址 必須是 K 的倍數(shù)

每種類型的對象 都滿足 自己的對齊限制, 就能 保證對齊

image.png

(2) struct 類型的 指針:

`成員 j: 4 字節(jié)對齊 => 其 地址是 4 的倍數(shù)`
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載膘螟,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者笙僚。
  • 序言:七十年代末芳肌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肋层,更是在濱河造成了極大的恐慌亿笤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件栋猖,死亡現(xiàn)場離奇詭異净薛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掂铐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門罕拂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人全陨,你說我怎么就攤上這事爆班。” “怎么了辱姨?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵柿菩,是天一觀的道長。 經(jīng)常有香客問我雨涛,道長枢舶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任替久,我火速辦了婚禮凉泄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚯根。我一直安慰自己后众,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布颅拦。 她就那樣靜靜地躺著蒂誉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪距帅。 梳的紋絲不亂的頭發(fā)上右锨,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音碌秸,去河邊找鬼绍移。 笑死,一個胖子當(dāng)著我的面吹牛讥电,可吹牛的內(nèi)容都是我干的登夫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼允趟,長吁一口氣:“原來是場噩夢啊……” “哼恼策!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起潮剪,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤涣楷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抗碰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狮斗,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年弧蝇,在試婚紗的時候發(fā)現(xiàn)自己被綠了碳褒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片折砸。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沙峻,靈堂內(nèi)的尸體忽然破棺而出睦授,到底是詐尸還是另有隱情,我是刑警寧澤摔寨,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布去枷,位于F島的核電站,受9級特大地震影響是复,放射性物質(zhì)發(fā)生泄漏删顶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一淑廊、第九天 我趴在偏房一處隱蔽的房頂上張望逗余。 院中可真熱鬧,春花似錦季惩、人聲如沸猎荠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽关摇。三九已至,卻和暖如春碾阁,著一層夾襖步出監(jiān)牢的瞬間输虱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工脂凶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宪睹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓蚕钦,卻偏偏與公主長得像亭病,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嘶居,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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