1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        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è)置斷點,然后分析原因;當(dāng)線上服務(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在當(dāng)前位置偏移量為[+-]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每當(dāng)某個表達式改變了值就暫停執(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ù)斷點時,需要注意:

        • 當(dāng)監(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個棧幀,如果不存在,則打印當(dāng)前棧幀
        up n選擇當(dāng)前棧幀編號+n的棧幀
        down n選擇當(dāng)前棧幀編號-n的棧幀
        info frame [addr]描述當(dāng)前選擇的棧幀
        info args當(dāng)前棧幀的參數(shù)列表
        info locals當(dāng)前棧幀的局部變量

        多進程、多線程

        多進程

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

        多線程

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

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

        命令作用
        info threads查看線程列表
        print $_thread顯示當(dāng)前正在調(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)心當(dāng)前線程,建議臨時設(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表示4byte,g表示8bytegiant word
        命令作用
        x/8xb array以16進制打印數(shù)組array的前8個byte的值
        x/8xw array以16進制打印數(shù)組array的前16個word的值
        打印局部變量
        命令作用
        info locals打印當(dāng)前函數(shù)局部變量的值
        backtrace full打印當(dāng)前棧幀各個函數(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í)行完當(dāng)前函數(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文件,記錄當(dāng)前進程的狀態(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命令是列出當(dāng)前堆棧中的所有幀。在上面的例子中,棧上只有一幀,編號為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總是位于當(dāng)前正在執(zhí)行函數(shù)對應(yīng)棧幀的上下文中。

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

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

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

        (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:通過該命令來查看當(dāng)前處于什么模式下,通過輸出可以看出,處于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í)行該命令,通過輸出可以看出,當(dāng)前處于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和當(dāng)前進程的位置
        • fork fork-id: 參數(shù)fork-id是GDB分配的內(nèi)部fork編號,該編號可用通過上面的命令info forks獲取

        coredump

        當(dāng)我們開發(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可以判斷當(dāng)前有沒有配置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命令來查看堆?;厮菪畔ⅰ?/span>

        ?

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

        ?

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

        ?

        需要注意的是,在多線程運行的時候,core不一定在當(dāng)前線程,這就需要我們對代碼有一定的了解,能夠保證哪塊代碼是安全的,然后通過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 到一個指定的進程,使其成為當(dāng)前進程跟蹤的子進程,而子進程的行為等同于它進行了一次 PTRACE_TRACEME 操作。但需要注意的是,雖然當(dāng)前進程成為被跟蹤進程的父進程,但是子進程使用 getppid() 的到的仍將是其原始父進程的pid
          • PTRACE_CONT:繼續(xù)運行之前停止的子進程??赏瑫r向子進程交付指定的信號

        調(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)操作。

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

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

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

        斷點原理

        實現(xiàn)原理

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

        設(shè)置原理

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

        命中判斷

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

        條件判斷

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

        單步原理

        這個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)代碼??偸峭T谕粋€位置,那個位置就需要重點關(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
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            男人天堂1024 | 久久久久性爱 | 啊轻点灬太粗嗯太深了用力视频 | 777在线视频 | 操比视频免费观看 | 日本亲与子乱人妻hd | 91青娱乐在线视频 | 欧美日韩操逼网站 | 老司机午夜激情 | 国产精品国产精品国产专区不蜜 |