linux必先利其器篇之--GDB詳解

gdb調試完整示例(教程):ncurses

我對在網上找到顯示命令而不顯示其輸出的“ gdb示例”感到有些沮喪。 gdb是GNU調試器,Linux上的標準調試器。在觀看《給我15分鐘》鏈接: link.時,我被提醒缺乏示例輸出,我將改變Greg Law在CppCon 2015上對GDB演講的看法,所幸其中包括輸出! 15分鐘是值得的。

這也啓發了我分享一個完整的gdb調試示例,其中包括輸出和涉及的每個步驟,包括死衚衕。這不是一個特別有趣或陌生的問題,它只是例行的gdb調試會話。但是它涵蓋了基礎知識,並且可以用作各種教程,請記住,gdb的功能比我在這裏使用的要多得多。

由於我正在調試需要root訪問權限的工具,因此我將以root用戶身份運行以下命令。根據需要替換非root用戶和sudo。您也不應該通讀所有這些內容:我已經列舉了每個步驟,因此您可以瀏覽它們並找到感興趣的步驟。

1. The Problem

BPF工具的密件抄送集合有一個對緩存頂部的拉取請求,該請求使用類似頂部的顯示來按進程顯示頁面緩存統計信息。大!但是,當我對其進行測試時,它遇到了段錯誤:

# ./cachetop.py
Segmentation fault

請注意,它顯示的是“分段故障”,而不是“分段故障(核心已轉儲)”。我想要一個核心轉儲來調試它。 (核心轉儲是進程內存的副本–名稱來自磁核心內存的時代–可以使用調試器進行調查。)

核心轉儲分析是一種調試方法,但不是唯一的一種。我可以在gdb中實時運行該程序以檢查問題。我還可以使用外部跟蹤器來捕獲數據並在segfault事件上跟蹤堆棧。我們將從核心轉儲開始。

2. Fixing Core Dumps

我將檢查覈心轉儲設置:

# ulimit -c
0
# cat /proc/sys/kernel/core_pattern
core

ulimit -c 顯示核心轉儲最大的創建數,現在被設置成0:關閉核心轉儲(對當前進程和他的子進程)

/proc/…/core_pattern設置爲“ core”,它將在當前目錄中刪除一個名爲“ core”的核心轉儲文件。暫時可以,但是我將展示如何在全球位置進行設置:

# ulimit -c unlimited
# mkdir /var/cores
# echo "/var/cores/core.%e.%p" > /proc/sys/kernel/core_pattern

您可以進一步自定義該core_pattern;例如,%h表示主機名,%t表示轉儲時間。這些選項記錄在Linux內核源文件的Documentation / sysctl / kernel.txt下。

要使core_pattern永久且在重啓後仍然有效,可以通過/etc/sysctl.conf中的“ kernel.core_pattern”進行設置。

再試一次:

# ./cachetop.py
Segmentation fault (core dumped)
# ls -lh /var/cores
total 19M
-rw------- 1 root root 20M Aug  7 22:15 core.python.30520
# file /var/cores/core.python.30520 
/var/cores/core.python.30520: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from 'python ./cachetop.py'

我們有核心轉儲文件了。

3. Starting GDB

現在,我將使用目標程序位置運行gdb(使用shell替換,“`”,儘管您應該指定完整路徑,除非您確定可以使用),並使用核心轉儲文件:

# gdb `which python` /var/cores/core.python.30520
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.

warning: core file may not match specified executable file.
[New LWP 30520]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.
Core was generated by `python ./cachetop.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007f0a37aac40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5

最後兩行特別有趣:它告訴我們這是libncursesw庫的doupdate()函數中的分段錯誤。如果這是一個衆所周知的問題,那麼值得快速進行網絡搜索。我快速瀏覽了一下,但沒有找到一個常見原因。

我已經可以猜出libncursesw是幹什麼用的,但是如果您不熟悉libncursesw,那麼在“ / lib”下並以“ .so。*”結尾表示這是一個共享庫,其中可能包含手冊頁,網站,軟件包說明等

# dpkg -l | grep libncursesw
ii  libncursesw5:amd64                  6.0+20160213-1ubuntu1                    amd64
     shared libraries for terminal handling (wide character support)

我碰巧正在Ubuntu上調試它,但是Linux發行版對於gdb的使用並不重要。

4. Back Trace

堆棧跟蹤顯示了我們如何到達故障點,並且通常足以幫助識別常見問題。通常這是我在gdb會話中使用的第一個命令:bt(回溯的縮寫):

gdb) bt
#0  0x00007f0a37aac40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
#1  0x00007f0a37aa07e6 in wrefresh () from /lib/x86_64-linux-gnu/libncursesw.so.5
#2  0x00007f0a37a99616 in ?? () from /lib/x86_64-linux-gnu/libncursesw.so.5
#3  0x00007f0a37a9a325 in wgetch () from /lib/x86_64-linux-gnu/libncursesw.so.5
#4  0x00007f0a37cc6ec3 in ?? () from /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
#5  0x00000000004c4d5a in PyEval_EvalFrameEx ()
#6  0x00000000004c2e05 in PyEval_EvalCodeEx ()
#7  0x00000000004def08 in ?? ()
#8  0x00000000004b1153 in PyObject_Call ()
#9  0x00000000004c73ec in PyEval_EvalFrameEx ()
#10 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#11 0x00000000004caf42 in PyEval_EvalFrameEx ()
#12 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#13 0x00000000004c2ba9 in PyEval_EvalCode ()
#14 0x00000000004f20ef in ?? ()
#15 0x00000000004eca72 in PyRun_FileExFlags ()
#16 0x00000000004eb1f1 in PyRun_SimpleFileExFlags ()
#17 0x000000000049e18a in Py_Main ()
#18 0x00007f0a3be10830 in __libc_start_main (main=0x49daf0 <main>, argc=2, argv=0x7ffd33d94838, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7ffd33d94828) at ../csu/libc-start.c:291
#19 0x000000000049da19 in _start ()

從下至上閱讀,從父母到孩子。 “?”條目是符號轉換失敗的地方。產生堆棧跟蹤的堆棧遍歷也可能失敗。在這種情況下,您可能會看到一個有效的幀,然後是少量的虛假地址。如果符號或堆棧損壞得太嚴重而無法理解堆棧跟蹤,則通常可以通過以下方法來解決:安裝調試信息包(爲gdb提供更多符號,並讓其執行基於DWARF的堆棧遍歷),或重新編譯軟件從源中獲取具有幀指針和調試信息的(-fno-omit-frame-pointer -g)。上面的許多“ ??”可以通過添加python-dbg軟件包來修復條目。

這個特定的堆棧看起來不太有用:第5幀到第17幀(在左側索引)是Python的內部結構,儘管我們尚未看到Python方法。然後第4幀是_curses庫,然後在libncursesw中。看起來像wgetch()-> wrefresh()-> doupdate()。僅根據名稱,我想會刷新窗口。爲什麼要進行核心轉儲?

5. Disassembly

我將從反彙編我們的段錯誤的函數doupdate()開始:

(gdb) disas doupdate
Dump of assembler code for function doupdate:
   0x00007f0a37aac2e0 <+0>:   push   %r15
   0x00007f0a37aac2e2 <+2>:   push   %r14
   0x00007f0a37aac2e4 <+4>:   push   %r13
   0x00007f0a37aac2e6 <+6>:   push   %r12
   0x00007f0a37aac2e8 <+8>:   push   %rbp
   0x00007f0a37aac2e9 <+9>:   push   %rbx
   0x00007f0a37aac2ea <+10>:  sub    $0xc8,%rsp
[...]
---Type <return> to continue, or q <return> to quit---
[...]
   0x00007f0a37aac3f7 <+279>: cmpb   $0x0,0x21(%rcx)
   0x00007f0a37aac3fb <+283>: je     0x7f0a37aacc3b <doupdate+2395>
   0x00007f0a37aac401 <+289>: mov    0x20cb68(%rip),%rax        # 0x7f0a37cb8f70
   0x00007f0a37aac408 <+296>: mov    (%rax),%rsi
   0x00007f0a37aac40b <+299>: xor    %eax,%eax
=> 0x00007f0a37aac40d <+301>: mov    0x10(%rsi),%rdi
   0x00007f0a37aac411 <+305>: cmpb   $0x0,0x1c(%rdi)
   0x00007f0a37aac415 <+309>: jne    0x7f0a37aac6f7 <doupdate+1047>
   0x00007f0a37aac41b <+315>: movswl 0x4(%rcx),%ecx
   0x00007f0a37aac41f <+319>: movswl 0x74(%rdx),%edi
   0x00007f0a37aac423 <+323>: mov    %rax,0x40(%rsp)
[...]

輸出被截斷。 (我也可以只鍵入“ disas”,並且默認情況下爲doupdate。)

箭頭“ =>”指向我們的段錯誤地址,它正在執行mov 0x10(%rsi),%r​​di:從%rsi寄存器中指向的內存加上偏移量0x10的移動到%rdi寄存器。接下來,我將檢查寄存器的狀態。

6. Check Registers

使用i r打印寄存器狀態(short for info registers):

(gdb) i r
rax            0x0  0
rbx            0x1993060    26816608
rcx            0x19902a0    26804896
rdx            0x19ce7d0    27060176
rsi            0x0  0
rdi            0x19ce7d0    27060176
rbp            0x7f0a3848eb10   0x7f0a3848eb10 <SP>
rsp            0x7ffd33d93c00   0x7ffd33d93c00
r8             0x7f0a37cb93e0   139681862489056
r9             0x0  0
r10            0x8  8
r11            0x202    514
r12            0x0  0
r13            0x0  0
r14            0x7f0a3848eb10   139681870703376
r15            0x19ce7d0    27060176
rip            0x7f0a37aac40d   0x7f0a37aac40d <doupdate+301>
eflags         0x10246  [ PF ZF IF RF ]
cs             0x33 51
ss             0x2b 43
ds             0x0  0
es             0x0  0
fs             0x0  0
gs             0x0  0

好吧,%rsi爲零。這是我們的問題!零不太可能是有效地址,這種類型的段錯誤是常見的軟件錯誤:取消引用未初始化或NULL指針。

7. Memory Mappings

您可以使用i proc m再次檢查零是否有效(short for info proc mappings):

(gdb) i proc m
Mapped address spaces:

      Start Addr           End Addr       Size     Offset objfile
        0x400000           0x6e7000   0x2e7000        0x0 /usr/bin/python2.7
        0x8e6000           0x8e8000     0x2000   0x2e6000 /usr/bin/python2.7
        0x8e8000           0x95f000    0x77000   0x2e8000 /usr/bin/python2.7
  0x7f0a37a8b000     0x7f0a37ab8000    0x2d000        0x0 /lib/x86_64-linux-gnu/libncursesw.so.5.9
  0x7f0a37ab8000     0x7f0a37cb8000   0x200000    0x2d000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
  0x7f0a37cb8000     0x7f0a37cb9000     0x1000    0x2d000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
  0x7f0a37cb9000     0x7f0a37cba000     0x1000    0x2e000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
  0x7f0a37cba000     0x7f0a37ccd000    0x13000        0x0 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
  0x7f0a37ccd000     0x7f0a37ecc000   0x1ff000    0x13000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
  0x7f0a37ecc000     0x7f0a37ecd000     0x1000    0x12000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
  0x7f0a37ecd000     0x7f0a37ecf000     0x2000    0x13000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
  0x7f0a38050000     0x7f0a38066000    0x16000        0x0 /lib/x86_64-linux-gnu/libgcc_s.so.1
  0x7f0a38066000     0x7f0a38265000   0x1ff000    0x16000 /lib/x86_64-linux-gnu/libgcc_s.so.1
  0x7f0a38265000     0x7f0a38266000     0x1000    0x15000 /lib/x86_64-linux-gnu/libgcc_s.so.1
  0x7f0a38266000     0x7f0a3828b000    0x25000        0x0 /lib/x86_64-linux-gnu/libtinfo.so.5.9
  0x7f0a3828b000     0x7f0a3848a000   0x1ff000    0x25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9
[...]

第一個有效的虛擬地址是0x400000。低於該值的任何內容(如果被引用)將觸發分段錯誤。

此時,有幾種不同的方法可以進一步挖掘。我將從一些說明步驟開始。

8. Breakpoints

Back to the disassembly:

   0x00007f0a37aac401 <+289>:   mov    0x20cb68(%rip),%rax        # 0x7f0a37cb8f70
   0x00007f0a37aac408 <+296>:   mov    (%rax),%rsi
   0x00007f0a37aac40b <+299>:   xor    %eax,%eax
=> 0x00007f0a37aac40d <+301>:   mov    0x10(%rsi),%rdi

閱讀這四條指令:似乎是從棧中將某些東西拉到%rax中,然後將%rax引用到%rsi中,將%eax設置爲零(xor是一種優化,而不是移動$ 0),然後儘管我們知道%rsi爲零,但我們用偏移量取消了對%rsi的引用。此序列用於遍歷數據結構。也許%rax很有趣,但是先前的指令已將其設置爲零,因此我們無法在覈心轉儲寄存器狀態下看到它。

我可以在doupdate + 289上設置一個斷點,然後單步執行每條指令以瞭解如何設置和更改寄存器。首先,我需要啓動gdb以便我們實時執行程序:

# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.
(gdb) b *doupdate + 289
No symbol table is loaded.  Use the "file" command.

哎呀。我想顯示此錯誤,以解釋爲什麼我們通常以main上的斷點開始,然後在該點上加載符號,然後設置感興趣的實際斷點。我將直接進入doupdate函數條目,運行問題,然後在碰到函數時設置偏移斷點:

(gdb) b doupdate
Function "doupdate" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (doupdate) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.

Breakpoint 1, 0x00007ffff34ad2e0 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) b *doupdate + 289
Breakpoint 2 at 0x7ffff34ad401
(gdb) c
Continuing.

Breakpoint 2, 0x00007ffff34ad401 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5

我們到了斷點。

如果您以前沒有做過,r(運行)命令將使用將傳遞給我們先前在命令行(python)上指定的gdb目標的參數。因此,這最終運行了“ python cachetop.py”。

原文鏈接地址:http://www.brendangregg.com/blog/2016-08-09/gdb-example-ncurses.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章