banner
Zein

Zein

x_id

gdbベストプラクティスマニュアル

launch/attach process#

注意:gdb 中只需输入完整命令的前几个字母甚至首字母即可。前提是缩写不和其它命令有歧义

gdb ./a.out       # 从头调试尚未运行的程序

gdb a <pid>       # 绑定正在运行的进程
gdb -p <pid>      # 同上一条

gdb -q a
(gdb) attach <pid>      # 也可以先启动再绑定

(gdb) detach            # 如果通过gdb attach调试,detach后,原进程将继续执行
(gdb) q                 # 退出gdb

----------------静默启动选项:直接进入gdb,而非打印一堆版本版权信息
gdb -silent
gdb -q
#可在~/.bashrc中,为gdb设置一个别名:
alias gdb="gdb -q"

----------------静默退出设置:gdb退出时会提示:是否退出调试的程序
#A debugging session is active.
#
#    Inferior 1 [process 29686    ] will be killed.

#Quit anyway? (y or n) n

#静默退出:也可把这个命令加到.gdbinit文件里。
(gdb) set confirm off

----------------gdb启动时,指定被调试程序的参数
gdb -args ./a.out a b c
----------------也可在gdb中,通过命令来设置
(gdb) set args a b c
(gdb) show args
Argument list to give program being debugged when it is started is "a b c".
----------------还可在运行程序时指定
(gdb) r a b          #这似乎会被保存为参数列表,下次r时可能会带保存的参数列表,建议(gdb) show args看看

----------------进入gdb后再构建程序
(gdb) shell
(gdb) make CFLAGS="-g -O0"

用自动获取 pid 的脚本启动 attach debugging#

#!/bin/bash
# 指示使用 Bash 解释器来执行脚本
# 保存为 agdb.sh,并添加可执行权限(chmod +x xgdb.sh)
# 用法:./agdb.sh <program_name>

# 获取程序名,即第一个参数
prog_bin=\$1

# 检查是否传入了程序名
if [ -z "$prog_bin" ]; then
  echo "プログラムの名前またはパスを渡してください!"
  exit -1
fi

# 获取程序的 PID
running_name=$(basename $prog_bin)  # 提取程序名
pid=$(pidof $running_name)

# 检查PID是否为空
if [ -z "$pid" ]; then
  echo "実行中のプロセスが見つかりません:$running_name"
  exit -1
fi

# 附加到进程
echo "プロセス PID にアタッチしています: $pid..."
gdb -q -p $pid

远程调试#

(gdb) i target              #显示调试目标平台的信息
Remote target               #或Local target
  Connected to remote target via <connection info>   #端口号
  Architecture: <arch info>                          #目标架构(如x86、ARM 等)

(gdb) target remote localhost:25000                  #连接gdbsever

gdb 临时环境变量#

在 GDB 中设置只对 debugging function 生效的环境变量。通常是临时的,仅在调试会话期间有效。

(gdb) set env PATH=$PATH:/path/to/some/library
(gdb) set env LD_PRELOAD=$LD_PRELOAD:/path/to/some/library

(gdb) show env LD_PRELOAD            #查看gdb设置的临时环境变量
(gdb) unset env LD_PRELOAD

也可以在.gdbinit配置 gdb 启动时运行初始化命令:

set env LD_PRELOAD=$LD_PRELOAD:/path/to/some/library

运行控制#

(gdb) r arg1 arg2 ...             # 重新开始运行二进制
(gdb) start                   # 用于开始执行程序,并在程序的第一条指令处暂停执行,比如停在main{
(gdb) stop                        # 暂停运行
(gdb) c                           # 继续执行
(gdb) n                           # 单步执行,遇到函数则跳过
(gdb) s                           # 单步执行,遇到函数则跳入函数体

(gdb) until  line                 # 运行直到到达指定行

breakpoints 设置#

-----------------------------------breakpoints set
(gdb) b <file:line>                    # 设置文件中某行断点;通过行号设置断点的弊端:如果更改了源程序,那么之前设置的断点就可能不是你想要的
(gdb) b <file:function>                # 设置文件中某函数断点

(gdb) b <namespace::class::function>   # 设置某类的成员函数断点
(gdb) b (anonymous namespace)::<func>  # 设置匿名空间中的函数断点

(gdb) b <location> <thread-id>         # 设置某个线程在某处的断点
(gdb) b <location>  if <condition>     # 设置某处的条件断点

(gdb) b *0x400522                      # 调试汇编或无调试信息的程序时,经常要在程序地址上打断点

(gdb) tb a.c:15                        # tb打只生效一次的临时断点,断点命中一次后,就被删掉

-----------------------------------breakpoints 删除/启用/禁用
(gdb) d <break-id>                      # 删除某断点
(gdb) disable   <break-id>              # 禁用某断点
(gdb) enable    <break-id>              # 启用某断点

-----------------------------------breakpoints忽略,可用于调试循环
(gdb) ignore 1 5                        # 忽略断点1前五次触发
(gdb) ignore 1 0                        # 取消忽略

-----------------------------------breakpoints 设置自动执行命令
(gdb) command <break-id>           # 修改已存在的断点命令也是这条
>silent                            # 静默执行命令
>if x > 10
>p <var>                           # 运行到断点处时自动打印变量<var>
>end

(gdb) i breakpoints                # 用于查看断点命令
(gdb) delete                       # 用于删除断点命令

-----------------------------------breakpoints环境保存
(gdb) save breakpoints my_breakpoints.txt           # 将当前的断点保存到路径my_breakpoints.txt

(gdb) source my_breakpoints.txt           # 在下次启动 GDB 调试时,加载my_breakpoints.txt保存的断点

在程序入口处打断点#

strip a.out                #去掉调试信息
-----------------------------获取程序入口1:用 readelf 命令查看 ELF 文件头,找到 Entry point address:
readelf -h a.out
#Entry point address:               0x400440
-----------------------------获取程序入口2:(gdb) info files,找到 Entry point address:
(gdb) info files
#Entry point address:               0x400440
-----------------------------获取程序入口3:objdump -f a.out,找到 Entry point address:
objdump -f a.out
#Entry point address:               0x400440

-----------------------------在程序入口处打断点
(gdb) b *0x400440

快速尝试不同输入#

在不修改源码,不重新编译的情况下,通过断点命令 + 反向调试为函数尝试不同输入

(gdb) b drawing
Breakpoint 1 at 0x40064d: file win.c, line 6.
(gdb) command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent                         # 避免在断点停下时输出信息。
>set variable n = 0             # 修改输入
>c
>end

#看了结果后反向执行再改

watchpoint 设置#

写观察点:程序在某个变量(观察点)的值变化时暂停。
读观察点:当发生读取变量行为时,程序暂停

硬件观察点:设置观察点时,GDB 使用硬件级监控功能追踪变量值变化。不影响程序性能。但硬件观察点数量可能有上限,超限时,GDB 会使用软件观察点。
软件观察点:如硬件不支持观察点或已达到硬件观察点限制,GDB 用软件方式实现。通常影响程序运行速度,因为涉及到不断地检查和管理内存。

(gdb) watch a                            # 设置变量a为写观察点
(gdb) wa a thread 2                      # 只在 thread2线程 触发 a变量的观察点
(gdb) rw a                               # 读观察点,只对硬件观察点生效
(gdb) aw a                               # 读写观察点

(gdb) delete watchpoint 2

(gdb) disable watchpoint 2               # 禁用观察点2
(gdb) enable watchpoint 2

(gdb) set can-use-hw-watchpoints off     # 禁用硬件观察点
(gdb) set can-use-hw-watchpoints on

-------------------------------用内存地址设置观察点
(gdb) p &a                               # 列出a的地址
\$1 = (int *) 0x6009c8 <a>

(gdb) watch *(int*)0x6009c8              # 用内存地址的解引用设置观察点

catchpoint 设置#

catchpoint 用于捕捉特定事件发生(如信号、函数调用、系统调用等)时中断程序执行;catchpoint 默认是持久的,会在程序每次触发对应事件时中断。

(gdb) tcatch fork       # 设置为只捕获一次的临时捕捉点

(gdb) catch fork        # 捕获 fork() syscall;GNU/Linux支持这个功能。其他平台是否支持查官方gdb手册
(gdb) catch vfork
(gdb) catch exec
(gdb) catch syscall                         # 捕捉所有syscall
(gdb) catch syscall [syscall | number]      # 捕获 指定名称或编号 的syscall

破解 anti-debugging#

有些程序不想被 gdb 调试,就在程序中调用 “ptrace” 函数,一旦返回失败,就证明程序正在被 gdb 等类似的程序追踪,直接退出。

破解这类程序的办法就是为ptrace调用设置catchpoint,然后修改ptrace返回值

----------------------------eg
#include <sys/ptrace.h>
#include <stdio.h>
int main(){
    if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0 ) {
        printf("Gdb is debugging me, exit.\n");
        return 1;
    }
    printf("No debugger, continuing\n");
    return 0;
}

----------------------------(gdb) catch syscall ptrace
(gdb) catch syscall ptrace
Catchpoint 2 (syscall 'ptrace' [101])
(gdb) r                                                                                               # 重新运行
Starting program: /data2/home/nanxiao/a

Catchpoint 2 (call to syscall ptrace), 0x00007ffff7b2be9c in ptrace () from /lib64/libc.so.6
(gdb) c
Continuing.

Catchpoint 2 (returned from syscall ptrace), 0x00007ffff7b2be9c in ptrace () from /lib64/libc.so.6
(gdb) set $rax = 0                                                                                    #Linux64 syscall返回值存在rax
(gdb) c
Continuing.
No debugger, continuing
[Inferior 1 (process 11491) exited normally]

修改 machine states#

(gdb) set $var=XXX                # 设置gdb变量
----------------修改变量
(gdb) set var <varname> = <val>   # 修改变量

(gdb) set file::array="Jil"             # 修改字符串数组值,字符串常量在只读数据段,不可修改
(gdb) p &array                          # 修改内存的方式修改变量,但内存越界很危险
(gdb) set {char[4]} 0x80477a4 = "Jil"   # set {type}address=expr

----------------修改register
(gdb) set var $eax = 8          # 修改返回值寄存器eax
(gdb) set var $pc=0x08050949    # 只要你知道某条指令的地址,就能通过修改PC指令下一步跳转过去

----------------修改

----------------修改

----------------修改

跳转执行#

调试常规方法是设置断点并重新运行程序,跳过不影响结果的中间 machine states 修改过程,以及非关键过程到某位置执行,可节约时间;

gdb 跳转指令的原理是修改程序计数器 PC,使程序跳到指定行继续执行。jump 跳到指定执行点不暂停,若想 gdb 在该执行点暂停,需手动设置断点。

(gdb) tb <file>:<num>       # tb打只生效一次的临时断点,断点命中一次后,就被删掉

(gdb) j <num>               # 跳转到指定行号
(gdb) j <func>
(gdb) j <file>:<num>
(gdb) j <file>:<func>
(gdb) j *addr               # 跳转到某地址指向的指令

(gdb) j +<num> # 向后跳转n行
(gdb) j -<num> # 向前跳转n行

Time Travel Debugging#

记录每条 gdb 指令引起的 machine states 变化 (如变量、寄存器、内存的数据变化等),默认存入 gdb_record.process_id。当需回溯至过去状态时,调试器会按相反顺序逐条指令恢复这些状态

使用场景:
1)很难复现的 BUG,逻辑非常复杂的问题
2)多线程并发问题
3)内存相关的问题,如内存踩踏,内存泄漏、重复释放等
4)调用栈显示全是问号的问题

(gdb) record save filename                      # 把程序执行历史状态信息保存到文件,默认名字是gdb_record.process_id
(gdb) record restore filename                   # 从历史记录文件中恢复状态信息
(gdb) show record full insn-number-max          # 查看可记录执行状态信息的最大指令个数,默认200000
(gdb) set record full insn-number-max n         # 设置可记录最大指令数n
(gdb) set record full insn-number-max unlimited # 设置可记录最大指令数无限制

(gdb) set exec-direction reverse               # 设置程序逆向执行,执行此命令后,常用命令如next, nexti, step, stepi, continue、finish等都变成逆向执行
(gdb) set exec-direction forward               # 设置执行方向为正向

(gdb) b n
(gdb) record                                   # 记录程序执行过程中所有状态信息
(gdb) c

(gdb) reverse-search                           # 反向搜索曾经输入过的命令
(gdb) record goto start                        # 跳转到记录开始处
(gdb) record goto end                          # 跳转到记录结束处
(gdb) record goto n                            # 跳转到记录中的指定位置 n

(gdb) reverse-next                             # 逆向执行一行代码,遇函数调用不进入
(gdb) reverse-nexti                            # rni:逆向执行一条指令,遇到函数调用不进入
(gdb) reverse-step                             # rs:逆向执行一行代码,遇函数调用则进入
(gdb) reverse-stepi                            # rsi:反向单步执行(机器码级别)
(gdb) reverse-continue                         # rc:反向继续执行
(gdb) reverse-finish                           # 逆向执行,一直到函数入口处

(gdb) record stop                              # 停止记录状态信息

切换 shell 环境执行#

(gdb) shell                       # 进入shell模式,回到linux终端
(gdb) !<shell_cmd>                # 相当于(gdb) shell <shell_cmd>;
(gdb) pwd                         # 在gdb中切换工作目录
(gdb) cd tmp
(gdb) exit                        # 退出shell模式,回到gdb命令行

print#

输出设置#

指定 gdb 输入输出设备#

缺省情况下程序的输入输出和 gdb 使用同一个终端

tty                             #查看本终端

gdb -tty /dev/pts/2 ./a.out     #设置程序的输入输出

(gdb) tty /dev/pts/2            #设置程序的输入输出

打印字符串长度设置#

gdb 会限制打印字符串的最大长度。当 gdb 输出信息较多时,gdb 会暂停输出,并会打印 “---Type <return> to continue, or q <return> to quit---”;使用下列命令可修改限制:gdb 就会全部输出,中间不会暂停

(gdb) show print elements                          # 显示字符串最大打印长度

(gdb) set print elements <number-of-elements>      # 设置字符串最大打印长度

(gdb) set print elements 0                         # 取消字符串最大打印长度
(gdb) set print elements unlimited                 # 同上

(gdb) set pagination off   # 取消分页输出
(gdb) set height 0         # 取消输出信息最大高度限制

machine state#

---------------------------------------打印variables/打印stack
(gdb) whatis <var>                # 打印变量类型
(gdb) ptype <var>                 # 打印变量类型定义
(gdb) i variables                 # 打印当前作用域所有变量所在文件及类型
(gdb) i variables ^<var>$         # ^ 表示以<var>开头;$ 表示以<var>结尾

(gdb) p <var>                     # 打印变量<var>
(gdb) i locals                    # 列出当前所处函数栈帧中所有局部变量(包括静态局部变量)
(gdb) i variables                 # 列出所有的全局和静态变量
(gdb) i variables var             # 列出以正则var开头的全局和静态变量
(gdb) p 'file.c'::<var>           # 属于不同文件的extern全局/静态变量可能有相同变量名,此时要指定所属文件

(gdb) p sizeof(wchar_t)           # print wchar_t类型字节大小
(gdb) x/s str                     # 打印ASCII字符串或字符数组变量str
(gdb) x/hs str                    # 打印2字节符串或字符数组变量str
(gdb) x/ws str                    # 打印4字节符串或字符数组变量str

(gdb) p array[index]@num          # 从索引index开始,打印num个连续元素
(gdb) set print array-indexes on  # 打印数组时,缺省不打印索引下标;显示索引下标

(gdb) display <var>                                       #跟踪变量或表达式,每次程序暂停时自动显示指定<var>的值;也就是遇到断点、单步执行、或手动暂停时
(gdb) undisplay <var>                                     #取消跟踪变量

---------------------------------------打印protobuf message
(gdb) p <var>.DebugString()       # 用DebugString()将proto对象内部结构打印出来;

---------------------------------------打印CPUregister
(gdb) i r                         # 打印所有CPU寄存器的值,不包括浮点寄存器和向量寄存器
(gdb) i all-registers             # 打印所有CPU寄存器
(gdb) i r es                      # 打印CPU寄存器es的值
(gdb) p $es                       # 打印CPU寄存器es的值

(gdb) layout regs                 # tui模式下,显示寄存器窗口
(gdb) tui reg float               # 查看浮点寄存器
(gdb) tui reg general             # 查看通用寄存器

---------------------------------------打印memmory
# n:正整数,你想查看的内存单元的数量
#
# f:打印格式:
# - x: 十六进制
# - d: 十进制
# - u: 十六进制
# - o: 八进制
# - t: 二进制
# - a: 十六进制
# - c: 字符格式
# - f: 浮点数
#
# u: 单个内存单元大小:
# - b: 单字节
# - h: 双字节
# - w: 四字节
# - g: 八字节
#
# addr: 要打印的内存地址
(gdb) x/<nfu>  <addr>   # 打印内存地址,<addr>可以是某个变量的指针值;常用x/nxb x/nub x/ntb

和_和和__用于内存调试#

(gdb) x/16xb a                             # 打印内存
0x7fffffffe4a0: 0x00    0x01    0x02    0x03    0x04    0x05    0x06    0x07
0x7fffffffe4a8: 0x08    0x09    0x0a    0x0b    0x0c    0x0d    0x0e    0x0f

(gdb) p $_                                # $_存储最后一次用x命令检查的内存地址
\$1 = (int8_t *) 0x7fffffffe4af

(gdb) p $__                               # $__存储x命令最后检查的内存数据的值(通常是最后一个字节)
\$2 = 15

--------------------------------------一些 GDB 命令(如 i breakpoint)可能修改 $_ 值。
(gdb) i breakpoint                         # 这个命令生成一个匿名变量存储断点,地址0x00000000004004a0,值1
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004004a0 in main at a.c:13
        breakpoint already hit 1 time
(gdb) p $_
\$6 = (void *) 0x4004a0 <main+44>

打印进程的虚拟内存#

(gdb) i proc mappings                               #显示当前进程的虚拟内存,包括各段的地址范围、大小、偏移量及访问权限等
process 27676 flags:
PR_STOPPED Process (LWP) is stopped
PR_ISTOP Stopped on an event of interest
PR_RLC Run-on-last-close is in effect
PR_MSACCT Microstate accounting enabled
PR_PCOMPAT Micro-state accounting inherited on fork
PR_FAULTED : Incurred a traced hardware fault FLTBPT: Breakpoint trap

Mapped address spaces:
# Start Addr: 段起始地址;End Addr: 段结束地址;Size: 段大小;Offset: 偏移量;Flags: 权限(r 可读,w 可写,x 可执行)
    Start Addr   End Addr       Size     Offset   Flags
     0x8046000  0x8047fff     0x2000 0xfffff000 -s--rwx
     0x8050000  0x8050fff     0x1000          0 ----r-x
     0x8060000  0x8060fff     0x1000          0 ----rwx
    0xfee40000 0xfef4efff   0x10f000          0 ----r-x
    0xfef50000 0xfef55fff     0x6000          0 ----rwx
    0xfef5f000 0xfef66fff     0x8000   0x10f000 ----rwx
    0xfef67000 0xfef68fff     0x2000          0 ----rwx
    0xfef70000 0xfef70fff     0x1000          0 ----rwx
    0xfef80000 0xfef80fff     0x1000          0 ---sr--
    0xfef90000 0xfef90fff     0x1000          0 ----rw-
    0xfefa0000 0xfefa0fff     0x1000          0 ----rw-
    0xfefb0000 0xfefb0fff     0x1000          0 ----rwx
    0xfefc0000 0xfefeafff    0x2b000          0 ----r-x
    0xfeff0000 0xfeff0fff     0x1000          0 ----rwx
    0xfeffb000 0xfeffcfff     0x2000    0x2b000 ----rwx
    0xfeffd000 0xfeffdfff     0x1000          0 ----rwx

(gdb) i files                 #列出各段在内存中的位置,以及所加载共享库的.text,数据段、未初始化数据段.bss内容映射到虚拟内存的位置
Symbols from "/data1/nan/a".
Unix /proc child process:
    Using the running image of child Thread 1 (LWP 1) via /proc.
    While running this, GDB does not access memory from...
Local exec file:
    `/data1/nan/a', file type elf32-i386-sol2.
    Entry point: 0x8050950
    0x080500f4 - 0x08050105 is .interp
    0x08050108 - 0x08050114 is .eh_frame_hdr
    0x08050114 - 0x08050218 is .hash
    0x08050218 - 0x08050418 is .dynsym
    0x08050418 - 0x080507e6 is .dynstr
    0x080507e8 - 0x08050818 is .SUNW_version
    0x08050818 - 0x08050858 is .SUNW_versym
    0x08050858 - 0x08050890 is .SUNW_reloc
    0x08050890 - 0x080508c8 is .rel.plt
    0x080508c8 - 0x08050948 is .plt
    ......
    0xfef5fb58 - 0xfef5fc48 is .dynamic in /usr/lib/libc.so.1
    0xfef5fc80 - 0xfef650e2 is .data in /usr/lib/libc.so.1
    0xfef650e2 - 0xfef650e2 is .bssf in /usr/lib/libc.so.1
    0xfef650e8 - 0xfef65be0 is .picdata in /usr/lib/libc.so.1
    0xfef65be0 - 0xfef666a7 is .data1 in /usr/lib/libc.so.1
    0xfef666a8 - 0xfef680dc is .bss in /usr/lib/libc.so.1

打印 heap#

在.gdbinit 中添加自定义打印内存分配命令

define mallocinfo
  set $__f = fopen("/dev/tty", "w")    # 打开 /dev/tty 文件进行输出
  call malloc_info(0, $__f)            # 调用 malloc_info 函数将内存分配信息输出到文件
  call fclose($__f)                    # 关闭文件
end

调试程序并检查内存分配:

(gdb) mallocinfo
<malloc version="1">                      #使用的内存分配函数的版本
<heap nr="0">                             #第一个堆,多线程程序会为每个线程分配一个独立的堆,减少线程间竞争

<sizes>                                   #不同大小的内存块的分配情况,在示例程序中,没有分配内存块
</sizes>

<total type="fast" count="0" size="0"/>   #快速分配和其它类型内存块,count="0"表示当前没有分配内存块,size="0"表示占用内存大小为0。
<total type="rest" count="0" size="0"/>   #这部分是针对malloc内部实现的,通常用来优化内存分配,区分不同的内存池。
<system type="current" size="1134592"/>   #当前从os请求并分配的内存
<system type="max" size="1134592"/>       #so允许程序最大能分配的内存,说明程序当前没有超过系统分配的限制。
<aspace type="total" size="1134592"/>     #地址空间的大小
<aspace type="mprotect" size="1134592"/>  #mprotect 是一个系统调用,用于改变某一段内存的访问权限,用来保护内存不被非法修改
</heap>

<total type="fast" count="0" size="0"/>
<total type="rest" count="0" size="0"/>
<system type="current" size="1134592"/>
<system type="max" size="1134592"/>
<aspace type="total" size="1134592"/>
<aspace type="mprotect" size="1134592"/>
</malloc>

多态接口中按实参类型打印形参变量#

#include <iostream>       //示例代码
using namespace std;

class Shape {
public:  virtual void draw () {}
};

class Circle : public Shape {
 int radius;
 public:
  Circle () { radius = 1; }
  void draw () { cout << "drawing a circle...\n"; }
};

class Square : public Shape {
 int height;
 public:
  Square () { height = 2; }
  void draw () { cout << "drawing a square...\n"; }
};

void drawShape (class Shape &p){          //暴露一个多态的接口,依据传入对象类型,调用不同派生类重载的draw 实现
  p.draw ();
}

int main (void){
  Circle a;
  Square b;
  drawShape (a);
  drawShape (b);
  return 0;
}

gdb 默认按声明类型打印对象,则打印上面多态接口 p 的类型总是 Shape ;我们希望在这个多态接口函数栈帧中按实参类型打印形参变量

(gdb) set print object on

GDB Pretty Printers#

STL 容器内部使用了复杂的内存布局,GDB 直接打印 STL 容器会显示一堆底层实现细节;GCC 提供 Python 脚本 (GDB 7.0 始功能) 增强 GDB 对 C++ 容器的打印功能

gcc 高版本 (>4.6.0) 情况下会自带 pretty printer:/usr/share/gcc/python
gcc.gnu.org Git - gcc.git/tree - libstdc++-v3/python/libstdcxx/

1) 将 Pretty Printers 脚本加到 cpp 项目的.gdbinit

python
import sys
sys.path.insert(0, '/usr/share/gcc/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end

2)

(gdb) i pretty-printer      #查看是否成功启用了STL容器Pretty Printers

(gdb) set print pretty on

dependencies#

shared library#

(gdb) i sharedlibrary           # 列出程序加载的所有共享链接库
(gdb) i sharedlibrary hiredi    # 列出 符合正则hiredi 的所有共享链接库

macro#

gcc -g3以下编译生成的程序不包含预处理器宏信息:

(gdb) p NAME No symbol "NAME" in current context.

如果想在 gdb 中查看宏信息,可以使用 gcc -g3 进行编译:

(gdb) p NAME
$1 = "Joe"

points#

(gdb) i b                          # 查询所有断点

(gdb) i watch                      # 列出所有观察点

source#

gdb 命令行状态下 CTRL + X + A 调出代码窗口 tui,再按一次退出代码窗口。

(gdb) dir <source_directory>         # 添加查找source的路径
(gdb) set substitute-path /old /new  # 把旧的source检索路径替换为新的路径

(gdb) l                         # print暂停处代码
(gdb) l   <function>            # print函数function的代码
(gdb) l   <file:function>       # print文件file的函数function代码
(gdb) l   <file:line>           # print文件file中指定行数line代码
(gdb) l -                       # 向后打印
(gdb) l +

(gdb) focus                     #tui模式
gdb -tui program                #以tui模式启动
----------------调整tui窗口大小
win <win_name> +7           #win_name可以是src、cmd、asm、regs窗口;+或-显示的行数

gdb info#

(gdb) show version            #查看gdb版本信息
(gdb) show copying            #查看gdb版权相关信息
(gdb) show warranty           #查看gdb免责声明

gdb -help            #帮助信息

(gdb) apropos set    #查找所有符合set正则表达式的命令信息
(gdb) help b         #help command命令得到某一个具体命令用法

function debugging#

栈帧跟踪#

stack frame:栈帧保持函数执行环境,帧指针Frame Pointer 指向当前栈帧底部,栈帧基址 Stack Pointer始终指向栈帧顶部。

每次函数调用,都会在调用栈call stack 上维护一个栈帧stack frame。每个独立的栈帧一般包括:
Stack Pointer→
)local variables
)register
)Frame Pointer
)return address 当前函数过程的下一条指令地址
) arguments 传入参数创建的形参变量
)previous Frame Pointer
Frame Pointer→

image

(gdb) i functions          #列出可执行文件被定义的函数,以及引用的的链接函数和系统函数
(gdb) i functions thre*    #列出名称匹配 正则表达式thre 的函数,正则后面的*表示是否带调试信息

(gdb) bt                      # 打印 程序执行到当前断点的 函数调用栈
(gdb) frame                   # 显示当前所在调用栈层次、所在文件及行号等

(gdb) frame 2                 # 选择 第二层栈帧 ,查看其中变量
(gdb) up 1                    # 切换到上一个栈帧,也就是previous 调用栈帧
(gdb) down 2
(gdb) up-silently n           # 静默切换版
(gdb) down-silently n
(gdb) frame addr              # 选择 帧数地址为addr的栈帧 ,也就是下面的called by frame 指向的上一个栈帧的帧底previous Frame Pointer

(gdb) i frame                 # 打印当前选择的 函数栈帧内容,默认打印最上层
Stack level 0, frame at 0x7fffffffe590:                          #最上层栈帧,Stack Pointer
rip = 0x40054e in func (a.c:5);                                  #return address指向func下一条要执行的指令
saved rip = 0x400577                                             #上一栈帧的return address,即调用func的地方,也是当前函数执行完毕后,应该跳转到的地址
called by frame at 0x7fffffffe5a0                                #上一个栈帧的Frame Pointer
source language c.                                               #
Arglist at 0x7fffffffe580, args: a=1, b=2                        #形参变量
Locals at 0x7fffffffe580, Previous frame's sp is 0x7fffffffe590  #局部变量,上一个栈帧的Stack Pointer,这里由于是最上层栈帧,指向自己
Saved registers:                                                 #
rbp at 0x7fffffffe580, rip at 0x7fffffffe588                     #寄存器

---------------------print尾调用堆栈帧
#开启O1的Tail call优化选项编译:当一系列函数最后一条指令是调用另外一个函数的情况优化为:函数直接调用了最终函数,而中间函数在汇编层面被优化掉了
(gdb) set debug entry-values 1   # 设置:除输出正常函数堆栈帧信息外,还输出尾调用相关信息

---------------------打印函数调用栈各层局部变量
(gdb) bt                      # 打印 程序执行到当前断点的 函数调用栈
#0  fun_a () at a.c:6
#1  0x000109b0 in fun_b () at a.c:12
(gdb) bt full                 # 打印 各层局部变量
#0  fun_a () at a.c:6
        a = 0
#1  0x000109b0 in fun_b () at a.c:12
        b = 1
(gdb) bt full n               # 打印 最上n层的局部变量
(gdb) bt full -n              # 打印 最下n层的局部变量

函数执行控制#

(gdb) finish                        # 运行直到跳出当前函数
(gdb) return val                    # 不执行函数剩下的语句,直接返回val值

(gdb) call func()                   # 停在某处时运行此语句,直接调用执行文件中定义的func函数
(gdb) print func()                  # 停在某处时运行此语句,直接调用执行文件中定义的func函数

进入不带调试信息的函数#

默认情况下,(gdb) s不会进入不带调试信息的函数,并如同(gdb) n那样跳过;

不带调试信息的原因:
1)没开启-g生成调试信息,特别是链接的共享库,提供方编译时就没开启-g
2)-O2-O3优化掉了
3)内联函数被编译器嵌入调用位置,因此不会在调试中作为独立函数出现。可通过编译选项禁用内联优化

(gdb) set step-mode on   #设置不跳过没有调试信息的函数,即使没调试信息,也可以先进入,然后开启汇编调试

$_exitcode 用于打印程序退出状态码#

(gdb) p $_exitcode

进 / 线程调试#

进 / 线程执行控制#

默认情况下,gdb 检测到有线程产生和退出时,会打印提示信息

(gdb) set print thread-events off              # 关闭打印提示

#避免在调试一个线程时,其他线程的执行会干扰你的调试过程
(gdb) set scheduler-locking off              # 默认,gdb只控制调试的单线程运行,其他线程的调度是独立的
(gdb) set scheduler-locking on               # 锁定调度,当调试一个线程时,其他线程阻塞,直到显式切换线程或完成调试。
(gdb) set scheduler-locking step             # 用 step 单步调试时,其他线程会被暂停;用 next 调试,其他线程可能继续执行。

(gdb) thread apply <id1><id2> command    # 指定线程集执行gdb命令
(gdb) thread apply all command           # 指定线程集执行gdb命令

进程调试#

调试多进程程序时,GDB 默认控制父进程运行而子进程独立运行。

---------------------------------调试非锁定调度的fork进程:一个进程gdb控制执行,一个独立运行
(gdb) set follow-fork-mode child    # GDB在fork()暂停,如果(gdb) n跟踪的就是父进程,此时可以告诉gdb跟踪子进程;此命令在非linux上不知道是否支持
(gdb) start
(gdb) n

(gdb) set follow-fork-mode parent   # 默认控制父进程运行(默认)

(gdb) i inferiors                   # 显示当前调试的所有进程,包括父进程和子进程
(gdb) inferior <num>                # 切换到指定编号的进程num调试
(gdb) attach <pid>                  # 相当于切换跟踪

---------------------------------调试锁定调度的的fork进程:父子进程由gdb控制为交替执行,并发调度的
(gdb) set detach-on-fork on           # 默认

(gdb) set detach-on-fork off          # 同时调试父子进程
(gdb) start                           # 程序会在fork()调用处停下来。
(gdb) i inferiors                     # 显示当前调试的所有进程,包括父进程和子进程
(gdb) inferior <num>                  # 切换到指定编号的进程num调试
(gdb) n

---------------------------------调试可并行的父子进程:
(gdb) set schedule-multiple off   # 默认

(gdb) set detach-on-fork off      # 可同时调试父子进程
(gdb) set schedule-multiple on    # 执行到 fork() 时,父子进程会并行运行
(gdb) start

---------------------------------调试多进程
gdb a
(gdb) start
(gdb) add-inferior -copies 2 -exec b       #-copies复制进程数,-exec指定要加载的可执行文件
(gdb) i inferiors
  Num  Description       Executable
  3    <null>            /home/nanxiao/b
  2    <null>            /home/nanxiao/b
* 1    process 1586      /home/nanxiao/a
(gdb) inferior 2                           #调试进程2
(gdb) clone-inferior -copies 1             #克隆当前调试的程序
(gdb) i inferiors
  Num  Description       Executable
  4    <null>            /home/nan/b
  3    <null>            /home/nan/b
* 2    process 1590      /home/nan/x
  1    process 1586      /home/nan/a

打印进程空间#

--------------------------打印调试会话中所有进程空间
(gdb) maint info program-spaces    # 打印调试会话中所有进程空间,包括进程空间号与所绑定的进程id
  Id   Executable
  4    /home/nan/b
        Bound inferiors: ID 4 (process 0)
  3    /home/nan/b
        Bound inferiors: ID 3 (process 0)
* 2    /home/nan/b
        Bound inferiors: ID 2 (process 15902)
  1    /home/nan/a
        Bound inferiors: ID 1 (process 15753)

线程调试#

(gdb) i threads                   # 查看当前程序中的所有线程编号,线程id,当前所处调用栈函数
(gdb) i threads 1 2               # 查看指定线程

(gdb) thread 2                    # GDB 切换到线程 2调试,并显示该线程当前的调用栈函数

(gdb) wa a thread 2               # 只在 thread2线程 触发 a变量的观察点

pstack <pid>      # 可事先dump某个进程下所有线程的thread id和backtrace,方便gdb调试

$_thread 用于线程调试#

#可以帮助你快速定位并发程序中的潜在问题,比如竞态条件、死锁等
(gdb) watch a                        #监控变量a
(gdb) command 2                      #设置当编号2的断点触发时自动执行gdb命令:打印修改a的线程
> printf "thread id=%d\n", $_thread  #$_thread 会被替换为当前线程的线程 ID
> end

Solaris 专用线程调试 cmd#

(gdb) maint info sol-threads      # 查看当前程序中的所有线程:类型,编号,对应lwp,,当前线程状态,,该线程入口函数
user   thread #1, lwp 1, (active)
user   thread #2, lwp 2, (active)    startfunc: monitor_thread
user   thread #3, lwp 3, (asleep)    startfunc: mem_db_thread
- Sleep func: 0x000aa32c          # 如果线程处于休眠状态,还会显示休眠函数的地址Sleep func

Asynchronous Debugging#

同步任务是按特定顺序执行的操作(如多线程按顺序互斥修改内存),常见的同步机制 (锁机制) 会在执行过程中阻塞其他任务

异步任务是含有需要等待的操作,异步任务要高效利用系统资源:需要在进行需等待的操作时让出 cpu,然后在操作完成时通过某种机制(如信号,事件循环、回调、线程池等)抢占当前正在执行的任务

signals debugging#

GDB 提供了强大的信号处理控制功能

(gdb) i signals                                             # 查看当前调试进程对每个信号的默认处理方式
Signal        Stop      Print   Pass to program Description # 信号名、捕获信号是否停止程序、是否打印信息、GDB是否会传递给程序的自定义handler处理、描述
SIGHUP        Yes       Yes     Yes             Hangup
SIGINT        Yes       Yes     No              Interrupt   # 这个信号不会传递给程序处理,而是用于进程控制的信号
SIGQUIT       Yes       Yes     Yes             Quit
...
SIGALRM       No        No      Yes             Alarm clock

(gdb) handle <Signal> nostop # 捕获 SIGHUP 信号不停止程序
(gdb) handle <Signal> stop

(gdb) handle <Signal> noprint
(gdb) handle <Signal> print

(gdb) handle <Signal> nopass #是否传递给程序,让程序用自定义handle处理
(gdb) handle <Signal> pass

(gdb) signal <Signal>        # 给给调试程序发送信号,这比换到shell用kill发送方便
(gdb) signal 0               # 当程序暂停后,用这条使程序重新运行

$_siginfo 处理异常退出#

$_siginfo 保存当前收到信号的详细信息。在处理诸如 SIGSEGV(段错误)或 SIGFPE(算术错误)等信号时,特别有用

(gdb) ptype $_siginfo
type = struct {
    int si_signo; #信号编号,指示接收到的信号类型。
    int si_errno; #信号相关的错误信息(通常为0)。
    int si_code;  #kernel给的附加信息,描述信号的具体原因。
    union {
        int _pad[28];
        struct {...} _kill;
        struct {...} _timer;
        struct {...} _rt;
        struct {...} _sigchld;
        struct {...} _sigfault;
        struct {...} _sigpoll;
    } _sifields; #根据不同类型的信号,包含不同的结构体信息,提供更多信号特定的数据。
}
(gdb) ptype $_siginfo._sifields._sigfault
type = struct {
    void *si_addr;
}
(gdb) p $_siginfo._sifields._sigfault.si_addr    #引发查看段错误的内存地址
\$4 = (void *) 0x850e

死锁调试#

core dump#

ulimit -a
-a  显示目前资源限制的设定。
-c <core文件上>  设定core文件的最大值,单位为区块。
-d <数据节区大>  程序数据节区的最大值,单位为KB。
-f <文件大>  shell所能建立的最大文件,单位为区块。
-H  设定资源的硬性限制,也就是管理员所设下的限制。
-m <内存大>  指定可使用内存的上限,单位为KB。
-n <文件数>  指定同一时间最多可开启的文件数。
-p <缓冲区大>  指定管道缓冲区的大小,单位512字节。
-s <堆叠大>  指定堆叠的上限,单位  为KB。
-S  设定资源的弹性限制。
-t <CPU时>  指定CPU使用时间的上限,单位为秒。
-u <程序数>  用户最多可开启的程序数目。
-v <虚拟内存大>  指定可使用的虚拟内存上限,单位为KB。

一般情况下,当设置了ulimit -c unlimited; 当程序出现严重错误(如段错误、非法指令等)时,将程序 machine states 转储到 core 文件,进行故障排查。

ulimit -c            # 查看当前系统是否允许生成 core 文件;返回0表示系统禁用 core 文件生成。输出正整数表示系统允许生成的cool文件字节大小
ulimit -c unlimited  # 调整生成 core 文件的大小限制
echo "/coredump/core.%e.%p" > /proc/sys/kernel/core_pattern  #编辑/etc/sysctl.conf或/proc/sys/kernel/core_pattern文件设置core文件保存路径,文件名为core.<程序名>.<PID>

-------------生成core
(gdb) generate-core-file                # 生成core文件,记录当前调试进程状态。默认生成的 core 文件名为 core.<PID>
(gdb) generate-core-file my_core_dump   # 为 core 文件指定一个自定义的文件名

(gdb) gcore                             # gcore工具与gdb内部generate-core-file 命令作用一样,默认生成的 core dump 文件名为 core.<PID>
(gdb) gcore my_core_dump

#gcore工具可对正在运行的进程进行 core dump(即使该进程没有崩溃),生成该进程的内存快照:
ps aux | grep <your_program>            # 找到进程的pid
gcore <pid>                             # 生成core.<PID>文件

-------------------分析core
gdb -c core.a.1402638140 ./a.out                # 调试./a.out 生成的 core 文件 core
(gdb) bt full                                   # 查看异常的backtrace,查看崩的地方
(gdb) i locals

#或者像下面那样加载coredump
gdb -q
(gdb) file ./a.out
(gdb) core /coredump/core.a.1402638140

汇编调试#

print assembly#

(gdb) disass                    # print当前汇编指令
(gdb) disassemble func          # print func函数汇编指令
(gdb) disas /m func             # 将源代码与汇编指令一起显示
(gdb) disassemble /r func       # 以16进制print func函数机器码
(gdb) disassemble /mr func      # 汇编+机器码

(gdb) i line 13                             # 查看该行涉及的汇编指令的起始和结束地址
Line 13 of "foo.c" starts at address 0x4004e9 <main+37> and ends at 0x40050c <main+72>.
(gdb) disassemble 0x4004e9, 0x40050c        # 查看地址范围对应的汇编指令

(gdb) set disassemble-next-line off
(gdb) set disassemble-next-line on    # 自动反汇编后面要执行的代码
(gdb) set disassemble-next-line auto  # 后面的代码没有源码的情况下才反汇编后面要执行的代码,一般是一些没调试信息的函数
(gdb) set disassembly-flavor intel # intel格式,同理att则为at&t格式
(gdb) display /ni $pc                # 每次回到gdb命令行时,只把将要执行的n条汇编指令打印出来
(gdb) undisplay

(gdb) layout asm                #tui模式下显示汇编代码窗口
(gdb) layout split              #tui模式下汇编和source双窗口

汇编调试运行控制#

(gdb) si    # 单步执行一条汇编指令
(gdb) ni    # 不跳入

在函数的第一条汇编指令打断点#

-------eg:
#include <stdio.h>
int global_var;

void change_var(){
    global_var=100;
}

int main(void){
    change_var();
    return 0;
}

给函数打断点的命令:“b func”,不会把断点设在汇编指令层次函数的开头:

(gdb) b main
Breakpoint 1 at 0x8050c12: file a.c, line 9.
(gdb) r
Starting program: /data1/nan/a
[Thread debugging using libthread_db enabled]
[New Thread 1 (LWP 1)]
[Switching to Thread 1 (LWP 1)]

Breakpoint 1, main () at a.c:9
9           change_var();
(gdb) disassemble
Dump of assembler code for function main:
   0x08050c0f <+0>:     push   %ebp
   0x08050c10 <+1>:     mov    %esp,%ebp
=> 0x08050c12 <+3>:     call   0x8050c00 <change_var>
   0x08050c17 <+8>:     mov    $0x0,%eax
   0x08050c1c <+13>:    pop    %ebp
   0x08050c1d <+14>:    ret
End of assembler dump.

断点前加 * 才能在能把断点设置在汇编指令层次函数的开头,“b *func”:

(gdb) b *main
Breakpoint 1 at 0x8050c0f: file a.c, line 8.
(gdb) r
Starting program: /data1/nan/a
[Thread debugging using libthread_db enabled]
[New Thread 1 (LWP 1)]
[Switching to Thread 1 (LWP 1)]

Breakpoint 1, main () at a.c:8
8       int main(void){
(gdb) disassemble
Dump of assembler code for function main:
=> 0x08050c0f <+0>:     push   %ebp
   0x08050c10 <+1>:     mov    %esp,%ebp
   0x08050c12 <+3>:     call   0x8050c00 <change_var>
   0x08050c17 <+8>:     mov    $0x0,%eax
   0x08050c1c <+13>:    pop    %ebp
   0x08050c1d <+14>:    ret
End of assembler dump.

修改被调试程序的二进制文件#

修改被调试程序的二进制文件 | 100 个 gdb 小技巧

gdb logging#

gdb 默认不开启日志,若开启了,则当前目录下生成 gdb.txt 记录 gdb 命令行所有输出结果,方便回溯历史。

(gdb) show logging               #查看日志设置

(gdb) set logging on             # 设置开启gdb日志
(gdb) set logging off
(gdb) set logging file log.txt   # 修改日志文件路径为log.txt

(gdb) set logging redirect on    #GDB 会话中的输出(包括命令的执行结果和调试信息)将被写入日志文件,而不是在终端显示。
(gdb) set logging redirect off

(gdb) set logging overwrite on   #每次启动 GDB 并开启日志记录时,如果日志文件已经存在,则以覆写模式记录
(gdb) set logging overwrite off  #追加模式记录

.gdb_history#

gdb 缺省不保存历史命令;若保存,则历史命令缺省保存在当前目录下的.gdb_history

(gdb) set history save on              # 开启gdb历史cmd保存
(gdb) set history filename ~/.gdbcmd   # 设置保存路径

.gdbinit 模板#

gdb 启动时,会执行 HOME 目录和当前目录下.gdbinit 脚本里的命令

c 项目#

cpp 项目#

# 打印STL容器中的内容
python
import sys
sys.path.insert(0, "/home/xmj/project/gcc-trunk/libstdc++-v3/python")
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers (None)
end

# 保存历史命令
set history filename ~/.gdb_history
set history save on

# 退出时不显示提示信息
set confirm off

# 按照派生类型打印对象
set print object on

# 打印数组的索引下标
set print array-indexes on

# 每行打印一个结构体成员
set print pretty on

内存调试#

todo#

gdb 多窗口管理
查看对象类型
C++ 跨平台多线程
多线程调试管理
查找线程、线程断点
为线程执行命令
线程日志信息控制
调用内部及外部函数
跳过函数
制作、调试发行版
为软件制作补丁

内存泄漏检测
内存检查
远程调试
死锁调试
core dump 基础
栈溢出 core dump 分析
无调试符号 coredump 分析
软件 “破解”

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。