手把手教你學(xué)會(huì)gdb撒轮,適應(yīng)Linux調(diào)試環(huán)境

在前文 基于vscode 打造Linux C++編碼環(huán)境 一期中,講解了如何基于vscode搭建Linux c++的編碼環(huán)境贼穆,但是還沒有講解如何基于vscod搭建調(diào)試環(huán)境题山。本期,主要有兩個(gè)任務(wù):

  • 講解常用的gcc編譯選項(xiàng)
  • 講解常用的gdb編譯指令


常用gcc編譯選項(xiàng)

深入了解C++系列中,我經(jīng)常使用如下的格式進(jìn)行編譯崖蜜、執(zhí)行demo:

$ g++ -g -O0 main.cc -o main && ./main

下面浊仆,我們來看看常用的gcc編譯選項(xiàng)有哪些。

選項(xiàng) 作用
-E 生成預(yù)處理文件
-S 生成匯編文件
-c 生成可目標(biāo)文件
-o 指定生成文件的文件名
-On 指定代碼優(yōu)化等級(jí)
-g 用于gdb調(diào)試豫领、objdump
-Wall 顯示代碼中的所有warning行為
-w 禁止顯示代碼中的warning行為
-Werror 將代碼中的warning行為視為為error
-D 設(shè)置預(yù)定義宏
-l 鏈接(link)指定的函數(shù)庫
-std=c++11 指定編譯代碼的C++標(biāo)準(zhǔn)為C++11

對(duì)于這些編譯選項(xiàng)抡柿,簡單的解釋下。

-E等恐、 -S 洲劣、-c 三個(gè)選項(xiàng)直接對(duì)應(yīng)著編譯的前三個(gè)基本階段

預(yù)編譯處理(.i)

將源文件main.cc 經(jīng)過預(yù)處理后,生成文件預(yù)處理所得文件main.i

g++ -E main.cc -o main.i
編譯课蔬、優(yōu)化程序(.s)

main.i 文件翻譯成一個(gè)匯編文件 main.s 囱稽;

g++ -S main.i  -o main.s
匯編程序(.o)

運(yùn)行匯編器,將 main.s 翻譯成一個(gè)可重定位目標(biāo)文件 main.o 二跋;

 g++ -c main.s -o main.o
鏈接程序(.elf)

運(yùn)行鏈接器战惊,將 main.o 中使用到的目標(biāo)文件組合起來,并創(chuàng)建一個(gè)可執(zhí)行的文件 main 扎即。由于main.cc代碼沒有額外的依賴吞获,因此可以直接輸出main文件况凉。

 g++ main.o -o main

實(shí)際上,一步就能完成上面所有的操作:

g++ main.cc -o main
定義宏 -D

比如各拷,對(duì)于下面的一段demo刁绒,如果定義了宏DEBUG,則輸出hello cpp烤黍。

int main(int argc, char const *argv[]) {
#ifdef DEBUG
  std::cout<<"Hello Cpp" <<std::endl;
#endif
  return 0;
}

下面在gcc編譯時(shí)基于-D選項(xiàng)設(shè)置DEBUG宏知市,來控制程序執(zhí)行。

$ g++ -DDEBUG main.cc -o main && ./main
Hello Cpp

對(duì)于GCC的編譯選項(xiàng)蚊荣,沒有必要全部記住初狰,記住常用的即可,其他用到了再去官網(wǎng)查詢:

https://gcc.gnu.org/onlinedocs/gcc/Invoking-GCC.html

常用gdb指令

本期主要講解下我常用的gdb指令互例、以及怎么去學(xué)習(xí)gdb奢入。希望能通過本期博客,能幫助你擺脫對(duì)gdb恐懼媳叨,并熟悉下gdb的常用指令腥光,對(duì)于沒有講解到的指令,在本期之后糊秆,可以去官方網(wǎng)站自行學(xué)習(xí)武福,那里有著詳細(xì)且為全面的介紹:

https://sourceware.org/gdb/current/onlinedocs/gdb/

為了方便后面基于gdb調(diào)試REDIS源碼的講解,可以先下載REDIS6.0的源碼痘番,并在編譯代碼的時(shí)候捉片,加上-g -O0選項(xiàng),生成調(diào)試信息汞舱。比如伍纫,我學(xué)習(xí)REDIS的時(shí)候,編譯指令如下:

$ git clone https://github.com/redis/redis.git  # 下載redis源碼
$ cd redis/src                                  # 進(jìn)入源代碼
$ make FLAGS="-g -O0"  -j 16                    # 編譯
$ ./redis-server                                # 運(yùn)行REDIS服務(wù)器

啟動(dòng)gdb

關(guān)于啟動(dòng)gdb的方式昂芜,下面介紹下常用的三種啟動(dòng)gdb方式:

  1. gdb [program]:這種方式最常用莹规,比如使用gdb調(diào)試上面編譯生成的main文件,那么就直接 gdb main泌神。
  2. gdb [program] core:用于調(diào)試導(dǎo)致coredump的錯(cuò)誤良漱,此時(shí)需要在program后面加上因?yàn)閏oredump生成的core文件路徑。
  3. gdb -p [pid]:使用gdb調(diào)試正在運(yùn)行的pid進(jìn)程

gdb program

以如下的main程序?yàn)槔?/p>

// main.cc
#include <iostream>

int main(int argc, char const *argv[])
{
  int cnt =0;
  for(int idx=0; idx < 10; ++idx) { 
    cnt++;
  }
  std::cout<<cnt<<std::endl;

  return 0;
}

編譯指令:

$ g++ -g -O0 main.cc -o main

在終端輸入gdb main欢际,會(huì)從main文件中加載符號(hào)表母市,便于設(shè)置斷點(diǎn)等信息:

$ gdb main
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
# 以上都是關(guān)于gdb的開源信息,為便于描述损趋,下面的教程中會(huì)省略這部分信息
Reading symbols from main...
(gdb) 

輸入gdb main后窒篱,會(huì)首先顯示關(guān)于gdb的一大串的開源信息,而且每次啟動(dòng)都會(huì)顯示。因此墙杯,在后文的講解中,每次啟動(dòng)gdb會(huì)省略掉這部分信息括荡。

attach pid

如果某個(gè)程序正在運(yùn)行出現(xiàn)故障高镐,比如服務(wù)器程序,無法被中止畸冲,如何使用gdb來調(diào)試它嫉髓?

比如,此刻我電腦正在運(yùn)行REDIS服務(wù)器程序,其pid是1607:

  • 我先以root權(quán)限啟動(dòng)gdb
  • 再使用attach pid命令來調(diào)試正在運(yùn)行的REDIS服務(wù)器程序

示例如下:

$ sudo gdb                          # 先以root權(quán)限啟動(dòng)gdb
# ...關(guān)于gdb的開源聲明省略
(gdb) attach 1607                   # 再使當(dāng)前gdb環(huán)境去調(diào)試redis服務(wù)器
Attaching to process 1607
[New LWP 1608]
[New LWP 1609]
[New LWP 1610]
[New LWP 1611]
[New LWP 1612]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f2d694925ce in epoll_wait (epfd=5, events=0x7f2d68ede980, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c
(gdb) 

當(dāng)使用attach命令調(diào)試完服務(wù)器程序,可以使用detach指令退出褪子。

(gdb) detach        
Detaching from program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server, process 1607
[Inferior 1 (process 1607) detached]

gdb -p pid

當(dāng)然菠发,也可以直接使用gdb -p pid指令,來調(diào)試正在運(yùn)行的REDIS服務(wù)器程序,其效果和attach一致:

$ sudo gdb -p 1607              # 也要使用root權(quán)限
Attaching to process 1607
[New LWP 1608]
[New LWP 1609]
[New LWP 1610]
[New LWP 1611]
[New LWP 1612]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
--Type <RET> for more, q to quit, c to continue without paging--
0x00007f2d694925ce in epoll_wait (epfd=5, events=0x7f2d68ede980, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c

毫無疑問漏益,這也是可以由detach命令,退出調(diào)試環(huán)境:

(gdb) detach 
Detaching from program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server, process 1607
[Inferior 1 (process 1607) detached]

其他啟動(dòng)gdb的方式,可以參考官方文檔:

https://sourceware.org/gdb/current/onlinedocs/gdb/Invoking-GDB.html#Invoking-GDB

運(yùn)行程序

run

run 指令,簡寫是r,在啟動(dòng)gdb環(huán)境之后,用于運(yùn)行待調(diào)試的程序。比如啟動(dòng)REDIS程序:

$ gdb redis-server       # 先啟動(dòng) gdb 環(huán)境
#...
Reading symbols from redis-server...
(gdb) r                  # 再啟動(dòng)redis服務(wù)器
# ---------------- 下面是redis的啟動(dòng)信息矾瑰,暫時(shí)不用管 --------------- #
Starting program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
1845:C 27 Mar 2021 20:42:02.143 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1845:C 27 Mar 2021 20:42:02.143 # Redis version=6.0.5, bits=64, commit=00000000, modified=0, pid=1845, just started
1845:C 27 Mar 2021 20:42:02.143 # Warning: no config file specified, using the default config. In order to specify a config file use /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server /path/to/redis.conf
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 1845
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

1845:M 27 Mar 2021 20:42:02.146 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1845:M 27 Mar 2021 20:42:02.146 # Server initialized
1845:M 27 Mar 2021 20:42:02.146 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
[New Thread 0x7ffffe7a0700 (LWP 1849)]
[New Thread 0x7ffffdf90700 (LWP 1850)]
[New Thread 0x7ffffd780700 (LWP 1851)]
[New Thread 0x7ffffcf70700 (LWP 1852)]
[New Thread 0x7ffffc760700 (LWP 1853)]
-----1----1845:M 27 Mar 2021 20:42:02.151 * Loading RDB produced by version 6.0.5
1845:M 27 Mar 2021 20:42:02.151 * RDB age 839 seconds
1845:M 27 Mar 2021 20:42:02.151 * RDB memory usage when created 0.77 Mb
1845:M 27 Mar 2021 20:42:02.152 * DB loaded from disk: 0.000 seconds
1845:M 27 Mar 2021 20:42:02.152 * Ready to accept connections

set args

如果待調(diào)試的程序需要輸入?yún)?shù)征绎,那么在啟動(dòng)gdb環(huán)境后、運(yùn)行待調(diào)試程序前歼指,使用set args指令來設(shè)置程序所需的輸入?yún)?shù)。

比如在啟動(dòng)REDIS的哨兵服務(wù)器時(shí)孟害,需要設(shè)置哨兵模式下的配置文件路徑:

$ gdb redis-server                                                           # 啟動(dòng) gdb 環(huán)境
(gdb) set args  /home/szza/redis-6.0.5/redis-6.0.5/sentinel.conf --sentinel  # 設(shè)置輸入?yún)?shù)
(gdb) r                                                                      # 運(yùn)行

退出gdb

退出gdb調(diào)試界面命令是:quit玉组,簡寫q惯雳。

如果程序正在運(yùn)行,你嘗試去退出仗颈,會(huì)有個(gè)提示,是否真的要退出陌粹,防止你不小心將gdb調(diào)試終止:

(gdb) quit
A debugging session is active.

        Inferior 1 [process 1660] will be killed.

Quit anyway? (y or n) 

斷點(diǎn)

break

break指令或舞,簡寫是b荆姆,用于在指定的地方加上斷點(diǎn),當(dāng)程序運(yùn)行至斷點(diǎn)處就會(huì)暫停映凳,便于調(diào)試胆筒。break指令如下:

  • breakbreak后面沒有任何參數(shù),那么就在當(dāng)前棧幀的下一個(gè)指令處加上斷點(diǎn)

  • break line:在當(dāng)前運(yùn)行程序的line行處加斷點(diǎn)诈豌。如果想在其他文件的某行添加斷點(diǎn)仆救,可以使用break filename:line指令。

  • break function:在當(dāng)前運(yùn)行程序的function處加上斷點(diǎn)矫渔。

    對(duì)于C++程序彤蔽,可能會(huì)存在重載,甚至不同類存在同名函數(shù)庙洼,那么可以更加具體的設(shè)置:

    • break filename:function:在filename文件的 function 處加上斷點(diǎn)
    • break filename:function(ArgsType...):在filename文件的function(args)處加上斷點(diǎn)顿痪,其參數(shù)類型ArgsType...
    • break class:function:在類classfunction處加上斷點(diǎn),當(dāng)然這里的函數(shù)可以加上具體參數(shù)類型

下面以REDIS程序?yàn)槔凸唬菔鞠聨追N打斷點(diǎn)的方法蚁袭。

在指令setCommand位置處加上斷點(diǎn):

# 方式1
(gdb) break t_string.c:99
Breakpoint 1 at 0x7c6e9: file t_string.c, line 99.
# 方式2
(gdb) break setCommand 
Note: breakpoint 1 also set at pc 0x7c6e9.
Breakpoint 2 at 0x7c6e9: file t_string.c, line 99.
# 方式3
(gdb) break t_string.c:setCommand 
Note: breakpoint 1 also set at pc 0x7c6e9.
Breakpoint 3 at 0x7c6e9: file t_string.c, line 99.

當(dāng)redis服務(wù)接收到客戶端的 SET指令時(shí),就會(huì)在該斷點(diǎn)位置處停止:

Thread 1 "redis-server" hit Breakpoint 3, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>) at t_string.c:99
99      void setCommand(client *c) {

關(guān)于break指令能指定位置石咬,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Specify-Location.html#Specify-Location

break … if cond

但是如果只想在滿足某個(gè)條件時(shí)揩悄,才觸發(fā)斷點(diǎn),怎么辦碌补?

可以考慮使用break … if cond命令虏束,其中...是上述break后的參數(shù)。

比如厦章,以上面的main.cc程序?yàn)槔蛟龋?dāng)cnt > 3的時(shí)候停止程序:

(gdb) break 7 if cnt > 3
Breakpoint 1 at 0x80011d0: file main.cc, line 7.

當(dāng)程序運(yùn)行到cnt >3時(shí)就會(huì)停止:

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:7
7           cnt++;
(gdb) print cnt     # 顯示 cnt 的值
$1 = 4
by the way

break … if cond指令有時(shí)候不會(huì)生效,比如:

(gdb) break main if cnt > 3
Breakpoint 2 at 0x80011a9: file main.cc, line 4.

整個(gè)程序運(yùn)行結(jié)束袜啃,也不會(huì)觸發(fā)汗侵。我猜測,條件斷點(diǎn)需要在cnt每次產(chǎn)生值改變的位置加上判斷條件群发,而這個(gè)位置剛好是第7行晰韵。

關(guān)于斷點(diǎn)指令的更多信息,參考官方文檔:

https://sourceware.org/gdb/current/onlinedocs/gdb/Set-Breaks.html#Set-Breaks

info b

查看斷點(diǎn)信息熟妓,可以使用info breakpoints指令雪猪,簡寫是info b

仍然以上面的REDIS程序?yàn)槔?/p>

(gdb) info b    
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99

disable 起愈、enable 只恨、delete

  • disable n1 n2 n3 ...:臨時(shí)關(guān)閉編號(hào)為n1译仗、n2...的斷點(diǎn)
  • enable n1 n2 n3 ...:開啟被disable指令關(guān)閉的斷點(diǎn) n1官觅、n2纵菌、...
  • delete n1 n2 n3 ...:直接刪除斷點(diǎn)n1 n2 n3 ...

如果disableenable休涤、delete后面沒有指定具體的參數(shù)咱圆,則是關(guān)閉、開啟功氨、刪除所有的斷點(diǎn)序苏。

下面是以REDIS為例的斷點(diǎn)設(shè)置(觀察Enb下的標(biāo)識(shí),Y表示開啟捷凄,N表示關(guān)閉):

(gdb) disable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) enable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) delete 1
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) disable 2 3
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) enable 2 3
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99

關(guān)于disable杠览、enable的其余指令,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Disabling.html#Disabling

執(zhí)行流程

僅僅有斷點(diǎn)還不行纵势,還是需要進(jìn)一步控制程序的執(zhí)行流程。主要有以下三種:

  • next
  • step
  • continue

continue

continue指令管钳,簡寫是c钦铁,用于恢復(fù)被break指令中斷的程序,使其繼續(xù)向下運(yùn)行才漆。

step [count]

step [count]指令牛曹,簡寫是s,是逐步執(zhí)行count個(gè)步驟醇滥,而不是count個(gè)語句黎比、函數(shù)。當(dāng)不寫count時(shí)鸳玩,默認(rèn)就執(zhí)行一步阅虫。

step指令,用于配合break指令一起使用:當(dāng)在某個(gè)函數(shù)起始處觸發(fā)斷點(diǎn)不跟,想要進(jìn)入該函數(shù)體颓帝,則可以使用step指令。而step count則是一次性執(zhí)行count步窝革,避免繁瑣的中間行為购城,比如避免C++中的構(gòu)造函數(shù)等。

比如對(duì)于下面的C++程序:

int main(int argc, char const *argv[])
{
  std::unordered_map<int, int> map;
  map.insert({1,1});    // 第 6 行
  return 0;
}

想要在gdb中查看insert函數(shù)的原型虐译,而忽略中間的{1,1}的構(gòu)造過程:

(gdb) break 6
Breakpoint 1 at 0x1298: file main.cc, line 6.
(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:6
6         map.insert({1,1});
(gdb) s 9       # 一次性執(zhí)行9步
std::unordered_map<int, int, std::hash<int>, std::equal_to<int>, std::allocator<std::pair<int const, int> > >::insert (this=0x7ffffffede64, __x=...)
    at /usr/include/c++/9/bits/unordered_map.h:585
585           insert(value_type&& __x)
(gdb) s         # 直接進(jìn)入insert函數(shù)體
586           { return _M_h.insert(std::move(__x)); }

這樣可以忽略中間構(gòu)造std::pair<int, int>{1,1}的行為瘪板,直接進(jìn)入insert函數(shù)中,使得調(diào)試更加清晰明了漆诽。

next [count]

next指令侮攀,簡寫是n锣枝,next指令是逐函數(shù)執(zhí)行,即當(dāng)停在斷點(diǎn)觸發(fā)的函數(shù)處:

  • step指令是逐步執(zhí)行魏身,下一步是會(huì)進(jìn)入函數(shù)體中
  • next指令會(huì)直接執(zhí)行完整個(gè)函數(shù)惊橱,然后進(jìn)入下一行

對(duì)于 step [count]中的演示demo,如果是next指令箭昵,會(huì)直接執(zhí)行完map.insert函數(shù)税朴,進(jìn)入下一行:

(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:6
6         map.insert({1,1});
(gdb) n     # 直接進(jìn)入下一行
7         return 0;
(gdb) 

stepnext合理的使用家制,控制調(diào)試的進(jìn)度正林,使得調(diào)試更加方便。

set step-mode

如果某個(gè)函數(shù)颤殴、語句沒有包含debug信息觅廓,gdb默認(rèn)就會(huì)跳過這個(gè)函數(shù)、語句涵但。但是杈绸,可以通過設(shè)置step-mode選項(xiàng)是否跳過:

  • set step-mode on:不跳過沒有調(diào)試信息的函數(shù)、語句
  • set step-mode off:默認(rèn)行為矮瘟,跳過

可以通過show step-mmode來查看:

(gdb) show step-mode 
Mode of the step operation is off.

finish

finish指令瞳脓,簡寫fin,用于將當(dāng)前函數(shù)剩下的部分執(zhí)行完畢澈侠,并且顯示輸出結(jié)果劫侧。

int countSum(int from, int to) {
  int sum =0;
  
  for (int from = 0; from < to; from++) 
  {
    sum += from;
  }
  sum+=1;
  sum+=2;
  sum+=3;
  sum+=4;
  sum+=5;
  sum+=6;
  sum+=7;   

  return sum;   // 第16行
}

int main(int argc, char const *argv[]) {
 
  countSum(0, 10);
  return 0;
}

countSum函數(shù)處添加斷點(diǎn),當(dāng)該斷點(diǎn)觸發(fā)哨啃,執(zhí)行step指令進(jìn)入countSum函數(shù)烧栋。此時(shí)拳球,直接執(zhí)行finish指令邑跪,gdb會(huì)直接返回countSum的結(jié)果宋距,然后進(jìn)入下一行:

(gdb) break countSum(int, int) 
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s                     // 進(jìn)入函數(shù)體
2         int sum =0;
(gdb) finish 
Run till exit from #0  countSum (from=0, to=10) at main.cc:2
main (argc=1, argv=0x7ffffffedfb8) at main.cc:22
22        return 0;
Value returned is $1 = 73   // 直接執(zhí)行完诱篷,并返回結(jié)果

finish指令默認(rèn)會(huì)顯示函數(shù)的返回結(jié)果,也可以設(shè)置為不顯示琳省。不過既然是調(diào)試拢蛋,那么肯定是提供越多信息越好。

  • set print finish [on|off]:控制finish返回結(jié)果是否顯示
  • show print finish:輸出finish的返回結(jié)果是否顯示
(gdb) show print finish
Printing of return value after `finish' is on.

return

return皆警,指令與finish不同:

  • finish會(huì)把這個(gè)函數(shù)剩余的部分绸罗,正常運(yùn)行完后在返回育灸;
  • return指令儿子,是直接在函數(shù)的當(dāng)前位置返回,不管你執(zhí)行到什么位置砸喻。

很好理解柔逼,就是finish把函數(shù)完整地執(zhí)行完畢后返回蒋譬,return是函數(shù)執(zhí)行到某個(gè)位置,強(qiáng)行的返回愉适,而不管函數(shù)的后續(xù)犯助。

until [location]

until指令,簡寫u维咸,可以用于直接跳出循環(huán)體剂买。

比如上面的countSum函數(shù),進(jìn)入后腰湾,如果不想一直next單步執(zhí)行雷恃,就執(zhí)行until指令,會(huì)直接跳出for循環(huán)费坊。

until

until指令倒槐,不加上參數(shù),沒有遇到循環(huán)體時(shí)功能類似于next附井,遇到了可以直接跳出循環(huán)體

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) until             # 進(jìn)入函數(shù)
2         int sum =0;
(gdb) until             # 遇到循環(huán)體
4         for (int from = 0; from < to; from++) 
(gdb) until             # 直接執(zhí)行完循環(huán)體
6           sum += from;
(gdb) until
4         for (int from = 0; from < to; from++) 
(gdb) until             # 執(zhí)行完循環(huán)體
8         sum+=1;
(gdb) until
9         sum+=2;
(gdb) until
10        sum+=3;
(gdb) 
until location

until location指令中的location格式和break location的格式一樣讨越,可以是行數(shù)、函數(shù)名永毅。 可以直接運(yùn)行到指定行數(shù)把跨。

以上面的countSum為例:

(gdb) break countSum(int, int) 
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s                              # 進(jìn)入函數(shù)體
2         int sum =0;       
(gdb) until main.cc:16               # 一直執(zhí)行到 return sum; 語句
countSum (from=0, to=10) at main.cc:16
16        return sum;
(gdb) n                              # 下一條就是函數(shù)返回了
17      }

會(huì)發(fā)現(xiàn),直接運(yùn)行到指定的位置:countSum函數(shù)的return語句處沼死。

進(jìn)一步着逐,將main函數(shù)修改如下:

int main(int argc, char const *argv[]) {
 
  countSum(0, 10);
  countSum(10, 20);
  return 0;
}

如果我在執(zhí)行countSum(0,10)函數(shù)時(shí),突然想執(zhí)行完當(dāng)前函數(shù)意蛀,然后跳到轉(zhuǎn)countSum(10,20)函數(shù)中耸别,行不行呢?

當(dāng)然是可以县钥,可以借助until location指令實(shí)現(xiàn)秀姐。

(gdb) break countSum(int, int)                          # 先在countSum函數(shù)處加上斷點(diǎn)
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=10) at main.cc:1     # countSum(0, 10)第一次觸發(fā)
1       int countSum(int from, int to) {
(gdb) s                                                 # 進(jìn)入函數(shù)體
2         int sum =0;       
(gdb) until main.cc:22                                  # 直接執(zhí)行完當(dāng)前函數(shù),并跳轉(zhuǎn)到 countSum(10, 20)
main (argc=1, argv=0x7ffffffedfb8) at main.cc:22
22        countSum(10, 20);
(gdb) s

Breakpoint 1, countSum (from=0, to=20) at main.cc:1     # 直接執(zhí)行到countSum(10, 20)
1       int countSum(int from, int to) {
(gdb) s
2         int sum =0;
(gdb)

通過until指令若贮,可以很好的控制函數(shù)的指令流程省有。更多指令可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#Continuing-and-Stepping

顯示

查看程序中運(yùn)行時(shí)的變量的值,有兩種方式:

  • print指令:手動(dòng)輸出
  • display指令:自動(dòng)顯示

下面分別講解谴麦。

print

print指令蠢沿,簡寫p,其格式如下兩種匾效。

  • print [[options] --] expr
  • print [[options] --] /f expr
print [[options] --] expr

print [[options] --] expr搏予,其中expr可以是表達(dá)式、變量弧轧。其中雪侥,輸出的變量碗殷,要么是全局變量、static變量速缨,要么就是在當(dāng)前作用域內(nèi)可見的局部變量锌妻。

在多數(shù)情況下,print指令輸出的結(jié)果就符合要求旬牲,但是有時(shí)候?yàn)榱双@得更好的顯示仿粹,可以提供 options 選項(xiàng),獲得更好的輸出原茅。

比如吭历,對(duì)于下面的代碼,

int main(int argc, char const *argv[]) {
  
  std::vector<int> vec{1,2,3};
  return 0;
}

想要在gdb中顯示vec的內(nèi)容:

23        std::vector<int> vec{1,2,3};
(gdb) n
24        return 0;
(gdb) print vec             # 直接輸出
$1 = std::vector of length 3, capacity 3 = {1, 2, 3}
(gdb) set print array on    # 開啟數(shù)組顯示
(gdb) print vec             # 有更好的輸出顯示
$2 = std::vector of length 3, capacity 3 = {
  1,
  2,
  3
}
(gdb) 

對(duì)于printoption選項(xiàng)設(shè)置擂橘,具體可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Data.html#Data
print [[options] --] /f expr

print [[options] --] /f expr晌区,其中/fexpr是輸出格式:

    x  按十六進(jìn)制格式顯示變量
    d  按十進(jìn)制格式顯示變量
    u  按十六進(jìn)制格式顯示無符號(hào)整型
    o  按八進(jìn)制格式顯示變量
    t  按二進(jìn)制格式顯示變量
    a  按十六進(jìn)制格式顯示變量
    c  按字符格式顯示變量
    f  按浮點(diǎn)數(shù)格式顯示變量
    s  按字符串顯示
    z  與'x'格式一樣,該值被視為整數(shù)并被打印為十六進(jìn)制通贞,但是前導(dǎo)零被打印出來以便將該值填充為整數(shù)類型的大小
    r  'r'是'raw'的縮寫朗若,按照python的Pretty-printer風(fēng)格進(jìn)行打印

以上面的countSum函數(shù)為例,按照不同格式顯示返回值sum

(gdb) print sum 
$4 = 73
(gdb) print/a sum
$5 = 0x49
(gdb) print/c sum
$7 = 73 'I'
(gdb) p/x $pc       # 當(dāng)前指令指向的地址
$23 = 0x807c6fa

順便說下昌罩,$pc表示當(dāng)前指令地址哭懈,因此print/x $pc是以16進(jìn)制顯示當(dāng)前指令的地址。

關(guān)于輸出流格式信息茎用,原文參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Output-Formats.html#Output-Formats

display

print指令遣总,是手動(dòng)輸出表達(dá)、變量的值轨功。display可以讓指定的表達(dá)式彤避、變量在每次的單步執(zhí)行中自動(dòng)顯示。主要有以下三種使用方式:

display   expr
display/f expr
display/f addr
display /f expr

display /f expr 的使用夯辖,和print的格式基本一致。

比如董饰,在countSum函數(shù)中蒿褂,想要觀察變量的sum值,由于是在一個(gè)循環(huán)體中卒暂,一直使用print指令查看sum變量的值啄栓,不免過于麻煩。此時(shí)也祠,使用display指令來查看昙楚,使得gdb在運(yùn)行每條語句的時(shí)候都會(huì)顯示一次sum的值。

效果如下:

Breakpoint 1, countSum (from=0, to=134222349) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s
2         int sum =0;
(gdb) n
4         for (int from = 0; from < to; from++) 
(gdb) display sum   # display 指令
1: sum = 0
(gdb) n             # 每條指令都會(huì)顯示 sum 的值
6           sum += from;
1: sum = 0          # 每條指令都會(huì)顯示 sum 的值
(gdb) 
4         for (int from = 0; from < to; from++) 
1: sum = 0          # 每條指令都會(huì)顯示 sum 的值
(gdb) 
6           sum += from;
1: sum = 0
(gdb) 
4         for (int from = 0; from < to; from++) 
1: sum = 1
...
display /f addr

當(dāng)自動(dòng)顯示的是地址時(shí)诈嘿,可以使用/i格式描述符堪旧,查看地址 addr的匯編代碼削葱,$pc指向的當(dāng)前指令的地址。

因此display /i &pc這條指令淳梦,可以查看當(dāng)前指令對(duì)應(yīng)的匯編代碼析砸。

Breakpoint 1, countSum (from=0, to=134222349) at main.cc:1
1       int countSum(int from, int to) {
(gdb) display sum       # 設(shè)置自動(dòng)顯示 sum 變量
1: sum = 134222272
(gdb) display /i $pc    # 設(shè)置顯示當(dāng)前代碼的匯編
2: x/i $pc
=> 0x8001129 <countSum(int, int)>:      endbr64 
(gdb) n                 # 每一步都會(huì)顯示上面的兩個(gè)設(shè)置
2         int sum =0;
1: sum = 134222272
2: x/i $pc
=> 0x8001137 <countSum(int, int)+14>:   movl   $0x0,-0x8(%rbp)

關(guān)于輸出顯示的指令的輸出顯示信息,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Data.html#Data

棧幀

backtrace

backtrace指令爆袍,簡寫bt首繁,可以在break指令設(shè)置的斷點(diǎn)觸發(fā)時(shí),查看程序是怎么執(zhí)行到此斷點(diǎn)處的陨囊,追蹤下棧幀信息弦疮。

比如,在REDIS程序中蜘醋,setCommand函數(shù)處的斷點(diǎn)觸發(fā)時(shí)胁塞,想要看看REEDIS是怎么從main函數(shù)執(zhí)行到setCommand函數(shù)的,可以使用bt指令來追蹤下棧幀軌跡:

Thread 1 "redis-server" hit Breakpoint 1, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>)
    at t_string.c:99
99      void setCommand(client *c) {
(gdb) bt
#0  setCommand (c=0x7fffff11c680) at t_string.c:101
#1  0x000000000804a765 in call (c=0x7fffff11c680, flags=15) at server.c:3301
#2  0x000000000804b73c in processCommand (c=0x7fffff11c680) at server.c:3695
#3  0x000000000805e24f in processCommandAndResetClient (c=0x7fffff11c680) at networking.c:2057
#4  0x000000000805e4ae in processInputBuffer (c=0x7fffff11c680) at networking.c:2169
#5  0x000000000805e874 in readQueryFromClient (conn=0x7fffff015140) at networking.c:2275
#6  0x000000000810888b in callHandler (conn=0x7fffff015140, handler=0x805e52e <readQueryFromClient>) at connhelpers.h:79
#7  0x0000000008108f57 in connSocketEventHandler (el=0x7fffff00b480, fd=8, clientData=0x7fffff015140, mask=1) at connection.c:330
#8  0x0000000008040cad in aeProcessEvents (eventLoop=0x7fffff00b480, flags=27) at ae.c:497
#9  0x0000000008040eeb in aeMain (eventLoop=0x7fffff00b480) at ae.c:558
#10 0x000000000804fac3 in main (argc=1, argv=0x7ffffffedf48) at server.c:5236
(gdb) 

bt指令的輸出信息可以看出整個(gè)調(diào)用鏈堂湖,是如何從main函數(shù)執(zhí)行到setCommand函數(shù)的闲先,這對(duì)于理清項(xiàng)目框架至關(guān)重要,尤其是大量使用回調(diào)函數(shù)的項(xiàng)目中无蜂,比如REDIS伺糠、Libuv。

frame N

frame指令斥季,簡寫f训桶,frame N表示跳轉(zhuǎn)到編號(hào)為N的棧幀中,不加參數(shù)的frame 指令酣倾,可以顯示當(dāng)前棧幀的基本信息舵揭。

上面的bt指令,可以詳細(xì)地看到從main函數(shù)運(yùn)行到setCommnad函數(shù)的調(diào)用過程躁锡。但是午绳,如果我想看看其中某一個(gè)棧幀的調(diào)用過程,那怎么辦映之?

比如拦焚,現(xiàn)在我就想知道REDIS是怎么處理客戶端的請(qǐng)求的,想去processInputBuffer函數(shù)所在棧幀杠输,那么就如下操作:

(gdb) frame 5
#5  0x000000000805e874 in readQueryFromClient (conn=0x7fffff015140) at networking.c:2275
2275         processInputBuffer(c);
(gdb) s                             # 進(jìn)入 processInputBuffer 函數(shù)
101         robj *expire = NULL;                

frame NN 是調(diào)用 processInputBuffer 函數(shù)的棧幀赎败,即 processInputBuffer 函數(shù)的上一個(gè)棧幀,由于 processInputBuffer 函數(shù)是在 readQueryFromClient 函數(shù)中被調(diào)用蠢甲,因此要查看processInputBuffer函數(shù)僵刮,需要進(jìn)入readQueryFromClient所處的棧幀,因此 N=5

info frame

info frame指令搞糕,簡寫info f勇吊,會(huì)顯示當(dāng)前棧幀的詳細(xì)信息,比如:當(dāng)前調(diào)用函數(shù)的地址寞宫,被調(diào)用函數(shù)的地址萧福,源碼語言、函數(shù)參數(shù)地址及值辈赋、局部變量的地址等等鲫忍。

比如,當(dāng)前執(zhí)行到setCommand函數(shù)中钥屈,那么info f就可以查看當(dāng)前的棧幀:

(gdb) info frame
 Stack level 0, frame at 0x7ffffffedae0:    # 當(dāng)前函數(shù)棧幀地址  
 rip = 0x807c6fa in setCommand (t_string.c:101); saved rip = 0x804a765
 called by frame at 0x7ffffffedb60          # 當(dāng)前函數(shù)在哪里被調(diào)用的
 source language c.                         # c 語言寫的
 Arglist at 0x7ffffffeda78, 
 args: c=0x7fffff11c680                     # 函數(shù)參數(shù)
 Locals at 0x7ffffffeda78, Previous frame's sp is 0x7ffffffedae0
 Saved registers:
  rbx at 0x7ffffffedac8, rbp at 0x7ffffffedad0, rip at 0x7ffffffedad8

info args

info args指令悟民,可以獲取當(dāng)前棧幀函數(shù) setCommand 的參數(shù)名及其值。

setCommand 的原型是 setCommand(client *c) 篷就,其參數(shù)是指針類型射亏,因此獲得參數(shù)c值后,可以打印參數(shù)c指向的數(shù)據(jù)竭业。比如智润,現(xiàn)在想看看 setCommand 的參數(shù)c中的字段c->argv的第一個(gè)字符串是不是set

(gdb) info args 
c = 0x7fffff11c680                       # 和 info frame 顯示的地址一致
(gdb) print (const char*)((client*)0x7fffff11c680)->argv->ptr
$16 = 0x7fffff134d93 "set"               # 確實(shí)是set

info locals

打印出當(dāng)前函數(shù)中所有局部變量及其值。

(gdb) info locals 
j = 0
expire = 0x7fffff009031
unit = -75072
flags = 32767

關(guān)于棧幀的更多信息未辆,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Stack.html#Stack

補(bǔ)充

shell

如果想要在gdb環(huán)境中窟绷,執(zhí)行Linux命令,可以在指令前加上shell即可咐柜,比如clear命令兼蜈,在gdb下執(zhí)行為:

(gdb) shell clear

空行

在gdb下,直接回車拙友,即輸入一個(gè)空行为狸,相當(dāng)于重復(fù)執(zhí)行上一條指令。

比如遗契,在 setCommand 函數(shù)觸發(fā)時(shí):

Thread 1 "redis-server" hit Breakpoint 1, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>)
    at t_string.c:99
99      void setCommand(client *c) {
(gdb) s
101         robj *expire = NULL;                //* 超時(shí)時(shí)間
(gdb) n
102         int unit = UNIT_SECONDS;            //* 超時(shí)的時(shí)間單位
(gdb)           # 空行就是重復(fù)執(zhí)行 next
103         int flags = OBJ_SET_NO_FLAGS;       //* set 指令的類型
(gdb)           # 空行就是重復(fù)執(zhí)行 next
107         for (j = 3; j < c->argc; j++) {
(gdb) 

到此辐棒,常用的GDB指令基本講解完畢,如果能跟著走一遍牍蜂,已經(jīng)能完成大部分的調(diào)試任務(wù)漾根。更多的GDB指令,以及某些指令更深入的使用捷兰,比如print指令的輸出格式,可以去官方文檔學(xué)習(xí)负敏。

如果熟悉了gcc編譯贡茅、gdb調(diào)試,基本就可以卸載vscode里面的code runner插件,也免去了每次task.json等文件的繁瑣配置顶考,可以盡情地享受命令行帶來的便捷赁还、愉快。

此外驹沿,之后會(huì)準(zhǔn)備技術(shù)直播 《基于vscode使用gdb帶你理清REDIS-6.0框架》系列艘策,用gdb去理清REDIS服務(wù)器框架。gdb配合vscode效果奇佳渊季,在直播中可以更好展示朋蔫,請(qǐng)敬請(qǐng)期待。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末却汉,一起剝皮案震驚了整個(gè)濱河市驯妄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌合砂,老刑警劉巖青扔,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異翩伪,居然都是意外死亡微猖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門缘屹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凛剥,“玉大人,你說我怎么就攤上這事囊颅〉被冢” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵踢代,是天一觀的道長盲憎。 經(jīng)常有香客問我,道長胳挎,這世上最難降的妖魔是什么饼疙? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮慕爬,結(jié)果婚禮上窑眯,老公的妹妹穿的比我還像新娘。我一直安慰自己医窿,他們只是感情好磅甩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著姥卢,像睡著了一般卷要。 火紅的嫁衣襯著肌膚如雪渣聚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天僧叉,我揣著相機(jī)與錄音奕枝,去河邊找鬼。 笑死瓶堕,一個(gè)胖子當(dāng)著我的面吹牛隘道,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播郎笆,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谭梗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了题画?” 一聲冷哼從身側(cè)響起默辨,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苍息,沒想到半個(gè)月后缩幸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竞思,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年表谊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盖喷。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爆办,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出课梳,到底是詐尸還是另有隱情距辆,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布暮刃,位于F島的核電站跨算,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏椭懊。R本人自食惡果不足惜诸蚕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望氧猬。 院中可真熱鬧背犯,春花似錦、人聲如沸盅抚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妄均。三九已至柱锹,卻和暖如春破讨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奕纫。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烫沙,地道東北人匹层。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像锌蓄,于是被迫代替她去往敵國和親升筏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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