菜鸟笔记
提升您的技术认知

gdb使用详细介绍

阅读 : 209

前言

  • 在工作中,无论是学习代码流程还是问题的定位,GDB都显得尤为重要,多掌握一些命令可以提升我们的效率和解决问题的能力;
  • 按照我的理解,对GDB的掌握程度可以分为三种人:
    • 基础命令,大家都知道
    • 相对高阶一点的,少数人了解,掌握之后可以提升调试解决问题的效率
    • 需要结合反汇编、栈回溯、malloc内存分配原理和结构、elf文件结构等知道,配合GDB来解决一些内存相关的偶现问题

本文结合工作经验,主要是对前面两部分内容进行描述,最后部分会在其他专题展开描述。掌握这些命令,在工作、熟悉代码或者调试问题时更加游刃有余。

1、GDB基础介绍

1.1简介

  • gdb是GNU开源组织发布的一个强大的Linux下的程序调试工具
  • gdb除支持c/c++语言外还支持go、D、object-c、fortran等语言
  • GDB主要帮助你完成下面四个方面的功能:
    • 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
    • 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
    • 当程序被停住时,可以检查此时你的程序中所发生的事。
    • 你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。

1.2编译

gdb是开源的,我们可以上网下载开源代码,然后根据自己的交叉编译链这块不是我们的重点,而且网上资料很多,不做展开

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位端口,必须与嵌入式设备运行时指定的端口一直,两者均为必填项,不能省略。
注:

  1. 设备端无需输入build_Debug ,否则加载会非常慢
  2. 由于设备端内存等的原因,一般情况下我们都是使用远程调试

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文件进行分析。
注:

  1. core生成的可执行程序要和core解析时使用的debug版本的可执行程序对应,否则会解析异常
  2. 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个栈帧
print 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、core dump

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文件记录了出错时的信息,但是如果栈帧遭到破坏了,比如往一个错误的地址写入数据,可能造成栈帧上的数据损坏,此时无法查看栈帧的信息,或者栈帧上的信息变得不可靠,只能通过其他方法查找问题原因。