- 在工作中,无论是学习代码流程还是问题的定位,gdb都显得尤为重要,多掌握一些命令可以提升我们的效率和解决问题的能力;
- 按照我的理解,对gdb的掌握程度可以分为三种人:
- 基础命令,大家都知道
- 相对高阶一点的,少数人了解,掌握之后可以提升调试解决问题的效率
- 需要结合反汇编、栈回溯、malloc内存分配原理和结构、elf文件结构等知道,配合gdb来解决一些内存相关的偶现问题
本文结合工作经验,主要是对前面两部分内容进行描述,最后部分会在其他专题展开描述。掌握这些命令,在工作、熟悉代码或者调试问题时更加游刃有余。
1.1简介
- gdb是gnu开源组织发布的一个强大的linux下的程序调试工具
- gdb除支持c/c 语言外还支持go、d、object-c、fortran等语言
- gdb主要帮助你完成下面四个方面的功能:
- 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
- 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
- 当程序被停住时,可以检查此时你的程序中所发生的事。
- 你可以改变你的程序,将一个bug产生的影响修正从而测试其他bug。
1.2编译
gdb是开源的,我们可以上网下载开源代码,然后根据自己的交叉编译链这块不是我们的重点,而且网上资料很多,不做展开
2.1使用方式
2.1.1本地调试
直接在服务器或者当前嵌入式设备环境下运行,如果gdb程序为gdb,应用程序为build,则运行调试使用命令如下:
~#gdb ./build
2.1.2远程调试
远程调试需要两个gdb程序,运行在远程设备(target)上的程序称之为gdbserver,运行在本地主机host上的gdb程序为交叉编译器,即在x86平台上运行arm平台的gdb程序,记作gdb_client
1、嵌入式设备执行:
~#./gdbserver 192.168.10.2:1234 ./build
其中192.168.10.2表示允许从这个ip地址登录道嵌入式设备,一般我们的pc服务器地址,也尅省略,表示允许从任何ip连入,:1234为端口号,build为即将调试的应用程序。
2、pc端执行:
~#arm-linux-gdb ./build_debug
handle sigpipe sigusr2 sig32 nostop noprint
target remote 172.8.4.11:1234 //与服务端建立关联
其中172.8.4.11为当前嵌入式设备的ip地址,:1234位端口,必须与嵌入式设备运行时指定的端口一直,两者均为必填项,不能省略。
注:
- 设备端无需输入build_debug ,否则加载会非常慢
- 由于设备端内存等的原因,一般情况下我们都是使用远程调试
2.1.3attach
在实际的交叉编译开发过程中,在某些时候我们运行程序时没有加载gdb,但是却发现了一个偶现问题,需要gdb来定位,这个时候就需要去attach分析。
1、找到当前可执行程序对应的debug版本
2、ps,查看当前可执行程序的进程号
3、设备端执行
./gdbserver 192.168.10.2:1234 --attach 669 (669是进程pid)
4、pc端:
~#arm-linux-gdb ./build_debug
handle sigpipe sigusr2 sig32 nostop noprint
target remote 172.8.4.11:1234
5、detach可退出gdb(退出之后程序正常运行)
2.1.4core分析
core文件是死机之后为了还原现场进行分析而生成的,后面会详细介绍,这里只是介绍一下core文件的解析:
~#arm-linux-gdb ./build_debug ./core.669
加载成功之后,可以直接bt查看到当前死机的线程,对core文件进行分析。
注:
- core生成的可执行程序要和core解析时使用的debug版本的可执行程序对应,否则会解析异常
- core加载有很多问号,需要指定一下动态库的路径
set solib-search-path ./libso/
set solib-absolute ./libso/
2.2常用命令
1、基础命令
命令 | 简介 | gdb功能 | 使用方法及备注 |
---|---|---|---|
run | r | 运行 | 调试开始 |
break | b | 设置断点 | b断点处 |
info | i | 查看信息 | 查看断点i b,等后面详细列举 |
delete | d | 删除断点 | delete断点编号 |
disable | disable | 禁用断点 | disable断点编号 |
backtrace | bt,where | 查看栈帧 | bt n显示开头n个栈帧, bt -n最后n个栈帧 |
p | 打印变量 | p argc打印变量,后面详细介绍 | |
x | x | 显示内存 | x 0x1234567,后面详细介绍 |
set | set | 改变变量值 | set variable <变量> = <表达式>;比如 set var test=3 |
next | n | 执行下一行 | n;执行到下一行,不管下一行多复杂 |
step | s | 执行下一行 | s;若下一行为函数,则进入函数内部 |
continue | c,cont | 继续 | c为继续的次数,可省略,表示继续一次 |
finish | finish | 执行完成当前函数 | |
until | until | 执行完成代码块 |
2、打印变量值
print支持格式化输出,命令格式:p/格式 变量;支持的格式如下:
格式 | 说明 |
---|---|
x | 显示为16进制 |
d | 显示为10进制 |
u | 显示为无符号10进制 |
o | 显示为8进制 |
t | 显示为2进制数,t表示two |
a | 地址 |
c | 显示为字符 |
f | 浮点小数 |
s | 显示为字符串 |
3、打印内存
格式:x/nfu addr
n:重复后面fu次数
f:/x16进制 /c字符 /s字符串 /a地址 /d十进制 /i汇编 /t二进制
u:b字节 h(2字节) w(4字节默认) g(8字节)
4、自动换行
(gdb)set height 0
去掉less的功能,一次性打印所有
5、打印所有线程堆栈
(gdb)thread apply all bt
6、查看某个地址意义
(gdb)info line *0x00f43126 //会打印出这个函数名
等价于:arm-linux-addr2line 0x00f43126 -e ./build_debug
7、查看结构体定义
(gdb)ptype ptimeval
type = struct{
int32 i32tv;
int32 i32usec;
}
8、打印格式美观
(gdb)set print pretty on
9、打印数组
(gdb)p *psttmpstruct->pst@4
$35 = {
0x1, 0x2, 0x3, 0x4}
10、display
每次断点时打印某个值
11、查看指令
info args:查看当前函数的参数及其值
info line:查看源代码在内存中地址,可以跟行号、函数名
info locals:显示当前函数的局部变量
info symbol:显示全局变量信息
info function:显示所有函数名称
info thread:查看线程信息
info registers:列举寄存器值
12、指定动态库位置
(gdb)set solib-search-patch ./libso/
(gdb)set solib-absolute-prefix ./libso/
13、打印当前进程map信息
定位内存相关死机问题时比较常用,确认当前申请的大块内存的头尾
i proc map
2.3常用进阶
1、gdb写一张图片
在实际工作中由于yuv内存一般都是直接从sdk接口中获取的物理内存,没有映射到虚拟地址,没有办法直接读写操作,所以在写之前通常我们需要先拷贝到malloc内存中,然后在使用gdb将内存中的数据dump到文件中
(gdb)p/x malloc(1024)
$3 = 0x3593490
(gdb)p/x memcpy(0x3593490, address, 1024)
(gdb)dump memory ~/test.yuv 0x3593490 0x3593490 1024
2、断点之后自动执行
如下所示,当断点执行到时,自动打印i的值,然后bt打印堆栈,然后继续往下执行,直到下一次断点,重复;适用于大型工程中,调试时,想知道每次断点处变量的值,可以使用该命令,不需要每次重复的去操作。
3、多线程调试
单步调试n、s都会遇到一个问题,某个接口可能是多线程调用的,n执行一步可能会跑到其他线程中执行,造成调试不便,所以在执行单步调试前我们可以先将线程锁定,只能执行到当前线程:
(gdb)set scheduler-locking on
(gdb)set scheduler-locking off
可以osa_gettimeofday为例分析
4、断点锁定某个线程
有时将某个接口,如gettimeofday增加断点时,该接口被多个线程调用,不是我们想分析的线程,如果想指定到某个线程的调用,需要指定线程号,针对线程打断点:
(gdb)b gettimeofday thread 23
5、watch
6、反汇编:disassemble
disassemble /m
汇编单步调试:nexti、stepi
汇编打断点:b*main 4 #4表示汇编指令的偏移
默认选择当前函数,也可以指定需要反汇编的函数。
7、栈回溯
在gdb的实际使用过程中,可能存在栈被破坏而导致的没有打印出堆栈的情况,我们可以加载脚本来打印堆栈:
具体实现和使用方法见:栈回溯工具及其使用方法
3.1介绍
核心转储文件是当前进程意外终止时进程地址空间的一文件,core dump也可以是主动产生的(如在gdb中)
操作系统在程序发生异常而异常在进城内部又没有被捕获的情况下,会把进程此刻内存寄存器状态、运行堆栈等信息转储保存在一个里
该文件也是二进制文件,可以使用gdb、elfdump、objdump等对其内容进行解析。
core dump记录了案发现场,通过分析core dump文件,我么可以还原系统发生异常时的情况,从而找出异常的原因。
3.2core生成
查看是否可以生成coredump可以在终端输入命令
~#ulimit -a
如图所示,corefile size为0表示用于生成core文件的大小为0,即不生成core,修改core file size可使用命令ulimit -c yoursize,其中yousize为用户指定的大小,若不希望限制大小,可以直接使用命令:
~#ulimit -c unlimited
设置完成core生成的大小之后还需要设置core生成的目录,设置core生成的目录可以通过往/proc/sys/kkernel/core_pattern 写入参数来设置。
先查看默认状态下/proc/sys/kkernel/core_pattern的值:
~#cat /proc/sys/kkernel/core_pattern
表示默认状态下会在当前目录下生成名为core的core文件,我们可以使用:
echo “/home/core-%e-%p-%s-%t” > /proc/sys/kernel/core_pattern
使用该命令修改core文件生成的目录以及命名规则。
%e出core进程的pid
%u出core进程的uid
%s造成core的signal号
%t出core的时间,从1970-01-0100:00:00开始的秒数
%e出core进程对应的可执行文件名
具体含义如下,同时需要注意,自linux内核2.6.19之后,core_pattern还支持管道命令,如果命令的第一个字符为管道符’l’,linux内核在捕获进程奔溃信息时,就会以root权限执行管道后面的程序或脚本。通过此方式,我们可以做一些操作比如core进行压缩等。
3.3core使用
使用方式前面已经介绍,加载core文件之后,调试命令与本地或远程调试gdb的方式一致,可通过bt命令查看栈帧,分析可能出错的地方,必要时需要反汇编,通过查看寄存器的值,来还原当时的场景。在有源代码的情况下,也可以直接对比元到吗分析出错的位置。
尽管core文件记录了出错时的信息,但是如果栈帧遭到破坏了,比如往一个错误的地址写入数据,可能造成栈帧上的数据损坏,此时无法查看栈帧的信息,或者栈帧上的信息变得不可靠,只能通过其他方法查找问题原因。