1. 程式人生 > >gdb除錯祕籍(暫存器、棧)

gdb除錯祕籍(暫存器、棧)

GDB的常用除錯命令大家可以查閱gdb手冊就可以快速的上手了,在這兒就不給大家分享了,需要的可以到GDB的官網去下載手冊。這裡重點分享下GDB除錯中的一些暫存器和棧的相關知識用於解決下列gdb除錯時的問題:

  1. 優化的程式碼在printf或其它glibc函式處core
  2. 沒有檢查返回值的函式呼叫異常導致的異常
  3. 優化的程式碼的計算異常的中間過程分析
  4. 棧溢位導致的core
  5. 區域性變數越界導致棧異常的core

暫存器

通常除錯的程式碼基本上都是在未開啟優化的情況下,各個變數都可以直接檢視,因此造成很多人除錯時基本上不會看暫存器,但是對於線上的生產環境,可能會因為效能的因素,需要開啟程式碼優化,此時出現異常需要除錯時就通常需要檢視暫存器了,下列是gdb除錯中需要了解的暫存器。

  • $rip 指令暫存器,指向當前執行的程式碼位置
  • $rsp 棧指標暫存器,指向當前棧頂
  • $rax,$rbx,$rcx,$rdx,$rsi,$rdi,$rbp,$r8,$r9,$r10,$r11,$r12,$r13,$r14,$r15 通用暫存器
  • 函式入參

    一般linux下會優先將引數壓到暫存器中,只有當暫存器不夠所有的引數時,才會將入參壓到棧上,一般入參的壓棧順序為$rdi、$rsi、$rdx、$rcx、$r8、$r9,如下
    int arg_int(int a, int b, int c,
              int d, int e, int f,
              int g, int h, 
    int i) { return (a + b + c + d + e + f + g + h + i); }
    arg_int入參
    Breakpoint 1, arg_int (a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9) at ./reg.cpp:7
    ......
    (gdb) i r
    rax            0x7ffff7639f60   140737343889248
    rbx            0x14     20
    rcx            0x4      4
    rdx            0x3      3
    rsi            0x2
    2 rdi 0x1 1 rbp 0xa 0xa rsp 0x7fffffffe4e8 0x7fffffffe4e8 r8 0x5 5 r9 0x6 6 r10 0x7fffffffe3b0 140737488348080 r11 0x7ffff72cbbe0 140737340292064 r12 0x1e 30 r13 0x28 40 r14 0x32 50 r15 0x3c 60 rip 0x4005e4 0x4005e4 <arg_int(int, int, int, int, int, int, int, int, int)>
    第7、8、9個入參將會被壓到棧上,具體的棧上的位置資訊大家可以自己研究。
    瞭解了入參時的暫存器知識,也該學以致用了,最典型的需要使用暫存器檢視函式入參的場景是glibc的函式,在此以printf函式作為示例。printf最一類典型的core場景,通常是因為指定的格式和實際的型別不符合造成的,但是如果沒有原始碼的話,即使斷點抓住了也不知道具體是哪個引數的格式不對,但是通過檢視暫存器就可以除錯此類問題了
      int along[9] = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
      printf("arg_long = %ld\n",
          arg_long(along[0], along[1], along[2],
                   along[3], along[4], along[5],
                   along[6], along[7], along[8]));
    斷點printf時暫存器資訊
    Breakpoint 3, 0x00007ffff72fb990 in printf () from /lib64/libc.so.6
    (gdb) i r
    rax            0x0      0
    rbx            0x7fffffffe648   140737488348744
    rcx            0x4      4
    rdx            0x15     21
    rsi            0x1ef    495
    rdi            0x400858 4196440
    rbp            0x0      0x0
    rsp            0x7fffffffe538   0x7fffffffe538
    r8             0x5      5
    r9             0x6      6
    r10            0x7fffffffe2c0   140737488347840
    r11            0x7ffff72fb990   140737340488080
    r12            0x400500 4195584
    r13            0x7fffffffe640   140737488348736
    r14            0x0      0
    r15            0x0      0
    rip            0x7ffff72fb990   0x7ffff72fb990 <printf>
    大家可以根據上面的知識來檢視暫存器知道具體的輸出資訊,$rdi是format,先檢視format後就可以按順序來檢視後面的引數是否正確了
    (gdb) p/s (char*)$rdi
    $1 = 0x400858 "arg_long = %ld\n"
    (gdb) p/d $rsi
    $2 = 495
  • 函式返回值

    函式返回值由$rax儲存返回,單步執行上面的用例,printf的返回值如下:
    arg_long = 495
    ......
    (gdb) p/d $rax
    $1 = 15
    列印的內容為”arg_long = 495\n”,一共15個位元組
    返回值檢視的具體使用場景比較廣,例如glibc、外部庫等各種看不到原始碼的函式呼叫的結果都可以通過$rax檢視返回值,這兒就不舉例了,大家可以自己驗證下
  • 函式執行中

    對於開啟了優化的場景下,區域性變數往往會僅在暫存器中儲存,如果需要檢視被優化了的區域性變數的值,如下:
      for (int i = 0 ; i < end ; ++i)
      {
          total += i;
      }
    執行N次後的斷點資訊如下:
    (gdb) 
    34              total += i;
    (gdb) p i
    $3 = <value optimized out>
    (gdb) p total 
    $4 = <value optimized out>
    直接列印i、total都顯示被優化了,因此無法直接列印其值,通過列印當前暫存器資訊得到了暫存器中的值,然後通過檢視彙編和原始碼對比
     0x0000000000400735 <+120>:   jle    0x400740 <main(int, char**)+131>
    => 0x0000000000400737 <+122>:   add    %edx,%esi
     0x0000000000400739 <+124>:   add    $0x1,%edx
     0x000000000040073c <+127>:   cmp    %eax,%edx
     0x000000000040073e <+129>:   jl     0x400737 <main(int, char**)+122>
    通過彙編可以知道,i對應為暫存器$rdx,total對應為暫存器$rsi,end對應為暫存器$rax
    (gdb) i r
    rax            0x5      5
    rbx            0x7fffffffe648   140737488348744
    rcx            0x5      5
    rdx            0x2      2
    rsi            0x1      1
    rdi            0x7fffffffe8a6   140737488349350
    rbp            0x0      0x0
    rsp            0x7fffffffe540   0x7fffffffe540
    r8             0x7ffff7637580   140737343878528
    r9             0x7ffff73eb9e0   140737341471200
    r10            0x5      5
    r11            0x1999999999999999       1844674407370955161
    r12            0x400500 4195584
    r13            0x7fffffffe640   140737488348736
    r14            0x0      0
    r15            0x0      0
    rip            0x400737 0x400737 <main(int, char**)+122>
    也就是此時i為2、total為1,total += i;執行完後$rsi應該會變為3,這點大家可以嘗試驗證下。

在gdb除錯棧錯誤前,你需要了解下列的棧知識

  • 函式呼叫跳轉時在新幀的棧首8Bytes存放上一幀的指令地址
  • 通常函式的起始操作為push $rbp,將上幀的棧底地址8Bytes壓入棧中
  • 在儲存完指令地址和棧底地址後,會進行一次sub xxx,$rsp,為當前函式內所有在棧上的區域性變數都申請好需要的棧空間
  • 函式呼叫前將需要儲存的暫存器值和超過6個的引數都壓入棧中

棧異常導致的core是線上最常見的core原因之一,常見原因有:

  • 遞迴呼叫或大變數消耗棧空間,導致棧溢位

    這類問題往往通過gdb檢視棧基本資訊就可以定位解決,話不多說,直接上實戰
    (gdb) bt
    #00x00007ffff72fb990 in printf () from /lib64/libc.so.6
    #10x000000000040069f in f1 (ac1=0x7fffffffe360, ac2=0x7fffffffe230) at ./stack.cpp:9
    #20x00000000004006f9 in f2 (ac=0x7fffffffe360) at ./stack.cpp:18
    #30x000000000040074a in main (argc=1, argv=0x7fffffffe658) at ./stack.cpp:27
    (gdb) info frame 0
    Stack frame at 0x7fffffffe1d0:
    Locals at 0x7fffffffe1c0, Previous frame's sp is 0x7fffffffe1d0
    (gdb) info frame 1
    Stack frame at 0x7fffffffe220:
    (gdb) info frame 2
    Stack frame at 0x7fffffffe350:
    (gdb) info frame 3
    Stack frame at 0x7fffffffe580:
    Stack frame at xxxx的意義為當前幀的使用者棧起始地址
    frame 0的當前棧使用為:0x7fffffffe1d0 - 0x7fffffffe1c0 = 16 Bytes
    frame 1的當前棧使用為:0x7fffffffe220 - 0x7fffffffe1d0 = 80 Bytes
    frame 2的當前棧使用為:0x7fffffffe350 - 0x7fffffffe220 = 304 Bytes
    frame 3的當前棧使用為:0x7fffffffe580 - 0x7fffffffe350 = 560 Bytes
    當前棧使用為:0x7fffffffe580 - 0x7fffffffe1c0
    由此類推,在遇到懷疑棧溢位的時候就可以根據整體棧使用和各幀的棧使用來快速定位出具體是哪層棧造成的溢位。
  • 棧上變數的越界訪問導致棧內的資料被改寫

    這類問題往往gdb檢視時會得到一個錯亂的棧資訊
    Program received signal SIGSEGV, Segmentation fault.
    0x00000000000b0000 in ?? ()
    (gdb) bt
    #00x00000000000b0000 in ?? ()
    #10x00000000000c0000 in ?? ()
    #20x00000000000d0000 in ?? ()
    #30x00000000000e0000 in ?? ()
    #40x00000000000f0000 in ?? ()
    #50x0000000000000002 in ?? ()
    #60x0000000000000003 in ?? ()
    ......
    這種情況下通常是棧上前面幀的指令地址被修改導致的,可能剛好被修改的是上一幀的指令地址,也可能修改的是幾幀前的指令地址。通常這類問題定位起來非常麻煩,根據$rsp不一定能還原棧資訊,定位起來時往往需要大量的猜測和驗證。
    下面的示例是在踩了棧後沒有其它的函式呼叫重新寫棧資訊
    ### gdb 查詢巨集
    define find
    set $ptr = $arg0
    set $cnt = 0
    while ( ($ptr<=$arg1) && ($cnt<$arg3) )
      if ( *(unsigned long *)$ptr == $arg2 )
          x/gx $ptr
          set $cnt = $cnt + 1
      end
      set $ptr = $ptr + 8
    end
    end
    (gdb) p $rsp
    $2 = (void *) 0x7fffffffe4d0
    (gdb) find 0x7fffffffe000 0x7fffffffe4d0 0x7fffffffe4c0 3
    0x7fffffffe370: 0x00007fffffffe4c0
    0x7fffffffe450: 0x00007fffffffe4c0
    (gdb) x/2gx 0x7fffffffe370
    0x7fffffffe370: 0x00007fffffffe4c0      0x00007ffff72fba2a
    (gdb) x/2gx 0x7fffffffe450
    0x7fffffffe450: 0x00007fffffffe4c0      0x000000000040066c
    addr2line -e ./stack 0x000000000040066c
    /data/lambygao/test/./stack.cpp:20
    示例中的find為定義的gdb巨集,用於在棧上查詢指定的地址值,$rsp為0x7fffffffe4d0,則該幀對應的$rpb為$rsp-16=0x7fffffffe4c0,在棧上查詢該值後得到2個地址,分別檢視按該地址為幀頭來測試,找到了懷疑的附近地址/data/lambygao/test/./stack.cpp:20
    當然如果是必現的core的話,也可以提前抓好棧的資訊,然後設定好watch,這點大家可以自己嘗試下。

補充:

find 0x7fffffffe000 0x7fffffffe4d0 0x7fffffffe4c0 3
其中查詢範圍的起始地址,和查詢的值這2個值可能不太理解為什麼是這2個值,因此補充下面的這張解釋圖說明下:

因為是在f1內越界訪問寫的f2內的陣列,破壞的是f2幀頭的main幀的返回後的執行程式碼地址和棧底。因此只有在f2函式執行完後出棧後才會core。剛好這個例子是f2呼叫了f1後只調用了printf,f1幀的棧的面貌沒有被其它的呼叫清理掉,所以嘗試找f1幀的棧頭資訊時剛好可以找到,否則的話找到的會是f2中最後一個呼叫函式呼叫入棧寫入的$rbp值。

  • $rsp - 16是f1 frame中儲存的f2 frame的棧底地址;

  • 按頁對齊0x7fffffffe4d0的頁起始值是0x7fffffffe000,先搜尋的本頁地址也就是從0x7fffffffe000到0x7fffffffe4d0,之所以按頁來查詢也是因為前面的地址頁是否存在,實際中可能需要逐漸搜尋多頁。