开发OpenWrt软件时不得不用的GDB调试工具详解

  • 在开发 C 语言应用程序时,经常会碰到内存使用错误导致的进程崩溃退出,这时我们 就需要一个工具来定位发生崩溃的代码所在位置以及当时的程序变量内容和调用先后顺 序等,GDB 工具就在这种情况下应运而生。本章首先讲述了如何使用 GDB 启动程序调试, 然后讲述了在 GDB 中如何设置断点以及查看程序的运行状态,最后讲述了使用 GDB 对运 行中程序的执行流程进行修改,这样可以以最快的速度定位问题所在。

一、什么是GDB

  • 概念:GDB(GNU Project debugger)是GNU项目开发的针对C/C++语言的代码调试工具,它可以让你看到一个程序执行时里面发生了什么事情,甚至是程序在崩溃时正在执行的语句和状态。
  • GDB主要有4个功能来帮助你捕捉发生BUG时的状态
    • ①启动应用程序,可以按照调试人员自定义的要求随心所欲地运行程序,例如设置 参数和环境变量。
    • ②可让被调试的程序在你所指定的调试断点处停住(断点可以是条件表达式)。
    • ③当程序停止执行时,可以检查此时程序中所有的状态。
    • ④动态改变你的程序。在程序运行中改变变量值和代码执行顺序,这样你就可以尝试修改这个 BUG。
  • 被调试程序可以是基于 C、C++、Objective-C 或 Pascal 等许多其他语言编写的。这些 程序可以和 GDB 在同一台计算机上(本地)或在不同的计算机上(远程)。GDB 可以运行Linux 和Windows 等操作系统上。
  • 常用的GDB命令:如果可执行程序增加可调试功能,需要在编译时增加-g 选项,然后使用命令“gdb” 启动应用程序。
命 令含 义示 例
break在指定的位置或函数处设置断点break main
run开始执行调试程序run
bt查看程序运行栈信息bt full
continue在程序中断之后继续执行程序c
next单步执行,如果是函数则执行完这个函数next
step单步执行,如果是函数则进入函数内部step
set args设置启动参数 set argsset args abc
print输出表达式或变量值print argc
quit退出程序调试quit
list输出现在执行程序停止位置附近的代码list
help输出 GDB 命令的帮助信息help next

-g参数与-O参数的注意事项

  • 为了能高效地调试程序,你需要在编译时产生调试信息。调试信息存储在对象文件中, 它描述了每一个变量或函数的数据类型,以及源代码行和执行代码的地址之间的关系。
  • 在编译时,指定“-g”选项即可产生调试信息。在把程序交给客户时通常会使用“-O” 选项进行编译优化,一些编译器不能同时处理“-g”和“-O”选项,GNU 的 C/C++编译 器支持同时带有两个参数。一般在研发过程中,我们推荐始终使用“-g”参数来编译你的 程序,因为你不知道程序何时会出现问题。
  • 编译命令参考如下:
g++ -g hello.c -o hello 

二、使用GDB启动程序

  • 方式一:最常见的启动 GDB 的方式是带有一个可执行程序名称的参数,例如:
gdb hello
  • 方式二:也可以带两个参数来启动 GDB,分别为可执行程序和一个进程崩溃后生成的文件。 例如:
gdb hello core
  • 方式三:调试正在运行的程序时,则带上进程号,程序进程号使用 ps 命令来查看。例如:
gdb hello 1234
  • 方式四:启动后,再使用 attach 命令来关联上正在运行的待调试进程。使用 detach 命令来和关联的进程分离。

程序参数设置

  • set args 用于指定程序启动时的参数。如果没有跟着参数将设置参数为空。
  • show args 用于显示程序启动时的参数。

三、环境变量的设置

  • show paths:显示程序的查找路径列表(系统的 PATH 环境变量)。
  • show environment HOME:显示系统的环境变量,例如这里是显示 HOME 环境 变量。
  • set environment varname [=value]:设置环境变量,这个环境变量仅仅在GDB启动的程序中有效,不会影响到系统的环境变量。例如进行如下设置:
(gdb)set env CONFIG_DIR = /etc/config
  • unset environment varname来取消环境变量设置,这对已开始执行的程序没有影响。

四、日志处理

  • 如何将当前进行调试过程中的GDB输出保存下来?可以通过 set logging 命令进行设 置,这在调试时非常有用,可以记录调试的过程,以供以后来分析。
  • GDB日志文件命令如下表所示:
命 令含 义
set logging on经屏幕输出同时输出到 log 文件中。默认输出为当前目录下的 gdb.txt 文件
set logging off关闭 log
set logging file file默认输出为 gdb.txt,这样将当前输出的默认 log 文件改名
set logging overwrite默认情况下 GDB 日志输出是附加到 log 文件中的。设置为 overwrite 时,每次 均重写一个全新的文件
show logging输出当前日志的设置

五、帮助文档

help命令

  • 单独使用help:不带参数的help命令列出命令的分类。GDB将所有命令分为12类。
  • 带有参数的help:使用命令的分类作为help参数,你可以看到这个分类中所有命令的列表。

apropos命令

  • 搜索命令帮助。
  • 常见的命令:
    • breakpoints:断点命令,将程序在特定条件下停止执行。
    • running:运行程序,包含将程序关联到进程、启动进程调试、单步执行、切换执行线程等命令。
    • stack:程序运行栈相关命令,如查看运行栈、在栈中各个栈帧之间切换等。
    • status:状态查询命令,包含info和show命令。

六、断点管理之“指令断点”

  • 断点介绍:在执行程序调试时,我们经常想让程序在某处停止下来,然后查看程序当时的状态, 这就需要设置断点。断点是广义上的程序执行停止点,是指能导致程序停止的任何事情,断点可以划分为指令断点、观察点和捕获点3种情况。
  • 指令断点介绍:指令断点一般简称为断点,设置断点命令为break,可以缩写为 b,用来在调试的程序中设置代码执行停止断点,可以设置为文件代码行或者是函数调用处。

break断点

  • 格式如下:有3个可选的参数。
break [LOCATION] [thread THREADNUM] [if CONDITION]
  • LOCATION:可以是代码行号、函数名或者一个带有星号的地址。如果指定代码行,在所指定的代码行执行前停止。如果指定函数,在函数执行入口处 停止。如果指定了地址,则在指定地址处停止。如果没有参数,使用当前选择的栈帧的下 一行地址,这在返回到当前的栈帧时非常有用。
  • THREADNUM:是线程号,可以用“info threads”命令来查看线程号。
  • CONDITION:是 一个布尔表达式。

tbreak断点

  • tbreak 用于设置一个临时断点,和“break”命令类似,唯一不同的是它所设置的断点 为临时断点,当命中这个断点后将删除断点。

显示断点信息(info)

  • 显示断点信息命令为 info break [n…],运行这一命令将输出所有的指令断点、观察点 和捕获点。有一个可选的参数,这意味着可以仅输出指定的断点、观察点或捕获点。
  • 格式:对于每一个断点,输出内容如下。
(gdb) info break
Num  Type       Disp   Enb   Address      What
1    breakpoint  keep    y   0x080489c6   in main
                                 at hello.c:50
  • 断点编号(NuM):GDB 将指令断点、观察点、捕获点三者统一顺序编号,编号从 1 开始。
  • 类型(Type):是指令断点还是观察点,还是捕获点。
  • 部署(Disposition):当执行到断点以后,是删除断点还是不再运行等。
  • 使能(Enb)状态:断点的使能状态,“y”表示断点启用,“n”表示断点不生效。
  • 地址(Address):断点的内存地址。如果断点的地址是未知的,显示“”。
  • 位置(What):断点在程序源代码中的位置,例如文件和行号。

删除断点(clear、delete)

  • 如果你觉得已定义好的断点不会再使用,你可以使用 clear、delete 这两个命令来进行删除。
  • clear:clear 带有一个可选参数,参数可以为代码行号、函数名和带有星号的地址。如果指定 了行号,这一行的所有断点将被清除;如果指定了函数,则函数起始位置的所有断点将被 删除;如果指定了地址,则该地址位置的断点均被删除。如果没有参数,则在所选择的栈 帧当前位置删除所有断点。
  • delete:delete 用于删除断点,如果不指定参数,将删除所有的断点。 参数为断点编号或者为断点范围。

断点失效与生效(disable、enable)

  • 使用disable,这样断点将不生效但断点位置等信息得到了保留, 你可要在稍后再次使用enable启用它。
  • disable:命令格式如下。
disable [breakpoints] [range...]

//breakpoints为断点编号。如果什么都不指定,表示使所有的断点不生效
  • enable:命令格式如下。
enable [breakpoints] [range...]

演示案例

  • 该命令将会删除编号为 1 的断点,如果不带编号参数,将删除所有的断点。
(gdb) delete breakpoint 1
  •  该命令在文件 hello.c 的 60 行代码处设置行断点。如果是指定当前文件的代码行,可 以不指定文件名。
(gdb) break hello.c:60
  • 该命令设置了一个条件断点,当 argc 为 2 时,执行到 67 行会触发这个断点。
(gdb) break 67 if argc==2
  • 该命令将禁止编号为 1 的断点,这时断点信息的使能域(Enb)将变为 n。
(gdb) disable breakpoint 1
  • 该命令将允许编号为 1 的断点启用,这时断点信息的使能域(Enb)将变为 y。
(gdb) enable breakpoint 1
  • 50 为源文件的行号,该位置的所有断点将被删除。
(gdb) clear 50

七、断点管理之“观察点

  • 观察点是一种特殊的断点,如果表达式修改了值程序执行就停止了。表达式可以是变 量,也可以是几个变量组合,有时会叫作数据断点。需要特别的命令来设置,其他对观察 点的管理命令和指令断点类似。
  • 常用语法如下:
    • watch:为表达式设置一个观察点。一旦表达式值发生变化时,马上停止执行程序。
    • rwatch:设置读观察点。当读到表达式的值时,程序停止执行。
    • awatch:设置访问观察点。当表达式读或写时,将停止执行程序。
    • info watchpoints:列出当前设置的所有观察点。格式与内容和查看指令断点的内 容相同。

八、断点管理之“捕获点

  • 你可以用捕获点调试某些程序事件(event),例如 C++异常、共享库的加载、系统调 用和进程启动等。
  • catch命令:使用catch命令来设置捕获点后,当事件发生时,程序会停止执行。常 见的事件有以下一些内容。
  • tcatch命令:是设置临时捕获点,即这个捕获点被执行到时会自动删除,仅被 执行到一次。
  • catch命令可用于以下的事件:
    • throw:一个 C++抛出的异常。
    • exec:当程序执行 exec 函数创建进程时。
    • syscall:参数为捕获系统调用它们的名字或编号。如果没有给出参数则每一个系 统调用将都被捕获到,例如调用 open 函数打开文件时。
    • load:加载共享库时。
    • fork:当程序调用 fork 创建进程时。

演示案例

  • 设置在系统调用 open 函数时停止执行。
 (gdb) catch syscall open
  • 设置在创建新进程时停止执行。
(gdb) catch fork

九、单步调试

  • 当你的程序被停止执行时,你可以用 continue 命令恢复程序的运行直到程序结束,或 下一个断点到来。也可以使用 step 或 next 命令单步跟踪程序。

continue

  • continue [ignore-count]:从断点停止的地方恢复程序执行。命令可以缩写为 c。ignorecount 则表示忽略这个位置的断点次数。程序继续执行直到遇到下一个断点。

step

  • 继续执行程序直到控制到达不同的源码行,然后停止执行并返回控制到 GDB。 命令可以缩写为 s。如果函数编译带有代码行信息,step 命令将进入函数。否则行为和 next 命令类似。后面可以加一个参数 count,加参数表示执行 count 次 step 指令,然后再停住, 或者其他原因导致停住。

next

  • 同样为单步跟踪,继续执行同一函数的下一行代码,这和 step 命令相似,但如 果有函数调用,它不会进入该函数内部。后面可以加数字 N,不加则表示一条一条地执行, 加表示一次执行 N 条命令的行为,然后程序再停止。

finish

  • 继续运行程序直到当前选择的栈帧返回,并输出返回值,命令缩写为 fin。

until

  • 执行程序直到大于当前已经执行的代码行,在程序循环时经常会用到它,即循 环体如果执行过一次,使用 until 命令将执行循环体完成之后下一行代码处停止。

十、程序运行状态之“查看栈帧信息”

  • 查看程序调用栈信息,当程序停止时,你第一个关注的是程序停止的代码位置和程序 的函数调用路径。当程序执行函数调用时,关于这次调用的信息(包含调用的代码位置、 传递的参数、函数的局部变量等信息)均保存到了一段内存当中,这段内存被称为栈帧或桢(Frame)。所有的栈帧组合称为调用栈。
  • 程序执行后将有很多帧,很多 GDB 命令均假定你选择了其中一个帧。例如你查看一 个变量值,这将在你所选的栈帧中输出局部变量的值,有一些命令用于你选择栈帧。当程 序停止时,GDB 将自动选择当前执行的帧。栈帧编号是一个从 0 开始的整数,是栈中的层 编号。0表示栈顶,main函数所在的层为栈底。

backtrace

  • backtrace 将输出当前的整个函数调用栈的信息,整个栈的每个帧一行显示。backtrace 可以缩写为 bt。
backtrace [full]/[number]
  • full:如果带有“full”限定符,将输出所有局部变量的值。
  • number:参数 number 可以是 一个正整数或负整数,表示只打印栈顶/栈底 n 层的栈信息。
  • 显示的信息格式:
  • 调用栈的每一行显示包含 4 部分,包含帧编号、函数名、函数的参数名称和传入的实参、调用的源代码文件名和行号。从上面可以看出,程序在hello.c文件第31行处停止执行,函数的调用顺序信息为:main() --> read_conf()。

其他语法

  • frame:为选择和输出栈帧。如果没有参数,输出当前选择的栈帧。如果有参数,表示 选择这个指定的栈帧。参数可以是栈帧编号或者地址。打印出的信息有:栈帧的层编号、 当前的函数名、函数参数值、函数所在文件及行号,以及函数当前执行到的代码行语句。
  • up:选择和输出栈帧,不带参数表示选择向上移动一层栈帧。可以带有参数来移动 多层。
  • down:不带参数表示选择向下移动一层栈帧。可以带有参数来移动多层。
  • return:返回到当前栈帧的调用处。
  • info frame:显示栈帧的所有信息。

多线程中的栈帧信息

  • 栈帧的调用关系只能在同一个线程中查看,如果一个程序有多个线程同时执行,我们可以输入 thread 命令和参数线程编号来在线程之间切换。线程是操作系统能够进 行运算调度的最小单元,线程之间共享其父进程中的所有资源,线程也有自己独立的 调用栈空间。经常使用的线程命令有查询所有线程命令“info threads”和切换线程命 令“thread”。

演示案例

  • 下面显示了当前选择栈帧的详细信息,包含栈帧地址、调用函数的地址、被调用 栈帧的地址、源代码的编程语言、参数地址和内容等。

十一、程序运行状态之“查看源程序信息”(list)

  • GDB 可以打印调试程序的源代码,由于你在编译时增加了-g 参数,调试信息保存在 可执行程序中,当你的程序停止执行时,GDB 将输出停止位置。这时你就可以开始调试了。 使用 list 等命令来查看当时编译的源代码等。
  • list 如果没有参数,输出当前 10 行代码或者紧接着上次的代码。“list -”输出当前位置 之前的 10 行代码,注意带有一个中划线作为参数。list 命令参数也可以是一个代码行或函 数名:如果为代码行,则列出指定行的代码;如果为函数名,则列出函数名附近的代码。
  • 演示案例:下面列出了main函数附近的代码。

十二、程序运行状态之“查看运行时数据”(print、x)

print

  • 使用print命令可以输出执行程序时的运行数据,例如表达式的值,但是需要在你的调用栈环境下,例如全局变量、静态全局变量和局部变量等。
  • 表示输出表达式的内容。如果局部变量和全局变量名称相同,则默认为输出局部变量的内容。
print /fmt exp
  • 如果需要输出全局变量,则需要增加全局限定符(为双冒号,::)。

  • 如果变量为数组,则需要@字符配合才能输出数组的内容,@的左侧是数组的地址, 右侧是数字的长度。如果是静态数组的话,可以直接用 print 数组名,就可以显示数组中 所有数据的内容。例如输出 main 函数的第 argc 个参数内容:

  • 某些情况下,程序变量的值不能被输出,因为你的程序打开了编译优化功能。这种情 况下,需要你在编译时关闭编译优化功能。

x

  • 你可以使用 x 来查看内存地址中的值。
  • x 命令的语法如下所示:
x /FMT ADDRESS

//ADDRESS 是一个内存地址
//FMT 是格式字符和多少个同样格式的内容连接在一起

自定义输出格式

  • 一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义print的输出格 式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量中的位的情况。 要做到这样,你可以使用GDB的数据显示格式。
  • 格式如下:
    • x:按十六进制格式显示变量。
    • d:按十进制格式显示变量。
    • u:按十六进制格式显示无符号整型。
    • o:按八进制格式显示变量。
    • t:按二进制格式显示变量。
    • a:按十六进制格式显示变量。
    • c:按字符格式显示变量。
    • f:按浮点数格式显示变量。
  • 程序执行过程中,有一些专用的GDB变量可以用来检查和修改计算机的通用寄存器, GDB提供了目前每一台计算机中实际使用的4个寄存器的标准名字:
    • $pc:程序计数器。
    • $fp:帧指针(当前堆栈帧)。
    • $sp:栈指针。
    • $ps:处理器状态。

十三、动态改变——改变程序的执行

  • 利用 GDB 调试你的程序时,如果你觉得程序运行流程不符合你的期望,或者某个变 量的值不是你所期望的,你可根据自己的思路来临时修改程序变量的值,这样就可以修改 程序的运行过程来验证是否是这个变量导致的 BUG。

常见用法:

  • 修改变量的值,通过 print 或者 set 命令来修改变量值。

  • 从不同的地址处执行,当程序在断点处停止时,你可以使用 continue 命令继续 执行,也可以使用 jump 指定下一条语句的运行点。参数可以是文件的行号,可 以是 file:line 格式,可以是+num 这种偏移量格式。表示着下一条运行语句从哪 里开始。
  • signal 产生信号,一般用于模拟进程收到信号的处理情况,例如 signal 9。
  • 强制函数返回,可以通过调用 return 命令来取消函数的继续执行,去返回到调用 处。可以带有一个参数,这个参数用于函数返回值。
  • 调用函数,通过 call 来调用函数,也可以使用 print 来调用函数。
命 令含 义
print输出并修改程序值
set修改程序值
jump跳转到指定行或地址来继续执行,最好在同一函数内部跳转
signal向程序发信号。例如 signal 9 将发出杀掉进程的信号
return强制函数返回,不会继续执行函数的剩余代码
call调用函数,不输出函数返回值

十四、参考资料

  • GNU 工程调试器(http://www.gnu.org/software/gdb/)。
  • 使用 GDB 来调试(https://sourceware.org/gdb/current/onlinedocs/gdb/)[2014-12-05]。
  • Linux 系统调用列表(http://www.ibm.com/developerworks/cn/linux/kernel/syscall/ part1/appendix.html)[2014-12-06]。
  • syscalls-Linux 系统调用(http://man7.org/linux/man-pages/man2/syscalls.2.html)[2016- 07-24]。

  • 我是小董,V公众点击"笔记白嫖"解锁更多OpenWrt资料内容。

相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页