<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          GDB調(diào)試-從入門實踐到原理

          共 8661字,需瀏覽 18分鐘

           ·

          2022-01-13 16:14

          你好,我是雨樂!

          在上篇文章中,我們分析了線上coredump產(chǎn)生的原因,其中用到了coredump分析工具gdb,這幾天一直有讀者在問,能不能寫一篇關(guān)于gdb調(diào)試方面的文章,今天借助此文,分享一些工作中的調(diào)試經(jīng)驗,希望能夠幫到大家。

          寫在前面

          在我的工作經(jīng)歷中,前幾年在Windows上進行開發(fā),使用Visual Studio進行調(diào)試,簡直是利器,各種斷點等用鼠標點點點就能設(shè)置;大概從12年開始轉(zhuǎn)Linux開發(fā)了,所以調(diào)試都是基于GDB的。本來這篇文章也想寫寫Windows下調(diào)試相關(guān),奈何好多年沒用了,再加上工作太忙,所以本文就只寫了Linux下GDB調(diào)試相關(guān),對于Windows開發(fā)人員,實在對不住了??。

          這篇文章,涉及的比較全面,總結(jié)了這些年的gdb調(diào)試經(jīng)驗(都是小兒科??),經(jīng)常用到的一些調(diào)試技巧,希望能夠?qū)氖翷inux開發(fā)的相關(guān)人員有所幫助

          背景

          作為C/C++開發(fā)人員,保證程序正常運行是最基本也是最主要的目的。而為了保證程序正常運行,調(diào)試則是最基本的手段,熟悉這些調(diào)試方式,可以方便我們更快的定位程序問題所在,提高開發(fā)效率。

          在開發(fā)過程,如果程序的運行結(jié)果不符合預(yù)期,第一時間就是打開GDB進行調(diào)試,在對應(yīng)的地方設(shè)置斷點,然后分析原因;當線上服務(wù)出了問題,第一時間查看進程在不在,如果不在的話,是否生成了coredump文件,如果有,則使用gdb調(diào)試coredump文件,否則通過dmesg來分析內(nèi)核日志來查找原因。

          概念

          GDB是一個由GNU開源組織發(fā)布的、UNIX/LINUX操作系統(tǒng)下的、「基于命令行的、功能強大的程序調(diào)試工具」

          GDB支持斷點、單步執(zhí)行、打印變量、觀察變量、查看寄存器、查看堆棧等調(diào)試手段。在Linux環(huán)境軟件開發(fā)中,GDB是主要的調(diào)試工具,用來調(diào)試C和 C++程序(也支持go等其他語言)。

          常用命令

          斷點

          斷點是我們在調(diào)試中經(jīng)常用的一個功能,我們在指定位置設(shè)置斷點之后,程序運行到該位置將會暫停,這個時候我們就可以對程序進行更多的操作,比如查看變量內(nèi)容,堆棧情況等等,以幫助我們調(diào)試程序。

          以設(shè)置斷點的命令分為以下幾類:

          • breakpoint
          • watchpoint
          • catchpoint

          breakpoint

          可以根據(jù)行號、函數(shù)、條件生成斷點,下面是相關(guān)命令以及對應(yīng)的作用說明:

          命令作用
          break [file]:function在文件file的function函數(shù)入口設(shè)置斷點
          break [file]:line在文件file的第line行設(shè)置斷點
          info breakpoints查看斷點列表
          break [+-]offset在當前位置偏移量為[+-]offset處設(shè)置斷點
          break *addr在地址addr處設(shè)置斷點
          break ... if expr設(shè)置條件斷點,僅僅在條件滿足時
          ignore n count接下來對于編號為n的斷點忽略count次
          clear刪除所有斷點
          clear function刪除所有位于function內(nèi)的斷點
          delete n刪除指定編號的斷點
          enable n啟用指定編號的斷點
          disable n禁用指定編號的斷點
          save breakpoints file保存斷點信息到指定文件
          source file導(dǎo)入文件中保存的斷點信息
          break在下一個指令處設(shè)置斷點
          clear [file:]line刪除第line行的斷點

          watchpoint

          watchpoint是一種特殊類型的斷點,類似于正常斷點,是要求GDB暫停程序執(zhí)行的命令。區(qū)別在于watchpoint沒有駐留某一行源代碼中,而是指示GDB每當某個表達式改變了值就暫停執(zhí)行的命令。

          watchpoint分為硬件實現(xiàn)和軟件實現(xiàn)兩種。前者需要硬件系統(tǒng)的支持;后者的原理就是每步執(zhí)行后都檢查變量的值是否改變。GDB在新建數(shù)據(jù)斷點時會優(yōu)先嘗試硬件方式,如果失敗再嘗試軟件實現(xiàn)。

          命令作用
          watch variable設(shè)置變量數(shù)據(jù)斷點
          watch var1 + var2設(shè)置表達式數(shù)據(jù)斷點
          rwatch variable設(shè)置讀斷點,僅支持硬件實現(xiàn)
          awatch variable設(shè)置讀寫斷點,僅支持硬件實現(xiàn)
          info watchpoints查看數(shù)據(jù)斷點列表
          set can-use-hw-watchpoints 0強制基于軟件方式實現(xiàn)

          使用數(shù)據(jù)斷點時,需要注意:

          • 當監(jiān)控變量為局部變量時,一旦局部變量失效,數(shù)據(jù)斷點也會失效
          • 如果監(jiān)控的是指針變量p,則watch *p監(jiān)控的是p所指內(nèi)存數(shù)據(jù)的變化情況,而watch p監(jiān)控的是p指針本身有沒有改變指向

          最常見的數(shù)據(jù)斷點應(yīng)用場景:「定位堆上的結(jié)構(gòu)體內(nèi)部成員何時被修改」。由于指針一般為局部變量,為了解決斷點失效,一般有兩種方法。

          命令作用
          print &variable查看變量的內(nèi)存地址
          watch *(type *)address通過內(nèi)存地址間接設(shè)置斷點
          watch -l variable指定location參數(shù)
          watch variable thread 1僅編號為1的線程修改變量var值時會中斷

          catchpoint

          從字面意思理解,是捕獲斷點,其主要監(jiān)測信號的產(chǎn)生。例如c++的throw,或者加載庫的時候,產(chǎn)生斷點行為。

          命令含義
          catch fork程序調(diào)用fork時中斷
          tcatch fork設(shè)置的斷點只觸發(fā)一次,之后被自動刪除
          catch syscall ptrace為ptrace系統(tǒng)調(diào)用設(shè)置斷點
          ?

          command命令后加斷點編號,可以定義斷點觸發(fā)后想要執(zhí)行的操作。在一些高級的自動化調(diào)試場景中可能會用到。

          ?

          命令行

          命令作用
          run arglist以arglist為參數(shù)列表運行程序
          set args arglist指定啟動命令行參數(shù)
          set args指定空的參數(shù)列表
          show args打印命令行列表

          程序棧

          命令作用
          backtrace [n]打印棧幀
          frame [n]選擇第n個棧幀,如果不存在,則打印當前棧幀
          up n選擇當前棧幀編號+n的棧幀
          down n選擇當前棧幀編號-n的棧幀
          info frame [addr]描述當前選擇的棧幀
          info args當前棧幀的參數(shù)列表
          info locals當前棧幀的局部變量

          多進程、多線程

          多進程

          GDB在調(diào)試多進程程序(程序含fork調(diào)用)時,默認只追蹤父進程。可以通過命令設(shè)置,實現(xiàn)只追蹤父進程或子進程,或者同時調(diào)試父進程和子進程。

          命令作用
          info inferiors查看進程列表
          attach pid綁定進程id
          inferior num切換到指定進程上進行調(diào)試
          print $_exitcode顯示程序退出時的返回值
          set follow-fork-mode child追蹤子進程
          set follow-fork-mode parent追蹤父進程
          set detach-on-fork onfork調(diào)用時只追蹤其中一個進程
          set detach-on-fork offfork調(diào)用時會同時追蹤父子進程

          在調(diào)試多進程程序時候,默認情況下,除了當前調(diào)試的進程,其他進程都處于掛起狀態(tài),所以,如果需要在調(diào)試當前進程的時候,其他進程也能正常執(zhí)行,那么通過設(shè)置set schedule-multiple on即可。

          多線程

          多線程開發(fā)在日常開發(fā)工作中很常見,所以多線程的調(diào)試技巧非常有必要掌握。

          默認調(diào)試多線程時,一旦程序中斷,所有線程都將暫停。如果此時再繼續(xù)執(zhí)行當前線程,其他線程也會同時執(zhí)行。

          命令作用
          info threads查看線程列表
          print $_thread顯示當前正在調(diào)試的線程編號
          set scheduler-locking on調(diào)試一個線程時,其他線程暫停執(zhí)行
          set scheduler-locking off調(diào)試一個線程時,其他線程同步執(zhí)行
          set scheduler-locking step僅用step調(diào)試線程時其他線程不執(zhí)行,用其他命令如next調(diào)試時仍執(zhí)行

          如果只關(guān)心當前線程,建議臨時設(shè)置 scheduler-lockingon,避免其他線程同時運行,導(dǎo)致命中其他斷點分散注意力。

          打印輸出

          通常情況下,在調(diào)試的過程中,我們需要查看某個變量的值,以分析其是否符合預(yù)期,這個時候就需要打印輸出變量值。

          命令作用
          whatis variable查看變量的類型
          ptype variable查看變量詳細的類型信息
          info variables var查看定義該變量的文件,不支持局部變量
          打印字符串

          使用x/s命令打印ASCII字符串,如果是寬字符字符串,需要先看寬字符的長度 print sizeof(str)

          如果長度為2,則使用x/hs打印;如果長度為4,則使用x/ws打印。

          命令作用
          x/s str打印字符串
          set print elements 0打印不限制字符串長度/或不限制數(shù)組長度
          call printf("%s\n",xxx)這時打印出的字符串不會含有多余的轉(zhuǎn)義符
          printf "%s\n",xxx同上
          打印數(shù)組
          命令作用
          print *array@10打印從數(shù)組開頭連續(xù)10個元素的值
          print array[60]@10打印array數(shù)組下標從60開始的10個元素,即第60~69個元素
          set print array-indexes on打印數(shù)組元素時,同時打印數(shù)組的下標
          打印指針
          命令作用
          print ptr查看該指針指向的類型及指針地址
          print *(struct xxx *)ptr查看指向的結(jié)構(gòu)體的內(nèi)容
          打印指定內(nèi)存地址的值

          使用x命令來打印內(nèi)存的值,格式為x/nfu addr,以f格式打印從addr開始的n個長度單元為u的內(nèi)存值。

          • n:輸出單元的個數(shù)
          • f:輸出格式,如x表示以16進制輸出,o表示以8進制輸出,默認為x
          • u:一個單元的長度,b表示1byteh表示2bytehalf word),w表示4byteg表示8bytegiant word
          命令作用
          x/8xb array以16進制打印數(shù)組array的前8個byte的值
          x/8xw array以16進制打印數(shù)組array的前16個word的值
          打印局部變量
          命令作用
          info locals打印當前函數(shù)局部變量的值
          backtrace full打印當前棧幀各個函數(shù)的局部變量值,命令可縮寫為bt
          bt full n從內(nèi)到外顯示n個棧幀及其局部變量
          bt full -n從外向內(nèi)顯示n個棧幀及其局部變量
          打印結(jié)構(gòu)體
          命令作用
          set print pretty on每行只顯示結(jié)構(gòu)體的一名成員
          set print null-stop不顯示'\000'這種

          函數(shù)跳轉(zhuǎn)

          命令作用
          set step-mode on不跳過不含調(diào)試信息的函數(shù),可以顯示和調(diào)試匯編代碼
          finish執(zhí)行完當前函數(shù)并打印返回值,然后觸發(fā)中斷
          return 0不再執(zhí)行后面的指令,直接返回,可以指定返回值
          call printf("%s\n", str)調(diào)用printf函數(shù),打印字符串(可以使用call或者print調(diào)用函數(shù))
          print func()調(diào)用func函數(shù)(可以使用call或者print調(diào)用函數(shù))
          set var variable=xxx設(shè)置變量variable的值為xxx
          set {type}address = xxx給存儲地址為address,類型為type的變量賦值
          info frame顯示函數(shù)堆棧的信息(堆棧幀地址、指令寄存器的值等)

          其它

          圖形化

          tui為terminal user interface的縮寫,在啟動時候指定-tui參數(shù),或者調(diào)試時使用ctrl+x+a組合鍵,可進入或退出圖形化界面。

          命令含義
          layout src顯示源碼窗口
          layout asm顯示匯編窗口
          layout split顯示源碼 + 匯編窗口
          layout regs顯示寄存器 + 源碼或匯編窗口
          winheight src +5源碼窗口高度增加5行
          winheight asm -5匯編窗口高度減小5行
          winheight cmd +5控制臺窗口高度增加5行
          winheight regs -5寄存器窗口高度減小5行

          匯編

          命令含義
          disassemble function查看函數(shù)的匯編代碼
          disassemble /mr function同時比較函數(shù)源代碼和匯編代碼

          調(diào)試和保存core文件

          命令含義
          file exec_file ?*# *加載可執(zhí)行文件的符號表信息
          core core_file加載core-dump文件
          gcore core_file生成core-dump文件,記錄當前進程的狀態(tài)

          啟動方式

          使用gdb調(diào)試,一般有以下幾種啟動方式:

          • gdb filename: 調(diào)試可執(zhí)行程序
          • gdb attach pid: 通過”綁定“進程ID來調(diào)試正在運行的進程
          • gdb filename -c coredump_file: 調(diào)試可執(zhí)行文件

          在下面的幾節(jié)中,將分別對上述幾種調(diào)試方式進行講解,從例子的角度出發(fā),使得大家能夠更好的掌握調(diào)試技巧。

          調(diào)試

          可執(zhí)行文件

          單線程

          首先,我們先看一段代碼:

          #include

          void?print(int?xx,?int?*xxptr)?{
          ??printf("In?print():\n");
          ??printf("???xx?is?%d?and?is?stored?at?%p.\n",?xx,?&xx);
          ??printf("???ptr?points?to?%p?which?holds?%d.\n",?xxptr,?*xxptr);
          }

          int?main(void)?{
          ??int?x?=?10;
          ??int?*ptr?=?&x;
          ??printf("In?main():\n");
          ??printf("???x?is?%d?and?is?stored?at?%p.\n",?x,?&x);
          ??printf("???ptr?points?to?%p?which?holds?%d.\n",?ptr,?*ptr);
          ??print(x,?ptr);
          ??return?0;
          }

          這個代碼比較簡單,下面我們開始進入調(diào)試:

          gdb?./test_main
          GNU?gdb?(GDB)?Red?Hat?Enterprise?Linux?7.6.1-114.el7
          Copyright?(C)?2013?Free?Software?Foundation,?Inc.
          License?GPLv3+:?GNU?GPL?version?3?or?later?
          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-redhat-linux-gnu".
          For?bug?reporting?instructions,?please?see:
          ...
          Reading?symbols?from?/root/test_main...done.
          (gdb)?r
          Starting?program:?/root/./test_main
          In?main():
          ???x?is?10?and?is?stored?at?0x7fffffffe424.
          ???ptr?points?to?0x7fffffffe424?which?holds?10.
          In?print():
          ???xx?is?10?and?is?stored?at?0x7fffffffe40c.
          ???xxptr?points?to?0x7fffffffe424?which?holds?10.
          [Inferior?1?(process?31518)?exited?normally]
          Missing?separate?debuginfos,?use:?debuginfo-install?glibc-2.17-260.el7.x86_64

          在上述命令中,我們通過gdb test命令啟動調(diào)試,然后通過執(zhí)行r(run命令的縮寫)執(zhí)行程序,直至退出,換句話說,上述命令是一個完整的使用gdb運行可執(zhí)行程序的完整過程(只使用了r命令),接下來,我們將以此為例子,介紹幾種比較常見的命令。

          斷點
          (gdb)?b?15
          Breakpoint?1?at?0x400601:?file?test_main.cc,?line?15.
          (gdb)?info?b
          Num?????Type???????????Disp?Enb?Address????????????What
          1???????breakpoint?????keep?y???0x0000000000400601?in?main()?at?test_main.cc:15
          (gdb)?r
          Starting?program:?/root/./test_main
          In?main():
          ???x?is?10?and?is?stored?at?0x7fffffffe424.
          ???ptr?points?to?0x7fffffffe424?which?holds?10.

          Breakpoint?1,?main?()?at?test_main.cc:15
          15???print(xx,?xxptr)
          ;
          Missing?separate?debuginfos,?use:?debuginfo-install?glibc-2.17-260.el7.x86_64
          (gdb)
          backtrace
          (gdb)?backtrace
          #0??main?()?at?test_main.cc:15
          (gdb)

          backtrace命令是列出當前堆棧中的所有幀。在上面的例子中,棧上只有一幀,編號為0,屬于main函數(shù)。

          (gdb)?step
          print?(xx=10,?xxptr=0x7fffffffe424)?at?test_main.cc:4
          4???printf("In?print():\n");
          (gdb)

          接著,我們執(zhí)行了step命令,即進入函數(shù)內(nèi)。下面我們繼續(xù)通過backtrace命令來查看棧幀信息。

          (gdb)?backtrace
          #0??print?(xx=10,?xxptr=0x7fffffffe424)?at?test_main.cc:4
          #1??0x0000000000400612?in?main?()?at?test_main.cc:15
          (gdb)

          從上面輸出結(jié)果,我們能夠看出,有兩個棧幀,第1幀屬于main函數(shù),第0幀屬于print函數(shù)。

          每個棧幀都列出了該函數(shù)的參數(shù)列表。從上面我們可以看出,main函數(shù)沒有參數(shù),而print函數(shù)有參數(shù),并且顯示了其參數(shù)的值。

          有一點我們可能比較迷惑,在第一次執(zhí)行backtrace的時候,main函數(shù)所在的棧幀編號為0,而第二次執(zhí)行的時候,main函數(shù)的棧幀為1,而print函數(shù)的棧幀為0,這是因為_與棧的向下增長_規(guī)律一致,我們只需要記住_編號最小幀號就是最近一次調(diào)用的函數(shù)_。

          frame

          棧幀用來存儲函數(shù)的變量值等信息,默認情況下,GDB總是位于當前正在執(zhí)行函數(shù)對應(yīng)棧幀的上下文中。

          在前面的例子中,由于當前正在print()函數(shù)中執(zhí)行,GDB位于第0幀的上下文中。可以通過frame命令來獲取當前正在執(zhí)行的上下文所在的幀

          (gdb)?frame
          #0??print?(xx=10,?xxptr=0x7fffffffe424)?at?test_main.cc:4
          4???printf("In?print():\n");
          (gdb)

          下面,我們嘗試使用print命令打印下當前棧幀的值,如下:

          (gdb)?print?xx
          $1?=?10
          (gdb)?print?xxptr
          $2?=?(int?*)?0x7fffffffe424
          (gdb)

          如果我們想看其他棧幀的內(nèi)容呢?比如main函數(shù)中x和ptr的信息呢?假如直接打印這倆值的話,那么就會得到如下:

          (gdb)?print?x
          No?symbol?"x"?in?current?context.
          (gdb)?print?xxptr
          No?symbol?"ptr"?in?current?context.
          (gdb)

          在此,我們可以通過_frame num_來切換棧幀,如下:

          (gdb)?frame?1
          #1??0x0000000000400612?in?main?()?at?test_main.cc:15
          15???print(x,?ptr);
          (gdb)?print?x
          $3?=?10
          (gdb)?print?ptr
          $4?=?(int?*)?0x7fffffffe424
          (gdb)

          多線程

          為了方便進行演示,我們創(chuàng)建一個簡單的例子,代碼如下:

          #include?
          #include?
          #include?
          #include?
          #include?

          int?fun_int(int?n)?{
          ??std::this_thread::sleep_for(std::chrono::seconds(10));
          ??std::cout?<"in?fun_int?n?=?"?<std::endl;
          ??
          ??return?0;
          }

          int?fun_string(const?std::string?&s)?{
          ??std::this_thread::sleep_for(std::chrono::seconds(10));
          ??std::cout?<"in?fun_string?s?=?"?<std::endl;
          ??
          ??return?0;
          }

          int?main()?{
          ??std::vector<int>?v;
          ??v.emplace_back(1);
          ??v.emplace_back(2);
          ??v.emplace_back(3);

          ??std::cout?<std::endl;

          ??std::thread?t1(fun_int,?1);
          ??std::thread?t2(fun_string,?"test");

          ??std::cout?<"after?thread?create"?<std::endl;
          ??t1.join();
          ??t2.join();
          ??return?0;
          }

          上述代碼比較簡單:

          • 函數(shù)fun_int的功能是休眠10s,然后打印其參數(shù)
          • 函數(shù)fun_string功能是休眠10s,然后打印其參數(shù)
          • main函數(shù)中,創(chuàng)建兩個線程,分別執(zhí)行上述兩個函數(shù)

          下面是一個完整的調(diào)試過程:

          (gdb)?b?27
          Breakpoint?1?at?0x4013d5:?file?test.cc,?line?27.
          (gdb)?b?test.cc:32
          Breakpoint?2?at?0x40142d:?file?test.cc,?line?32.
          (gdb)?info?b
          Num?????Type???????????Disp?Enb?Address????????????What
          1???????breakpoint?????keep?y???0x00000000004013d5?in?main()?at?test.cc:27
          2???????breakpoint?????keep?y???0x000000000040142d?in?main()?at?test.cc:32
          (gdb)?r
          Starting?program:?/root/test
          [Thread?debugging?using?libthread_db?enabled]
          Using?host?libthread_db?library?"/lib64/libthread_db.so.1".

          Breakpoint?1,?main?()?at?test.cc:27
          (gdb)?c
          Continuing.
          3
          [New?Thread?0x7ffff6fd2700?(LWP?44996)]
          in?fun_int?n?
          =?1
          [New?Thread?0x7ffff67d1700?(LWP?44997)]

          Breakpoint?2,?main?()?at?test.cc:32
          32???std::cout?<"after?thread?create"?<std::endl;
          (gdb)?info?threads
          ??Id???Target?Id?????????Frame
          ??3????Thread?0x7ffff67d1700?(LWP?44997)?"test"?0x00007ffff7051fc3?in?new_heap?()?from?/lib64/libc.so.6
          ??2????Thread?0x7ffff6fd2700?(LWP?44996)?"test"?0x00007ffff7097e2d?in?nanosleep?()?from?/lib64/libc.so.6
          *?1????Thread?0x7ffff7fe7740?(LWP?44987)?"test"?main?()?at?test.cc:32
          (gdb)?thread?2
          [Switching?to?thread?2?(Thread?0x7ffff6fd2700?(LWP?44996))]
          #0??0x00007ffff7097e2d?in?nanosleep?()?from?/lib64/libc.so.6
          (gdb)?bt
          #0??0x00007ffff7097e2d?in?nanosleep?()?from?/lib64/libc.so.6
          #1??0x00007ffff7097cc4?in?sleep?()?from?/lib64/libc.so.6
          #2??0x00007ffff796ceb9?in?std::this_thread::__sleep_for(std::chrono::duration<long,?std::ratio<1l,?1l>?>,?std::chrono::duration<long,?std::ratio<1l,?1000000000l>?>)?()?from?/lib64/libstdc++.so.6
          #3??0x00000000004018cc?in?std::this_thread::sleep_for<long,?std::ratio<1l,?1l>?>?(__rtime=...)?at?/usr/include/c++/4.8.2/thread:281
          #4??0x0000000000401307?in?fun_int?(n=1)?at?test.cc:9
          #5??0x0000000000404696?in?std::_Bind_simple<int?(*(int))(int)>::_M_invoke<0ul>(std::_Index_tuple<0ul>)?(this=0x609080)
          ????at?/usr/include/c++/4.8.2/functional:1732
          #6??0x000000000040443d?in?std::_Bind_simple<int?(*(int))(int)>::operator()()?(this=0x609080)?at?/usr/include/c++/4.8.2/functional:1720
          #7??0x000000000040436e?in?std::thread::_Impl<std::_Bind_simple<int?(*(int))(int)>?>::_M_run()?(this=0x609068)?at?/usr/include/c++/4.8.2/thread:115
          #8??0x00007ffff796d070?in????()?from?/lib64/libstdc++.so.6
          #9??0x00007ffff7bc6dd5?in?start_thread?()?from?/lib64/libpthread.so.0
          #10?0x00007ffff70d0ead?in?clone?()?from?/lib64/libc.so.6
          (gdb)?c
          Continuing.
          after?thread?create
          in?fun_int?n?
          =?1
          [Thread?0x7ffff6fd2700?(LWP?45234)?exited]
          in?fun_string?s?=?test
          [Thread?0x7ffff67d1700?(LWP?45235)?exited]
          [Inferior?1?(process?45230)?exited?normally]
          (gdb)?q

          在上述調(diào)試過程中:

          1. b 27 在第27行加上斷點

          2. b test.cc:32 在第32行加上斷點(效果與b 32一致)

          3. info b 輸出所有的斷點信息

          4. r 程序開始運行,并在第一個斷點處暫停

          5. c 執(zhí)行c命令,在第二個斷點處暫停,在第一個斷點和第二個斷點之間,創(chuàng)建了兩個線程t1和t2

          6. info threads 輸出所有的線程信息,從輸出上可以看出,總共有3個線程,分別為main線程、t1和t2

          7. thread 2 切換至線程2

          8. bt 輸出線程2的堆棧信息

          9. c 直至程序結(jié)束

          10. q 退出gdb

          多進程

          同上面一樣,我們?nèi)匀灰砸粋€例子進行模擬多進程調(diào)試,代碼如下:

          #include?
          #include?

          int?main()
          {
          ????pid_t?pid?=?fork();
          ????if?(pid?==?-1)?{
          ???????perror("fork?error\n");
          ???????return?-1;
          ????}
          ??
          ????if(pid?==?0)?{?//?子進程
          ????????int?num?=?1;
          ????????while(num?==?1){
          ??????????sleep(10);
          ?????????}
          ????????printf("this?is?child,pid?=?%d\n",?getpid());
          ????}?else?{?//?父進程
          ????????printf("this?is?parent,pid?=?%d\n",?getpid());
          ??????wait(NULL);?//?等待子進程退出
          ????}
          ????return?0;
          }

          在上面代碼中,包含兩個進程,一個是父進程(也就是main進程),另外一個是由fork()函數(shù)創(chuàng)建的子進程。

          在默認情況下,在多進程程序中,GDB只調(diào)試main進程,也就是說無論程序調(diào)用了多少次fork()函數(shù)創(chuàng)建了多少個子進程,GDB在默認情況下,只調(diào)試父進程。為了支持多進程調(diào)試,從GDB版本7.0開始支持單獨調(diào)試(調(diào)試父進程或者子進程)和同時調(diào)試多個進程。

          那么,我們該如何調(diào)試子進程呢?我們可以使用如下幾種方式進行子進程調(diào)試。

          attach

          首先,無論是父進程還是子進程,都可以通過attach命令啟動gdb進行調(diào)試。我們都知道,對于每個正在運行的程序,操作系統(tǒng)都會為其分配一個唯一ID號,也就是進程ID。如果我們知道了進程ID,就可以使用attach命令對其進行調(diào)試了。

          在上面代碼中,fork()函數(shù)創(chuàng)建的子進程內(nèi)部,首先會進入while循環(huán)sleep,然后在while循環(huán)之后調(diào)用printf函數(shù)。這樣做的目的有如下:

          • 幫助attach捕獲要調(diào)試的進程id
          • 在使用gdb進行調(diào)試的時候,真正的代碼(即print函數(shù))沒有被執(zhí)行,這樣就可以從頭開始對子進程進行調(diào)試
          ?

          可能會有疑惑,上面代碼以及進入while循環(huán),無論如何是不會執(zhí)行到下面printf函數(shù)。其實,這就是gdb的厲害之處,可以通過gdb命令修改num的值,以便其跳出while循環(huán)

          ?

          使用如下命令編譯生成可執(zhí)行文件test_process

          g++?-g?test_process.cc?-o?test_process

          現(xiàn)在,我們開始嘗試啟動調(diào)試。

          gdb?-q?./test_process
          Reading?symbols?from?/root/test_process...done.
          (gdb)

          這里需要說明下,之所以加-q選項,是想去掉其他不必要的輸出,q為quite的縮寫。

          (gdb)?r
          Starting?program:?/root/./test_process
          Detaching?after?fork?from?child?process?37482.
          this?is?parent,pid?=?37478
          [Inferior?1?(process?37478)?exited?normally]
          Missing?separate?debuginfos,?use:?debuginfo-install?glibc-2.17-260.el7.x86_64?libgcc-4.8.5-36.el7.x86_64?libstdc++-4.8.5-36.el7.x86_64
          (gdb)?attach?37482
          //符號類輸出,此處略去
          (gdb)?n
          Single?stepping?until?exit?from?function?__nanosleep_nocancel,
          which?has?no?line?number?information.
          0x00007ffff72b3cc4?in?sleep?()?from?/lib64/libc.so.6
          (gdb)
          Single?stepping?until?exit?from?function?sleep,
          which?has?no?line?number?information.
          main?()?at?test_process.cc:8
          8???????while(num==10){
          (gdb)

          在上述命令中,我們執(zhí)行了n(next的縮寫),使其重新對while循環(huán)的判斷體進行判斷。

          (gdb)?set?num?=?1
          (gdb)?n
          12???????printf("this?is?child,pid?=?%d\n",getpid());
          (gdb)?c
          Continuing.
          this?is?child,pid?=?37482
          [Inferior?1?(process?37482)?exited?normally]
          (gdb)

          為了退出while循環(huán),我們使用set命令設(shè)置了num的值為1,這樣條件就會失效退出while循環(huán),進而執(zhí)行下面的printf()函數(shù);在最后我們執(zhí)行了c(continue的縮寫)命令,支持程序退出。

          ?

          如果程序正在正常運行,出現(xiàn)了死鎖等現(xiàn)象,則可以通過ps獲取進程ID,然后根據(jù)gdb attach pid進行綁定,進而查看堆棧信息

          ?
          指定進程

          默認情況下,GDB調(diào)試多進程程序時候,只調(diào)試父進程。GDB提供了兩個命令,可以通過follow-fork-mode和detach-on-fork來指定調(diào)試父進程還是子進程。

          follow-fork-mode

          該命令的使用方式為:

          (gdb)?set?follow-fork-mode?mode

          其中,mode有以下兩個選項:

          • parent:父進程,mode的默認選項
          • child:子進程,其目的是告訴 gdb 在目標應(yīng)用調(diào)用fork之后接著調(diào)試子進程而不是父進程,因為在Linux系統(tǒng)中fork()系統(tǒng)調(diào)用成功會返回兩次,一次在父進程,一次在子進程
          (gdb)?show?follow-fork-mode
          Debugger?response?to?a?program?call?of?fork?or?vfork?is?"parent".
          (gdb)?set?follow-fork-mode?child
          (gdb)?r
          Starting?program:?/root/./test_process
          [New?process?37830]
          this?is?parent,pid?=?37826

          ^C
          Program?received?signal?SIGINT,?Interrupt.
          [Switching?to?process?37830]
          0x00007ffff72b3e10?in?__nanosleep_nocancel?()?from?/lib64/libc.so.6
          Missing?separate?debuginfos,?use:?debuginfo-install?glibc-2.17-260.el7.x86_64?libgcc-4.8.5-36.el7.x86_64?libstdc++-4.8.5-36.el7.x86_64
          (gdb)?n
          Single?stepping?until?exit?from?function?__nanosleep_nocancel,
          which?has?no?line?number?information.
          0x00007ffff72b3cc4?in?sleep?()?from?/lib64/libc.so.6
          (gdb)?n
          Single?stepping?until?exit?from?function?sleep,
          which?has?no?line?number?information.
          main?()?at?test_process.cc:8
          8???????while(num==10){
          (gdb)?show?follow-fork-mode
          Debugger?response?to?a?program?call?of?fork?or?vfork?is?"child".
          (gdb)

          在上述命令中,我們做了如下操作:

          1. show follow-fork-mode:通過該命令來查看當前處于什么模式下,通過輸出可以看出,處于parent即父進程模式
          2. set follow-fork-mode child:指定調(diào)試子進程模式
          3. r:運行程序,直接運行程序,此時會進入子進程,然后執(zhí)行while循環(huán)
          4. ctrl + c:通過該命令,可以使得GDB收到SIGINT命令,從而暫停執(zhí)行while循環(huán)
          5. n(next):繼續(xù)執(zhí)行,進而進入到while循環(huán)的條件判斷處
          6. show follow-fork-mode:再次執(zhí)行該命令,通過輸出可以看出,當前處于child模式下
          detach-on-fork

          如果一開始指定要調(diào)試子進程還是父進程,那么使用follow-fork-mode命令完全可以滿足需求;但是如果想在調(diào)試過程中,想根據(jù)實際情況在父進程和子進程之間來回切換調(diào)試呢?

          GDB提供了另外一個命令:

          (gdb)?set?detach-on-fork?mode

          其中mode有如下兩個值:

          on:默認值,即表明只調(diào)試一個進程,可以是子進程,也可以是父進程

          off:程序中的每個進程都會被記錄,進而我們可以對所有的進程進行調(diào)試

          如果選擇關(guān)閉detach-on-fork模式(mode為off),那么GDB將保留對所有被fork出來的進程控制,即可用調(diào)試所有被fork出來的進程。可用 使用info forks命令列出所有的可被GDB調(diào)試的fork進程,并可用使用fork命令從一個fork進程切換到另一個fork進程。

          • info forks: 打印DGB控制下的所有被fork出來的進程列表。該列表包括fork id、進程id和當前進程的位置
          • fork fork-id: 參數(shù)fork-id是GDB分配的內(nèi)部fork編號,該編號可用通過上面的命令info forks獲取

          coredump

          當我們開發(fā)或者使用一個程序時候,最怕的莫過于程序莫名其妙崩潰。為了分析崩潰產(chǎn)生的原因,操作系統(tǒng)的內(nèi)存內(nèi)容(包括程序崩潰時候的堆棧等信息)會在程序崩潰的時候dump出來(默認情況下,這個文件名為core.pid,其中pid為進程id),這個dump操作叫做coredump(核心轉(zhuǎn)儲),然后我們可以用調(diào)試器調(diào)試此文件,以還原程序崩潰時候的場景。

          在我們分析如果用gdb調(diào)試coredump文件之前,先需要生成一個coredump,為了簡單起見,我們就用如下例子來生成:

          #include?

          void?print(int?*v,?int?size)?{
          ??for?(int?i?=?0;?i?????printf("elem[%d]?=?%d\n",?i,?v[i]);
          ??}
          }

          int?main()?{
          ??int?v[]?=?{0,?1,?2,?3,?4};
          ??print(v,?1000);
          ??return?0;
          }

          編譯并運行該程序:

          g++?-g?test_core.cc?-o?test_core
          ./test_core

          輸出如下:

          elem[775]?=?1702113070
          elem[776]?=?1667200115
          elem[777]?=?6648431
          elem[778]?=?0
          elem[779]?=?0
          段錯誤(吐核)

          如我們預(yù)期,程序產(chǎn)生了異常,但是卻沒有生成coredump文件,這是因為在系統(tǒng)默認情況下,coredump生成是關(guān)閉的,所以需要設(shè)置對應(yīng)的選項以打開coredump生成。

          針對多線程程序產(chǎn)生的coredump,有時候其堆棧信息并不能完整的去分析原因,這就使得我們得有其他方式。

          18年有一次線上故障,在測試環(huán)境一切正常,但是在線上的時候,就會coredump,根據(jù)gdb調(diào)試coredump,只能定位到了libcurl里面,但卻定位不出原因,用了大概兩天的時間,發(fā)現(xiàn)只有在超時的時候,才會coredump,而測試環(huán)境因為配置比較差超時設(shè)置的是20ms,而線上是5ms,知道coredump原因后,采用逐步定位縮小范圍法,逐步縮小代碼范圍,最終定位到是libcurl一個bug導(dǎo)致。所以,很多時候,定位線上問題需要結(jié)合實際情況,采取合適的方法來定位問題。

          配置

          配置coredump生成,有臨時配置(退出終端后,配置失效)和永久配置兩種。

          臨時

          通過ulimit -a可以判斷當前有沒有配置coredump生成:

          ulimit?-a
          core?file?size??????????(blocks,?-c)?0
          data?seg?size???????????(kbytes,?-d)?unlimited
          scheduling?priority?????????????(-e)?0

          從上面輸出可以看出core file size后面的數(shù)為0,即不生成coredump文件,我們可以通過如下命令進行設(shè)置

          ulimit?-c?size

          其中size為允許生成的coredump大小,這個一般盡量設(shè)置大點,以防止生成的coredump信息不全,筆者一般設(shè)置為不限。

          ulimit?-c?unlimited

          需要說明的是,臨時配置的coredump選項,其默認生成路徑為執(zhí)行該命令時候的路徑,可以通過修改配置來進行路徑修改。

          永久

          上面的設(shè)置只是使能了core dump功能,缺省情況下,內(nèi)核在coredump時所產(chǎn)生的core文件放在與該程序相同的目錄中,并且文件名固定為core。很顯然,如果有多個程序產(chǎn)生core文件,或者同一個程序多次崩潰,就會重復(fù)覆蓋同一個core文件。

          過修改kernel的參數(shù),可以指定內(nèi)核所生成的coredump文件的文件名。使用下面命令,可以實現(xiàn)coredump永久配置、存放路徑以及生成coredump名稱等。

          mkdir?-p?/www/coredump/
          chmod?777?/www/coredump/

          /etc/profile
          ulimit?-c?unlimited

          /etc/security/limits.conf
          *??????????soft?????core???unlimited

          echo?"/www/coredump/core-%e-%p-%h-%t"?>?/proc/sys/kernel/core_pattern
          調(diào)試

          現(xiàn)在,我們重新執(zhí)行如下命令,按照預(yù)期產(chǎn)生coredump文件:

          ./test_coredump

          elem[955]?=?1702113070
          elem[956]?=?1667200115
          elem[957]?=?6648431
          elem[958]?=?0
          elem[959]?=?0
          段錯誤(吐核)

          然后使用下面的命令進行coredump調(diào)試:

          gdb?./test_core?-c?/www/coredump/core_test_core_1640765384_38924?-q

          輸出如下:

          #0??0x0000000000400569?in?print?(v=0x7fff3293c100,?size=1000)?at?test_core.cc:5
          5?????printf("elem[%d]?=?%d\n",?i,?v[i]);
          Missing?separate?debuginfos,?use:?debuginfo-install?glibc-2.17-260.el7.x86_64?libgcc-4.8.5-36.el7.x86_64?libstdc++-4.8.5-36.el7.x86_64
          (gdb)

          可以看出,程序core在了第5行,此時,我們可以通過where命令來查看堆棧回溯信息。

          ?

          在gdb中輸入where命令,可以獲取堆棧調(diào)用信息。當進行coredump調(diào)試時候,這個是最基本且最有用處的命令。where命令輸出的結(jié)果包含程序中 的函數(shù)名稱和相關(guān)參數(shù)值。

          ?

          通過where命令,我們能夠發(fā)現(xiàn)程序core在了第5行,那么根據(jù)分析源碼基本就能定位原因。

          ?

          需要注意的是,在多線程運行的時候,core不一定在當前線程,這就需要我們對代碼有一定的了解,能夠保證哪塊代碼是安全的,然后通過thread num切換線程,然后再通過bt或者where命令查看堆棧信息,進而定位coredump原因。

          ?

          原理

          在前面幾節(jié),我們講了gdb的命令,以及這些命令在調(diào)試時候的作用,并以例子進行了演示。作為C/C++ coder,要知其然,更要知其所以然。所以,借助本節(jié),我們大概講下GDB調(diào)試的原理。

          gdb 通過系統(tǒng)調(diào)用 ptrace 來接管一個進程的執(zhí)行。ptrace 系統(tǒng)調(diào)用提供了一種方法使得父進程可以觀察和控制其它進程的執(zhí)行,檢查和改變其核心映像以及寄存器。它主要用來實現(xiàn)斷點調(diào)試和系統(tǒng)調(diào)用跟蹤。

          ptrace系統(tǒng)調(diào)用定義如下:

          #include?
          long?ptrace(enum?__ptrace_request?request,?pid_t?pid,?void?*addr,?void?*data)
          • pid_t pid:指示 ptrace 要跟蹤的進程
          • void *addr:指示要監(jiān)控的內(nèi)存地址
          • enum __ptrace_request request:決定了系統(tǒng)調(diào)用的功能,幾個主要的選項:
            • PTRACE_TRACEME:表示此進程將被父進程跟蹤,任何信號(除了 SIGKILL)都會暫停子進程,接著阻塞于 wait() 等待的父進程被喚醒。子進程內(nèi)部對 exec() 的調(diào)用將發(fā)出 SIGTRAP 信號,這可以讓父進程在子進程新程序開始運行之前就完全控制它
            • PTRACE_ATTACH:attach 到一個指定的進程,使其成為當前進程跟蹤的子進程,而子進程的行為等同于它進行了一次 PTRACE_TRACEME 操作。但需要注意的是,雖然當前進程成為被跟蹤進程的父進程,但是子進程使用 getppid() 的到的仍將是其原始父進程的pid
            • PTRACE_CONT:繼續(xù)運行之前停止的子進程。可同時向子進程交付指定的信號

          調(diào)試原理

          運行并調(diào)試新進程

          運行并調(diào)試新進程,步驟如下:

          • 運行g(shù)db exe
          • 輸入run命令,gdb執(zhí)行以下操作:
            • 通過fork()系統(tǒng)調(diào)用創(chuàng)建一個新進程
            • 在新創(chuàng)建的子進程中執(zhí)行ptrace(PTRACE_TRACEME, 0, 0, 0)操作
            • 在子進程中通過execv()系統(tǒng)調(diào)用加載指定的可執(zhí)行文件

          attach運行的進程

          可以通過gdb attach pid來調(diào)試一個運行的進程,gdb將對指定進程執(zhí)行ptrace(PTRACE_ATTACH, pid, 0, 0)操作。

          需要注意的是,當我們attach一個進程id時候,可能會報如下錯誤:

          Attaching?to?process?28849
          ptrace:?Operation?not?permitted.

          這是因為沒有權(quán)限進行操作,可以根據(jù)啟動該進程用戶下或者root下進行操作。

          斷點原理

          實現(xiàn)原理

          當我們通過b或者break設(shè)置斷點時候,就是在指定位置插入斷點指令,當被調(diào)試的程序運行到斷點的時候,產(chǎn)生SIGTRAP信號。該信號被gdb捕獲并 進行斷點命中判斷。

          設(shè)置原理

          在程序中設(shè)置斷點,就是先在該位置保存原指令,然后在該位置寫入int 3。當執(zhí)行到int 3時,發(fā)生軟中斷,內(nèi)核會向子進程發(fā)送SIGTRAP信號。當然,這個信號會轉(zhuǎn)發(fā)給父進程。然后用保存的指令替換int 3并等待操作恢復(fù)。

          命中判斷

          gdb將所有斷點位置存儲在一個鏈表中。命中判定將被調(diào)試程序的當前停止位置與鏈表中的斷點位置進行比較,以查看斷點產(chǎn)生的信號。

          條件判斷

          在斷點處恢復(fù)指令后,增加了一個條件判斷。如果表達式為真,則觸發(fā)斷點。由于需要判斷一次,添加條件斷點后,是否觸發(fā)條件斷點,都會影響性能。在 x86 平臺上,部分硬件支持硬件斷點。不是在條件斷點處插入 int 3,而是插入另一條指令。當程序到達這個地址時,不是發(fā)出int 3信號,而是進行比較。特定寄存器的內(nèi)容和某個地址,然后決定是否發(fā)送int 3。因此,當你的斷點位置被程序頻繁“通過”時,盡量使用硬件斷點,這將有助于提高性能。

          單步原理

          這個ptrace函數(shù)本身就支持,可以通過ptrace(PTRACE_SINGLESTEP, pid,...)調(diào)用來實現(xiàn)單步。

          ?printf("attaching?to?PID?%d\n",?pid);
          ????if?(ptrace(PTRACE_ATTACH,?pid,?0,?0)?!=?0)
          ????{
          ????????perror("attach?failed");
          ????}
          ????int?waitStat?=?0;
          ????int?waitRes?=?waitpid(pid,?&waitStat,?WUNTRACED);
          ????if?(waitRes?!=?pid?||?!WIFSTOPPED(waitStat))
          ????{
          ????????printf("unexpected?waitpid?result!\n");
          ????????exit(1);
          ????}
          ???
          ????int64_t?numSteps?=?0;
          ????while?(true)?{
          ????????auto?res?=?ptrace(PTRACE_SINGLESTEP,?pid,?0,?0);
          ????}

          上述代碼,首先接收一個pid,然后對其進行attach,最后調(diào)用ptrace進行單步調(diào)試。

          其它

          借助本文,簡單介紹下筆者工作過程中使用的一些其他命令或者工具。

          pstack

          此命令可顯示每個進程的棧跟蹤。pstack 命令必須由相應(yīng)進程的屬主或 root 運行。可以使用 pstack 來確定進程掛起的位置。此命令允許使用的唯一選項是要檢查的進程的 PID。

          這個命令在排查進程問題時非常有用,比如我們發(fā)現(xiàn)一個服務(wù)一直處于work狀態(tài)(如假死狀態(tài),好似死循環(huán)),使用這個命令就能輕松定位問題所在;可以在一段時間內(nèi),多執(zhí)行幾次pstack,若發(fā)現(xiàn)代碼棧總是停在同一個位置,那個位置就需要重點關(guān)注,很可能就是出問題的地方;

          以前面的多線程代碼為例,其進程ID是4507(在筆者本地),那么通過

          pstack 4507輸出結(jié)果如下:

          Thread?3?(Thread?0x7f07aaa69700?(LWP?45708)):
          #0??0x00007f07aab2ee2d?in?nanosleep?()?from?/lib64/libc.so.6
          #1??0x00007f07aab2ecc4?in?sleep?()?from?/lib64/libc.so.6
          #2??0x00007f07ab403eb9?in?std::this_thread::__sleep_for(std::chrono::duration<long,?std::ratio<1l,?1l>?>,?std::chrono::duration<long,?std::ratio<1l,?1000000000l>?>)?()?from?/lib64/libstdc++.so.6
          #3??0x00000000004018cc?in?void?std::this_thread::sleep_for<long,?std::ratio<1l,?1l>?>(std::chrono::duration<long,?std::ratio<1l,?1l>?>?const&)?()
          #4??0x00000000004012de?in?fun_int(int)?()
          #5??0x0000000000404696?in?int?std::_Bind_simple<int?(*(int))(int)>::_M_invoke<0ul>(std::_Index_tuple<0ul>)?()
          #6??0x000000000040443d?in?std::_Bind_simple<int?(*(int))(int)>::operator()()?()
          #7??0x000000000040436e?in?std::thread::_Impl<std::_Bind_simple<int?(*(int))(int)>?>::_M_run()?()
          #8??0x00007f07ab404070?in????()?from?/lib64/libstdc++.so.6
          #9??0x00007f07ab65ddd5?in?start_thread?()?from?/lib64/libpthread.so.0
          #10?0x00007f07aab67ead?in?clone?()?from?/lib64/libc.so.6
          Thread?2?(Thread?0x7f07aa268700?(LWP?45709)):
          #0??0x00007f07aab2ee2d?in?nanosleep?()?from?/lib64/libc.so.6
          #1??0x00007f07aab2ecc4?in?sleep?()?from?/lib64/libc.so.6
          #2??0x00007f07ab403eb9?in?std::this_thread::__sleep_for(std::chrono::duration<long,?std::ratio<1l,?1l>?>,?std::chrono::duration<long,?std::ratio<1l,?1000000000l>?>)?()?from?/lib64/libstdc++.so.6
          #3??0x00000000004018cc?in?void?std::this_thread::sleep_for<long,?std::ratio<1l,?1l>?>(std::chrono::duration<long,?std::ratio<1l,?1l>?>?const&)?()
          #4??0x0000000000401340?in?fun_string(std::string?const&)?()
          #5??0x000000000040459f?in?int?std::_Bind_simple<int?(*(char?const*))(std::string?const&)>::_M_invoke<0ul>(std::_Index_tuple<0ul>)?()
          #6??0x000000000040441f?in?std::_Bind_simple<int?(*(char?const*))(std::string?const&)>::operator()()?()
          #7??0x0000000000404350?in?std::thread::_Impl<std::_Bind_simple<int?(*(char?const*))(std::string?const&)>?>::_M_run()?()
          #8??0x00007f07ab404070?in????()?from?/lib64/libstdc++.so.6
          #9??0x00007f07ab65ddd5?in?start_thread?()?from?/lib64/libpthread.so.0
          #10?0x00007f07aab67ead?in?clone?()?from?/lib64/libc.so.6
          Thread?1?(Thread?0x7f07aba80740?(LWP?45707)):
          #0??0x00007f07ab65ef47?in?pthread_join?()?from?/lib64/libpthread.so.0
          #1??0x00007f07ab403e37?in?std::thread::join()?()?from?/lib64/libstdc++.so.6
          #2??0x0000000000401455?in?main?()

          在上述輸出結(jié)果中,將進程內(nèi)部的詳細信息都輸出在終端,以方便分析問題。

          ldd

          在我們編譯過程中通常會提示編譯失敗,通過輸出錯誤信息發(fā)現(xiàn)是找不到函數(shù)定義,再或者編譯成功了,但是運行時候失敗(往往是因為依賴了非正常版本的lib庫導(dǎo)致),這個時候,我們就可以通過ldd來分析該可執(zhí)行文件依賴了哪些庫以及這些庫所在的路徑。

          用來查看程式運行所需的共享庫,常用來解決程式因缺少某個庫文件而不能運行的一些問題。

          仍然查看可執(zhí)行程序test_thread的依賴庫,輸出如下:

          ldd?-r?./test_thread
          ?linux-vdso.so.1?=>??(0x00007ffde43bc000)
          ?libpthread.so.0?=>?/lib64/libpthread.so.0?(0x00007f8c5e310000)
          ?libstdc++.so.6?=>?/lib64/libstdc++.so.6?(0x00007f8c5e009000)
          ?libm.so.6?=>?/lib64/libm.so.6?(0x00007f8c5dd07000)
          ?libgcc_s.so.1?=>?/lib64/libgcc_s.so.1?(0x00007f8c5daf1000)
          ?libc.so.6?=>?/lib64/libc.so.6?(0x00007f8c5d724000)
          ?/lib64/ld-linux-x86-64.so.2?(0x00007f8c5e52c000)

          在上述輸出中:

          • 第一列:程序需要依賴什么庫
          • 第二列:系統(tǒng)提供的與程序需要的庫所對應(yīng)的庫
          • 第三列:庫加載的開始地址

          在有時候,我們通過ldd查看依賴庫的時候,會提示找不到庫,如下:

          ldd?-r?test_process
          ?linux-vdso.so.1?=>??(0x00007ffc71b80000)
          ?libstdc++.so.6?=>?/lib64/libstdc++.so.6?(0x00007fe4badd5000)
          ?libm.so.6?=>?/lib64/libm.so.6?(0x00007fe4baad3000)
          ?libgcc_s.so.1?=>?/lib64/libgcc_s.so.1?(0x00007fe4ba8bd000)
          ?libc.so.6?=>?/lib64/libc.so.6?(0x00007fe4ba4f0000)
          ?/lib64/ld-linux-x86-64.so.2?(0x00007fe4bb0dc000)
          ??liba.so?=>?not?found

          比如上面最后一句提示,liba.so找不到,這個時候,需要我們知道liba.so的路徑,比如在/path/to/liba.so,那么可以有下面兩種方式:

          LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/

          這樣在通過ldd查看,就能找到對應(yīng)的lib庫,但是這個缺點是臨時的,即退出終端后,再執(zhí)行l(wèi)dd,仍然會提示找不到該庫,所以就有了另外一種方式,即通過修改/etc/ld.so.conf,在該文件的后面加上需要的路徑,即

          include?ld.so.conf.d/*.conf
          /path/to/

          然后通過如下命令,即可永久生效

          ?/sbin/ldconfig

          c++filt

          因為c++支持重載,也就引出了編譯器的name mangling機制,對函數(shù)進行重命名。

          我們通過strings命令查看test_thread中的函數(shù)信息(僅輸出fun等相關(guān))

          strings?test_thread?|?grep?fun_
          in?fun_int?n?=
          in?fun_string?s?=
          _GLOBAL__sub_I__Z7fun_inti
          _Z10fun_stringRKSs

          可以看到_Z10fun_stringRKSs這個函數(shù),如果想知道這個函數(shù)定義的話,可以使用c++filt命令,如下:

          ?c++filt?_Z10fun_stringRKSs
          fun_string(std::basic_string<char,?std::char_traits<char>,?std::allocator<char>?>?const&)

          通過上述輸出,我們可以將編譯器生成的函數(shù)名還原到我們代碼中的函數(shù)名即fun_string。

          結(jié)語

          GDB是一個在Linux上進行開發(fā)的一個必不可少的調(diào)試工具,使用場景依賴于具體的需求或者遇到的具體問題。在我們的日常開發(fā)工作中,熟練使用GDB加以輔助,能夠使得開發(fā)過程事半功倍。

          本文從一些簡單的命令出發(fā),通過舉例調(diào)試可執(zhí)行程序(單線程、多線程以及多進程場景)、coredump文件等各個場景,使得大家能夠更加直觀的了解GDB的使用。GDB功能非常強大,筆者工作中使用的都是非常基本的一些功能,如果想深入理解GDB,則需要去官網(wǎng)進行閱讀了解。

          好了,本期的文章就到這,我們下期見。

          ?

          本文從構(gòu)思到完成,大概用了三周時間,寫作過程是痛苦的(需要整理資料以及構(gòu)建各種場景,以及將各種現(xiàn)場還原),同時又是收獲滿滿的。通過本文,進一步加深了對GDB的底層原理理解。

          ?

          參考

          https://www.codetd.com/en/article/13107993

          https://www.codetd.com/en/article/13107993 https://users.ece.utexas.edu/~adnan/gdb-refcard.pdf?

          https://www.cloudsavvyit.com/10921/debugging-with-gdb-getting-started/?

          https://blog.birost.com/a?ID=00650-b03e2257-94bf-41f3-b0fc-d352d5b02431?

          https://www.cnblogs.com/xsln/p/ptrace.html


          如果對本文有疑問可以加筆者微信直接交流,筆者也建了C/C++相關(guān)的技術(shù)群,有興趣的可以聯(lián)系筆者加群。

          往期精彩回顧




          【線上問題】P1級公司故障,年終獎不保
          【性能優(yōu)化】lock-free在召回引擎中的實現(xiàn)
          【性能優(yōu)化】高效內(nèi)存池的設(shè)計與實現(xiàn)
          2萬字|30張圖帶你領(lǐng)略glibc內(nèi)存管理精髓
          【萬字長文】吃透負載均衡
          流量控制還能這么搞。。。
          技術(shù)十年
          聊聊服務(wù)注冊與發(fā)現(xiàn)

          點個關(guān)注吧!

          瀏覽 30
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产黄色精品视频 | 免费鸡巴视频网站 | 啪啪啪毛片 | 国产豆花在线视频 | 天天舔天天插 |