了解和使用GDB调试-基础

启动调试

1. 哪些程序可以被调试

  • 对于C和C++程序,编译时加上-g参数,会保留调试信息,否则无法使用GDB进行调试。

2. 如何判断文件是否可以调试

  • 直接使用gdb 文件名运行,如果不可调试则会有相应提示。
  • readelf查看段信息:readelf -S helloWorld|grep debug
  • file查看strip状况:file helloworld,如果最后提示为stripped,则说明文件的符号表信息和调试信息以及被去除。但是如果未 not stripped,也并不意味着一定可以进行gdb调试。

3. 无参程序启动调试

$ gdb helloWorld
(gdb)
(gdb) run

输入run命令,即可直接运行程序。

4. 带参程序启动调试

  • 方式1:run命令时带上所需参数即可
$ gdb hello
(gdb)run Argument1
  • 方式2:run命令前,使用set args
$ gdb hello
(gdb) set args Argument1
(gdb) run

5. 调试core文件

core文件介绍:https://www.jianshu.com/p/e38a3f1cf7f7

  • Linux下程序异常退出时,内核在当前工作目录下生成core文件,记录当时的内存映像和调试信息。可以使用gdb来查看core文件。
  • 产生core文件的前提是编译时带上了-g参数,并且core文件生成没有收到限制。
  • 关注core文件的生成开关和大小限制。
  • 关注core文件的名词和生成路径。
$ gdb [exec file] [core file]

6. 调试已运行程序

通过ps命令以运行的特点程序进程id

$ ps -ef|grep 进程名

或者可以自己编写测试文件,利用bg和fg来切换前后台。

使用attach直接调试相关进程id的进程,如果提示没有权限,可以sudo gdb

$ gdb
(gdb) attach 20829

7. 已运行程序且无调试信息

为了节省磁盘空间,已经运行的程序通常没有调试信息。并且不能停止当前程序进行重新编译调试,此时可以利用同样的代码,再编译一个带调试信息的版本。

$ gdb
(gdb) file hello
Reading symbols from hello...done.
(gdb)attach 20829

断点设置

1. 查看已设置的断点

命令info breakpoints 查看已设置的断点。

2. 根据行号设置断点

两种方式任一:

b 9  #break 可简写为b
b test.c:9

3. 根据函数名设置断点

当初程序调用到函数funcName时会断点

b funcName

4. 根据条件设置断点

特定行数 + 变量判断 组成条件设置,如果该条件成立,则形成断点可进行观察:

break test.c:23 if b==0

含义为:当b等于0时,程序将在23行断点。

condition命令有着类似作用:

condition 1 b==0 

含义为:当b等于0时,产生断点1。

5. 根据规则设置断点

#用法:rbreak file:regex
rbreak . 
rbreak test.c:. #对test.c中的所有函数设置断点
rbreak test.c:^print #对以print开头的函数设置断点

6. 设置临时断点

在某处的断点只生效一次,则可以设置临时断点:

tbreak test.c:l0  #在第10行设置临时断点

7. 跳过多次设置断点

对于某个断点处,前30次不会发生问题,可以跳过前30次:

ignore 1 30

其中1为通过info breakpoints查询的断点序号。

8. 根据表达式值变化来产生断点

观察某个特定表达式或者值,当其发生变化时,产生断点并打印相关内容:

watch a  
# 当产生值变化时,会打印出
Hardware watchpoint 2: a
Old value = 12
New value = 11

rwatch和awatch同样可以设置观察点前者是当变量值被读时断住,后者是被读或者被改写时断住。

9. 禁用、启用、删除断点

对于暂时不需要使用,但是也不可删除的断点,可以选择暂时禁用

disable  #禁用所有断点
disable bnum #禁用标号为bnum的断点
enable  #启用所有断点
enable bnum #启用标号为bnum的断点
clear   #删除当前行所有breakpoints
clear function  #删除函数名为function处的断点
clear filename:function #删除文件filename中函数function处的断点
clear lineNum #删除行号为lineNum处的断点
clear f:lename:lineNum #删除文件filename中行号为lineNum处的断点
delete  #删除所有breakpoints,watchpoints和catchpoints
delete bnum #删除断点号为bnum的断点

变量查看

1. 打印基本数据类型:变量、数组、字符串

使用print(可简写为p)打印变量内容:

(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "hello,shouwang"
(gdb)

可以在前面加上函数名或者文件名来区分同名变量:

(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main'::b
$2 = {1, 2, 3, 5}
(gdb) 

2. 打印指针指向内容

  • 如果以打印普通变量的形式打印指针,则会打印出指针地址:
(gdb) p d
$1 = (int *) 0x602010
  • 若需要打印指针所指向的内容,需要解引用:
(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb) 

通过@符号跟上想要打印的长度。

  • $在gdb中为上一个变量
(gdb) p *linkNode
(这里显示linkNode节点内容)
(gdb) p *$.next
(这里显示linkNode节点下一个节点的内容)
  • 设置gdb变量和使用累加
(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3

3. 按照特定格式打印变量

  • x 按十六进制格式显示变量。
  • d 按十进制格式显示变量。
  • u 按十六进制格式显示无符号整型。
  • o 按八进制格式显示变量。
  • t 按二进制格式显示变量。
  • a 按十六进制格式显示变量。
  • c 按字符格式显示变量。
  • f 按浮点数格式显示变量。
(gdb) p/x c
$19 = {0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x73, 0x68, 0x6f, 0x75, 0x77, 0x61, 
  0x6e, 0x67, 0x0}
(gdb)

4. 查看内存内容和寄存器内容

examine(简写为x)可以用来查看内存地址中的值:

x/[n][f][u] addr

其中:

  • n 表示要显示的内存单元数,默认值为1
  • f 表示要打印的格式,前面已经提到了格式控制字符
  • u 要打印的单元长度
    • b 字节
    • h 半字,即双字节
    • w 字,即四字节
    • g 八字节
  • addr 内存地址
(gdb) x/4tb &e  # &e开始的4块字节内存,以二进制打印
0x7fffffffdbd4:    00000000    00000000    00001000    01000001
(gdb) 

命令info registers可以查看寄存器内容:

(gdb)info registers
rax            0x0    0
rbx            0x0    0
rcx            0x7ffff7dd1b00    140737351850752
rdx            0x0    0
rsi            0x7ffff7dd1b30    140737351850800
rdi            0xffffffff    4294967295
rbp            0x7fffffffdc10    0x7fffffffdc10

5. 断点时自动打印变量内容

若希望程序在断点时自动打印某个变量的值,可以使用display命令:

(gdb) display e
1: e = 8.5

想要查看哪些变量被设置了display:

(gdb)into display

想要清除:

delete display num #num为前面变量前的编号,不带num时清除所有。

单步调试

1. 打印当前调试程序的源码

(gdb) list

2. 单步执行程序 next

若当前已经启动调试,停在某个断点处,使用next命令(简写为n)可以继续往下执行下一条语句。若跟上数字number,则表示执行number次:

$ gdb gdbStep   #启动调试
(gdb)b 25       #将断点设置在12行
(gdb)run        #运行程序
Breakpoint 1, main () at gdbStep.c:25
25        int b = 7;
(gdb) n     #单步执行
26        printf("it will calc a + b\n");
(gdb) n 2   #执行两次
it will calc a + b
28        printf("%d + %d = %d\n",a,b,c);
(gdb) 

3. 单步进入 step

若需要跟着进入函数内部查看情况,可以使用step命令(简写为s),单步跟踪导函数内部,前提为该函数有调试信息和源码信息。

$ gdb gdbStep    #启动调试
(gdb) b 25       #在12行设置断点
Breakpoint 1 at 0x4005d3: file gdbStep.c, line 25.
(gdb) run        #运行程序
Breakpoint 1, main () at gdbStep.c:25
25        int b = 7;
(gdb) s          
26        printf("it will calc a + b\n");
(gdb) s     #单步进入,但是并没有该函数的源文件信息
_IO_puts (str=0x4006b8 "it will calc a + b") at ioputs.c:33
33    ioputs.c: No such file or directory.
(gdb) finish    #继续完成该函数调用
Run till exit from #0  _IO_puts (str=0x4006b8 "it will calc a + b")
    at ioputs.c:33
it will calc a + b
main () at gdbStep.c:27
27        int c = add(a,b);
Value returned is $1 = 19
(gdb) s        #单步进入,现在已经进入到了add函数内部
add (a=13, b=57) at gdbStep.c:6
6        int c = a + b;

s命令会尝试进入函数,但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。

  • s命令可以设置选项,选择是否默认跳过没有调试信息的函数:
(gdb) show step-mode 
Mode of the step operation is off.
(gdb) set step-mode on
(gdb) set step-mode off
  • s命令为每次执行一条程序语句,可以使用stepi(简写为si),每次执行一条机器指令。

4. 继续执行到下一个断点 continue

使用continue命令(可简写为c),会继续执行当前程序,直到再次遇到断点。

5. 继续运行到指定行数位置 until

若我们希望在继续运行直到特定行数停住,可以使用until命令(简写为u):

6. 跳过执行 skip

skip可以在step时跳过一些不想关注的函数或者某个文件的代码:

$ gdb gdbStep
(gdb) b 27
Breakpoint 1 at 0x4005e4: file gdbStep.c, line 27.
(gdb) skip function add    # step时跳过add函数
Function add will be skipped when stepping.
(gdb) info skip   # 查看step情况
Num     Type           Enb What
1       function       y   add
(gdb) run
Starting program: /home/hyb/workspaces/gdb/gdbStep 
it will calc a + b

Breakpoint 1, main () at gdbStep.c:27
27        int c = add(a,b);
(gdb) s
28        printf("%d + %d = %d\n",a,b,c);
(gdb)skip file gdbStep.c # 跳过文件内的所有函数

其他相关命令:

  • skip delete [num] 删除skip
  • skip enable [num] 使能skip
  • skip disable [num] 去使能skip

源码查看

调试过程中一般需要对照整体或者部分源码查看,在GDB调试下快速查看源码或者对源码进行编辑。

1. 源码打印

  • 直接打印源码:list命令(简写l)
  • 列出指定行附近的源码:list命令 + 行号
(gdb) l 9
  • 列出指导函数附件的源码:list命令 + 函数名
  • 设置源码一次列出的行数,一般打印源码默认显示10行。通过listsize属性来设置。
(gdb) set listsize 20
(gdb) show listsize
  • 列出指定行之间区域的源码:list + 起始行号 + 结束行号
(gdb) l 3,15 # 列出3到15行之间的源码
  • 列出指导文件的源码
(gdb) l test.c:1
(gdb) l test.c:printNum1
(gdb) l test.c:1,test.c:3

2. 指定源码路径

查看源码之前,需要先确保程序能够关联到源码文件。但是当出现源码文件移动等情况时,无法直接通过lsit命令查看到源码。

  • 场景1:源码文件移动

源码文件 main.c 移动到temp目录下,此时执行list命令

(gdb) l
1    main.c: No such file or directory.
(gdb) 

通过dir命令重新指定源码路径:

(gdb) dir ./temp
Source directories searched: /home/hyb/workspaces/gdb/sourceCode/./temp:$cdir:$cwd
  • 场景2:更换源码目录

全部源码文件移动到了另一个目录,可以使用上述场景1中的方式添加源码搜索路径,也可以使用set substitute-path from to将原来的路径替换为新的路径

通过readelf命令可以查看原来源码路径:

$ readelf main -p .debug_str
  [     0]  long unsigned int
  [    12]  short int
  [    1c]  /home/hyb/workspaces/gdb/sourceCode
  [    40]  main.c
(显示部分内容)

替换路径:

(gdb) set substitute-path /home/hyb/workspaces/gdb/sourceCode /home/hyb/workspaces/gdb/sourceCode/temp
(gdb) show substitute-path
List of all source path substitution rules:
  `/home/hyb/workspaces/gdb/sourceCode' -> `/home/hyb/workspaces/gdb/sourceCode/temp'.
(gdb)

可以通过unset substitute-path [path]取消替换。

3. 编辑源码

启动调试后,若有编辑源码的需求,可以直接在gdb模式下进行编辑源码。gdb默认使用的编辑器为/bin/ex,可以设置替换编辑器:

$ EDITOR=/usr/bin/vim
$ export EDITOR

gdb调试模型下进行编辑源码,使用edit命令:

(gdb)edit 3  #编辑第三行
(gdb)edit printNum #编辑printNum函数
(gdb)edit test.c:5 #编辑test.c第五行

在vim编辑器下编辑保存完后,可以直接重新编译程序:

(gdb)shell gcc -g -o main main.c test.c

在gdb模式下执行shell命令,需要在命令前加上shell。

其他参考资料

命令汇总

https://www.toutiao.com/i6914639382385295886/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1637332354&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&use_new_style=1&req_id=202111192232330101502202011138A213&share_token=5985B6A6-7323-40D4-897E-3CF9544B6CAD&group_id=6914639382385295886&wid=1637663728310

命令再次列出:

  • file:装入调试程序源文件
  • kill:终止调试程序
  • list:打印源代码
  • break:设置断点
  • run:执行程序
  • quit:推出gdb
  • step:单步进入
  • next:单步执行
  • continue:继续运行,直到下一个断点
  • print:打印变量等
  • watch:监视变量值的变化
  • display:每次断点都会打印一次变量
  • start:开始执行程序,并在main函数的第一条语句前停下来
  • info:gdb相关信息
  • set var name = value:设置变量的值
  • backtrace:查看函数调用信息(堆栈)
  • frame:查看栈帧
  • return:强制函数返回
  • where:列出当前程序运行的位置
  • whatis:查看变量、函数的类型
  • examine:查看内存内容

调试脚本补丁

https://www.toutiao.com/i6836648226666316295/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1637326374&app=news_article&utm_source=weixin&utm_medium=toutiao_ios&use_new_style=1&req_id=20211119205254010151180208251D4D47&share_token=16A0B9FE-C93F-427F-B735-24C97F5A0096&group_id=6836648226666316295

  • 反汇编
(gdb) disassemble 函数名
  • GDB断点预置命令

GDB提供了一种功能,对于指定的断点,GDB允许用户预设一组操作(通常是调试命令),当断点被触发时,GDB会自动执行这组预设的操作。

先设置一个或者多个断点;再利用commands命令进行预设操作(id为断点id):

commands [id...]
  command-list
end
  • Debug过程中通过预置命令来快速修复已知的bug,避免多次编译的繁琐。
b *do_stuff
commands
  printf "\n ESI = %d\n",$esi
  set $esi=10
  printf "\n ESI = %d\n",$esi
  continue
end
# do_stuff函数入口处设置断点,通过修改esi寄存器来修改函数入参传递。
  • 将上述可以写为一个热补丁文件,test.fix.1.加入silent命令,屏蔽断点被触发时的打印信息,避免视觉干扰。
# test.fix.1
b *do_stuff
commands
  slient
  set $esi=10
  continue
end
  • GDB重新调试运行,-x参数加载脚本文件
gdb test -x test.fix.1
posted @ 2021-11-27 19:11  tlamm  阅读(840)  评论(0)    收藏  举报