文章目录
- 基本
- 启动调试与附加进程
- 断点
- 程序运行控制
- tui模式
- 查看堆栈与变量
- 监视变量
- 多线程调试
- 扩展
- 自定义跳转命令
- 解析自定义类型
- 禁用动态库自动加载
- 设置源码路径
- 断点时执行命令
- gdbserver远程调试
- gdb脚本
- QtCreator调试
- Linux下处理编译、运行时的一些问题
- undefined symbol问题-`nm`命令
- 系统环境变量问题
基本
启动调试与附加进程
- 转到程序运行目录下,执行
gdb ./程序名
即可
只是附加了一个调试文件,并没有启动这个程序,需要执行run
命令——简写r,来运行这个程序;启动前可以set args arg1 arg2 ...
来设置启动参数,运行中可以通过show args
查看启动参数 - 如果要附加运行,可以执行
gdb attach 进程ID
即可;脱离程序可以执行detach
- 在linux查看进程ID——一种方式直接在任务管理器里查看(如果是图形界面);或者在命令行里打印进程查找
ps -ef | grep 程序名
- 调试core文件——linux下的core文件就像windows下的dump文件,用于分析崩溃,命令很简单
gdb ./程序名 core文件名
,之后就可以用普通gdb调试用的命令查看切换堆栈、查看变量等 - core文件的生成
- 首先去除生成core文件大小限制。只需要解除linux下core文件大小限制
ulimit -c unlimited
ulimit是限制linux下每个用户使用系统资源的命令,比如core文件大小限制、最大内存、最大堆栈大小、单次CPU最大占用时间等等,可以通过ulimit -a
来查看当前所有的限制;在命令行中设置ulimit
只是临时的,想要永久生效,需要修改/etc/security/limits.conf
文件,如下:
* soft core unlimited root hard core unlimited
- 然后运行程序直到崩溃,就会在
/tmp
文件夹下生成core开头的文件
设置core文件生成的路径,可以修改/proc/sys/kernel/core_pattern
文件内容为/tmp/core-%p-%e-%t
;或者执行sysctl -w kernel.core_pattern=/tmp/core-%p-%e-%t
%p -进程ID
%e -进程名称
%t -生成时间
%u -用户ID
%g -组ID
%s -导致coredump生成的信号名
%h -主机名 - 首先去除生成core文件大小限制。只需要解除linux下core文件大小限制
断点
断点的命令是break,简写b,基本用法:
info break
,查看当前所有的断点信息——其实info
(简写i)本身还可以搭配其他指令,也是查看信息的意思,比如info thread
查看当前所有线程信息break line
,在行号处打断点break function
,在当前文件指定函数开头处断点。需要注意的是,gdb会为所有该名称的函数打上断点,可以b class::method
给指定类的方法打上断点,或者b function(int)
给指定重载形式的函数打断点;更进一步,有rb 正则表达式
来给正则表达式能够匹配上的函数打上断点break file:line
,在指定文件指定行断点break file:function
,在指定文件指定函数断点break +/-offset
,在当前行前/后几行位置处断点break ... if cond
,设置条件断点tbreak ...
,设置一个临时的断点(临时断点断过一次后就会失效)
通过i b
查看所有断点,可以enable 断点编号
,启用断点disable 断点编号
,禁用断点delete 断点编号
,删除断点,简写d
程序运行控制
- 单步(step over)——next单步执行,简写n;执行一次n之后可以直接enter继续执行n命令,因为gdb里没有输入命令就回车会执行上一次一样的命令
- 步进(step into)——step单步进入,简写s
- 运行到——until,执行到指定行
- 跳出当前函数——finish,继续执行完当前函数退出到堆栈上一级暂停
- 返回——return,直接跳出当前函数,不执行剩余内容
- 继续运行与中断——continue(简写c)继续运行程序;ctrl+c中断程序运行
tui模式
因为设置断点与单步执行,只能看到当前运行行附近几行,可以通过list
命令显示更多行(默认显示10行),用法如下:
list lineNo
,显示行号附近行list file:lineNo
,显示文件的第几行附近行list function
,显示当前文件中函数附近代码list file:function
,显示指定文件中函数附近代码list from,to
,显示从from到to的代码
即使通过list多显示几行,仍然非常不友好,可以启用gdb的tui(text user interface)模式,只需要在启动gdb的时候加上-tui
参数;或者在运行的时候ctrl+x+a
进入,界面如下:
查看堆栈与变量
- 查看堆栈命令
backtrace
(简写bt),显示的堆栈前会显示每一帧的帧号 frame 帧号
切换帧,简写f 帧号
;up/down n
基于当前帧上下切换帧info
信息——info args
查看当前函数调用参数,info locals
查看函数局部变量,info registers
查看寄存器,info mem
查看内存print
命令——打印变量值,p top
打印上一个变量的值whatis val
,打印变量val的类型。有意思的是,在模板函数/模板类的成员函数断点断住后,whatis T
也能获取到当前的泛型类型ptype val
,打印变量类型,会打印出类型的定义,包括成员变量和成员函数等,更详细print val
,打印显示变量的值;需要注意的是,print param=val
可以在调试过程中修改变量的值;print *this
输出当前对象各成员变量值
监视变量
display val
,可以在中断过程中始终显示某一个变量;info display
查看当前有哪些实时display的变量;undisplay
,取消指定序号变量的实时显示watch val
,监视某一个变量,当发生变化时中断,即数据断点
多线程调试
info thread
,查看都有哪些线程thread index
,切换到指定线程
扩展
自定义跳转命令
如果想要实现VS中一样的,拖动当前运行位置到其他行并执行,gdb里也有jump命令(简写j),使用格式如下:
jump 行号
——跳转到指定行jump +10
——跳转到当前代码下面10行处jump *0x12345678
——跳转到0x12345678地址的代码处,地址前要加*
跳转有两个注意事项——
- jump到指定位置,中间这些代码是直接跳过的(与VS是一样的)
- jump到指定位置后,如果该位置没有断点,是会继续往下运行的
鉴于以上第2点,如果我们需要跳转到指定行后暂停,然后我们单步调试的话,需要先打一个临时断点
(gdb)tbreak lineNum
(gdb)jump lineNum
以上跳转命令能否一次到位?gdb支持脚本,可以定义如下脚本,这样在gdb中可以使用自定义命令,move了
define move
if $argc != 1
help move
else
tbreak $arg0
jump $arg0
end
end
document move
go to specific line
usage:move line_number
end
以上:
- define定义了自定义命令,argc代表输入参数个数,arg0~arg9代表输入参数(最多十个),其中可以使用gdb的命令以及自定义命令,语法与一般语言差不多,
if...else...end
与while...end
等,continue对应loop_continue
,break对应loop_break
document
代表给命令写文档,当执行help 命令
的时候会输入这里面的内容- 将以上脚本写入
/etc/gdb/gdbinit
文件即可在gdb启动的时候加载
解析自定义类型
在gdb中打印std::map,std::vector以及QString等自定义类型非常不友好,显示如下:
是否有方法优化下显示?
禁用动态库自动加载
gdb启动调试的时候,因为要加载的动态库符号表太多了,所以附加上去会卡很久,禁用启动时自动加载动态库符号表,等到需要时再手动加载可加速启动速度;另外,按需加载动态库符号表的话,使用步进命令也会快得多,因为原来步进需要查找进入的函数在哪个动态库里,现在禁用自动加载了之后,本来加载的符号表就不多,查找自然很快
- 禁用方式
在/etc/gdb/gdbinit
文件里,加上如下设置即可:
set auto-solib-add off
然后附加调试之后,可以info sharedlibrary
查看加载的动态库,Sym列为No的表示未加载(Read列标*
的表示没有调试信息)
- 如何手动加载
-
执行
bt
查看当前堆栈,看下是当前是哪个动态库符号表未加载导致看不到 -
然后使用
add-symbol-file
命令加载该动态库文件到From内存地址即可
-
接下来再
bt
就可看到这个动态库的符号了
- 加载动态库符号还可以直接执行
sharedlibrary regex
(或者shared regex
),按照正则表达式regex
匹配需要加载的动态库
设置源码路径
调试时之所以能显示源代码,是因为查找到了源代码位置;如果调试时找不到源码,那可能是因为没有设置源码路径
show directory
,显示当前源码路径,默认会带cdir
与cwd
,cdir
表示编译路径,cwd
表示当前工作路径directory path
,添加path到源码查找路径路径,如果直接directory
为空,则会清除当前所有的源码路径,但会加上默认的cdir,cwd
断点时执行命令
- 先下断点——
b xxx
- 然后可以通过
i b
查看刚刚下的断点的编号 - 然后可以
commands 编号
来给指定编号的断点设置执行的命令,比如
以上给断点2设置了命令,当命中断点2时,会打印一个“hide”字符串到命令行,然后continue(也就是继续程序的执行),这样像界面刷新一类频繁调到的函数我们可以不中断直接continue,又可以知道是否调过这个函数,甚至打印调用时候它的局部变量等信息
4. 如上处理后,如果输出内容太多的话,会触发gdb的分页显示,需要用户enter继续显示或者q退出显示,这样实际断点还是会中断,可以在/etc/gdb/gdbinit
中设置关闭分页显示确认(也就是直接默认就显示,不需要确认)——set pagination off
gdbserver远程调试
- 远端运行gdbserver监听端口
- 没有gdbserver则
sudo apt install gdbserver
即可 gdbserver IP:端口 程序名
-监听指定IP主机,监听端口,IP可省略- 或者
gdbserver IP:端口 --attach PID
-附加到指定进程
- 没有gdbserver则
- 本地端连接调试
运行编译好的gdb,执行target remote IP:端口
- 像本地一样gdb调试
- 交叉编译gdb
如果要调试的目标主机架构与本机不一致,本地需要交叉编译,生成目标架构的gdb程序- 下载gdb源码
- 编译对应架构平台的gdb程序——解压源码,进入源码根目录,
mkdir build && cd build && ../configure --target=aarch64-linux-gnu && make -j4
,其中target
参数指定调试目标平台(aarch-linux-gnu为ARM,mips为MIPS);--host
指定编译出来的gdb将要运行于哪个平台,不需要指定,因为就是运行于当前平台(如果编译gdbserver,因为要运行于待调试的目标平台,要指定这个host参数) - 交叉编译这一点暂未验证
gdb脚本
gdb支持自己的脚本(调用gdb命令或者自定义的命令),也支持一些脚本语言(比如python),还可以通过shell
命令调本地命令行,可以通过脚本的方式把一些重复繁琐的调试任务自动化
有待摸索…
QtCreator调试
Qt的调试基本和VS是一样的F5运行,F10步进,F11是step into,shift F11跳出等,不做赘述
- 禁用启动调试时自动加载所有符号文件
修改~/.gdbinit文件,禁用自动加载符号,Qt Creator启动会读取这个配置文件
2. 手动加载用到的动态库符号
- 未加载符号的动态库,符号列显示No,release版本也可以加载符号,只不过符号类型列会显示buildid(debug版本的是plain),plain表示动态库中就包含了符号的定义,而buildid表示动态库so文件中的符号是个链接到外部的引用,实际符号文件已经剥离
- 未加载符号的话,断点打不上,断点上会显示一个沙漏(意思是等待未来加上符号后生效),正常是一个纯粹的红点
- 拷贝源码到指定搜索路径
未加载符号时中断程序,堆栈是看不到任何信息的,可以右键“创建完整回溯”,查看对应线程堆栈底下哪个动态库符号未识别,加载这个动态库符号,就能看到更多的堆栈的信息,继续以上步骤加载其他动态库符号就能将堆栈完整显示出来
加载了动态库符号后,堆栈的帧可能仍然是灰色的,说明源码没找到,可以双击帧在编辑区查看对应源码路径,然后把源码拷贝到这个路径下(或者创建软链接映射过去):
需要注意的是,
1)这个路径如果太长可能显示不全,需要鼠标移动到堆栈的帧上悬停一会查看完整路径
2)路径中的每一个层级都必须要有,比如如果有个路径是/home/cbb-xn/work/cbbcode/GLD/../Glodon/src/GLD/Qt/ThemeEngine/GMPRibbonStyle.cpp
,源码拷贝到路径后实际为/home/cbb-xn/work/cbbcode/Glodon/src/GLD/Qt/ThemeEngine/GMPRibbonStyle.cpp
,路径/home/cbb-xn/work/cbbcode
下没有GLD
文件夹(这个是编译过程中生成的,直接clone下来的源码是没有这个文件夹的),但是这里源码路径里有,也必须创建一个,不然即使把源码拷贝到正确的路径也找不到
4. 设置定位器
源码拷贝到指定的搜索路径下,只是从堆栈中点击可以跳转过去,无法直接在qtcreator中搜索对应文件直接打开。其实qtcreator的Locator本身就像内置的一个everything一样,可以搜索打开文件,比如简单的用法可以通过点击Locator的编辑框,查看上方的提示——a 文件名
定位所有项目中的文件,c 类名
定位所有项目中的c++类,m 函数名
定位所有项目内所有函数
定位器本身可以扩展,点击编辑框左侧的放大镜,在上拉框里点击Configure...
,然后增加自己的定位器。我增加了GMP、GLD、GCP、GGDB、Qt等源码的路径,并设置搜索搜索前缀为a
,这样输入a 文件名
后不仅能搜索当前所有项目下所有文件,还能搜索外部GMP,GCP,GLD,GGDB,Qt的源码文件直接打开
Linux下处理编译、运行时的一些问题
undefined symbol问题-nm
命令
==>一般在编译时就会报出来,但是如果动态库是通过QLibrary
在程序中显式加载(不是通过代码依赖隐式加载)的话,可能会在运行时报这个错,可以在QLibrary::load
失败后面加一句QLibrary::errorString
来获取加载时的错误,就可以看到undefined symbol
之类的提示了
==>这个问题原因如提示所言,是符号未定义,
- 如果是同一个模块内,调用出现undefined symbol,检查是否调用的函数未定义
- 如果是跨模块调用出现undefined symbol,可以使用
nm
命令,判断是调用方的问题,还是依赖库的问题——nm
命令用来查看动态库导出了哪些符号,如果导出符号中确实没有指定符号,则是依赖库问题(该文件没有编译,如没有该文件、cmake中指定了删除该文件、cmake中指定了不编译该文件等;或者该文件编译了但是没有正确导出该函数,如该函数cpp中未实现、该函数没有export导出等);如果动态库导出符号中有该符号,则是调用方使用不正确(cmake中没有配置依赖库等)
如A模块调B模块的func函数,执行命令nm -D B.so | grep func
,类似如下(一般T
表示符号在文本区,正常,U
的话是未定义,则不正常):
或者用objdump
与readelf
也是差不多的作用:
注:应该还有一个链接问题,如果调用库调用方法和依赖库的符号导出都没问题,还有可能是调用库压根调的不是这个依赖库(比如本地有多个版本,调用的时候找错了),可以ldd -s xxx.so
,可以列出动态库依赖哪些动态库,以及其实际找的链接的是哪个动态库