文章目录
一、gdb概述
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。
有以下4个作用:
1、启动程序,可以设置程序运行的参数或者环境。
2、可让程序stop在断点。
3、当程序被stop时,可以检查此时程序所发生的事。
4、动态的改变你程序的执行条件(如改变变量值)。
二、gdb使用
1、使用准备
如图可以知道,调试一般可以本地调试和远程调试。
准备工作:
1.1、安装gdb
安装适应的gdb,也可安装gdb-multiarch,如arm64切换到arm64
sudo apt install gdb-multiarch
gdb-multiarch
#之后执行
(gdb) set architecture aarch64
The target architecture is assumed to be aarch64
1.2、编译带有调试信息目标文件
编译时,必须要把调试信息加到可执行文件中。例如使用编译器(cc/gcc/g++)的 -g 参数即可。
如果没有-g,将看不见程序的函数名和变量名,代替它们的全是运行时的内存地址。
要用gdb调试程序,必须在编译时加上-g和-ggdb选项,-g选项的作用是在可执行文件中加入源文件信息,但并不是将源文件嵌入可执行文件,所以在调试时必须保证gdb必须能找到源文件。
-g 和 -ggdb 都是令 gcc 生成调试信息,但是它们也是有区别的:
g | 该选项可以利用操作系统的“原生格式(native format)”生成调试信息。GDB 可以直接利用这个信息,其它调试器也可以使用这个调试信息; |
ggdb | 使 GCC为GDB 生成专用的更为丰富的调试信息,但是,此时就不能用其他的调试器来进行调试了 (如ddx); |
-g 和 -ggdb 也是分级别的:
g1 | 级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储之用; |
g2 | 这是默认的级别,此时产生的调试信息包括扩展的符号表、行号、局部或外部变量信息; |
g3 | 包含级别2中的所有调试信息,以及源代码中定义的宏; |
关于调试信息:
1.3、如果远程调试
目标机上需安装gdbserver,这里不做暂时详细研究。
2、使用方法
2.1、启动调试
2.1.1、调试应用程序
1、开始一个程序调试:
gdb -args program a b c //abc为程序参数,无参数 gdb program
gdb -args test a //example
2、开始一个正在运行的程序调试:
gdb attach <pid> //调试正在运行的程序,也可进入gdb后attach <pid>
root@yf:~# ps -a
PID TTY TIME CMD
557 pts/0 00:00:00 test
570 ? 00:00:00 ps
root@XP5:~#
root@XP5:~#
root@XP5:~# gdb attach 557
2.1.2、GDB调试qemu-内核
qemu中包含有gdbserver功能,利用这个功能我们可以直接对kernel进行gdb调试;
-S freeze CPU at startup (use ‘c’ to start execution)
-s shorthand for -gdb tcp::1234
带参数启动,qemu系统在启动时就会暂停,并且使用gdbserver创建gdb调试端口1234
Plain Text
-S //在启动脚本增加qemu启动参数-S
./run-qemu.sh //启动qenu
pc上另外开一个调试Shell窗口
gdb-multiarch //启动调试
target remote:1234 //gdb里面输入连接qemnu端口
注意这个 file vmlinux //
2.1.3、GDB调试 coredump
配置并产生coredump文件(cornerstone默认就使能了coredump),参考:https://www.cnblogs.com/arnoldlu/p/11160510.html
# enable coredump default
/bin/mkdir /var/coredump
/bin/chmod 777 /var/coredump
# coredump 文件名加上 pid
/bin/echo 1 >/proc/sys/kernel/core_uses_pid
# 设定coredump 保存路径和命名方式
/bin/echo '/var/coredump/core-%e-%p-%t' > /proc/sys/kernel/core_pattern
# coredump 大小无上限
ulimit -c unlimited
# enable coredump end
开启gdb coredump调试
gdb program core //core是程序非法执行后core dump后产生的文件
gdb test core-test_thread_0-419-1688546899 //example
2.2、调试程序
2.2.1、入门命令
help命令只是例出gdb的命令种类,如果要看种类中的命令,可以使用 help 命令,如:help breakpoints,查看设置断点的所有命令。
(gdb) help
List of classes of commands:
aliases -- User-defined aliases of other commands.
breakpoints -- Making program stop at certain points.
data -- Examining data.
files -- Specifying and examining files.
internals -- Maintenance commands.
obscure -- Obscure features.
running -- Running the program.
stack -- Examining the stack.
status -- Status inquiries.
support -- Support facilities.
tracepoints -- Tracing of program execution without stopping the program.
user-defined -- User-defined commands.
运行程序:
• 执行run(简写r)命令用于运行代码,在程序结束或者遇到断点处停下;
• 执行start,程序停留在main();
设置断点:
• 设置断点,b + 行号,就设置断点了,运行就会停在断点处;
• 可以用info b看我们设了那些断点;clear + 行号;
• b filename:linenum ,在源文件filename的linenum行处停住;
单步调试:
• continue(简写c):继续执行,到下一个断点处(或运行结束)。
• next(简写n):单步执行,跳过子函数。
• strp(简写s):单步执行,进入子函数。
查看变量:
ptype 变量名 显示变量的类型
print(简写p)+变量名
(gdb) p test_a
$2 = {test_c = 0 '\000', b = 0x0, if_t = true}
(gdb)
其他
watch 使你能监视一个变量的值而不管它何时被改变
使用finish运行到函数到正常退出
set 设置变量的值。例如:set a=66 将把66保存到a变量中
return 强制从当前函数返回
list 查看源代码,No symbol table is loaded. Use the “file” command,编译时需要 -g
2.2.2、进阶命令
https://www.kancloud.cn/wizardforcel/gdb-tips-100/146708
gdb官方文档:https://sourceware.org/gdb/onlinedocs/gdb/
3、使用技巧
3.1、多线程
查看线程:
info threads ;其中Frame:显示的是线程执行到哪个函数;
也可以用“i threads [Id…]”指定打印某些线程的信息
(gdb) info threads
切换线程:thread [num]
通过线程的编号切换到指定的线程。例如:thread 2 //切换到编号为 2 的线程。
锁定线程:如果想在调试一个线程时,让其它线程暂停执行,可以使用 set scheduler-locking on 命令
(gdb) set scheduler-locking on
(gdb) r
补充:调试多进程可以新开窗口 attach 更加方便。
使用info inferiors 可以查看进程
3.2、查看栈信息
当程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入“栈”(Stack)中。可以用GDB命令来查看当前的栈中的信息。
打印当前的函数调用栈的所有信息 Backtrace (bt);bt full显示栈中所有帧的完全信息;
(gdb) bt
查看当前栈层的信息 f(frame);info f 这个命令会打印出更为详细的当前栈层的信息,运行时的内存地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等
(gdb) info f
查看当前函数各个参数的值 info args
(gdb) info args
a = 0x7fe4000b60
b = 0x7feabff7b8
timeout = 1000
(gdb)
打印出当前函数中所有局部变量及其值,info locals
(gdb) info locals
res = <optimized out>
a = 549399295992
b = <optimized out>
__func__ = "test_send"
end = <optimized out>
index = <optimized out>
info catch
打印出当前的函数中的异常处理信息。
3.3、查看寄存器的值
可以使用 i registers ;使用“i all-registers”命令,可以输出所有寄存器的内容;
要打印单个寄存器的值,可以使用 i registers regname 或者 p $regname ;
(gdb) i all-registers
x0 0x0 0
x1 0x0 0
x2 0x0 0
x3 0xb 11
x4 0x7f849e8c99 547685829785
x5 0x5581bfa8b8 367249041592
x6 0x6572207265767265 7309940820876358245
x7 0xb 11
x8 0x62 98
x9 0x6977203130303035 7599578291844952117
x10 0x7272652030206874 8246765056589588596
x11 0xa65646f6320726f 749115342594273903
x12 0x353220203a61626c 3833161555636216428
x13 0x545345542d2d205d 6076276550090432605
x14 0x545345542d2d205d 6076276550090432605
x15 0x0 0
x16 0x7f84cb8eb8 547688779448
x17 0x7f84af76c0 547686938304
x18 0x0 0
x19 0x7f70002308 547339903752
x20 0x7f84ccc000 547688857600
x21 0x7f70002328 547339903784
x22 0x7f70002310 547339903760
--Type <RET> for more, q to quit, c to continue without paging--
x23 0x7f84ccc000 547688857600
x24 0x0 0
x25 0x100050000 4295294976
x26 0x7f84ccb000 547688853504
x27 0x7f70000bf0 547339897840
x28 0x7f84cccfc4 547688861636
x29 0x7f76092810 547441158160
x30 0x7f84cc835c 547688842076
sp 0x7f760927f0 0x7f760927f0
pc 0x7f84cc835c 0x7f84cc835c <test_send+264>
cpsr 0x60001000 [ EL=0 SSBS C Z ]
#v0-v31
#fpsr
#fpcr
#q0-q31
#d0-d31
#s0-s31
#h0-h31
#b0-b31
这里看一下调试bug常见的arm64寄存器:
x0~x7:传递子程序的参数和返回值,使用时不需要保存,多余的参数用堆栈传递,64位的返回结果保存在x0中。
x8:用于保存子程序的返回地址,使用时不需要保存。
x9~x15:用于存储临时变量、计算中间结果和函数参数传递。在函数调用期间,这些寄存器的值可以被破坏。
x16~x17:用于异常处理和指令相关场景。
x18:平台寄存器,它的使用与平台相关。
x19~x28:临时寄存器,可用于存储局部变量。
x29:帧指针寄存器(FP),用于保存栈底地址;
x30:链接寄存器(LR),保存调用跳转指令bl指令的下一条指令的内存地址;
SP:栈顶寄存器,用于保存栈顶地址;
PC: 保存将要执行的指令的地址(操作系统决定其值,不能改写);
cpsr:状态寄存器,按位起作用;
补充说明:
(Caller-saved register(又名易失性寄存器AKA volatile registers, or call-clobbered)用于保存不需要在各个调用之间保留的临时数据。)
(Callee-saved register(又称非易失性寄存器AKA non-volatile registers, or call-preserved)用于保存应在每次调用中保留的长寿命值。当调用者进行过程调用时,可以期望这些寄存器在被调用者返回后将保持相同的值,这使被调用者有责任在返回调用者之前保存它们并恢复它们)
详细情况参考ARM64手册如下章节。
函数调用:每个函数调用,都会有 入栈 和 出栈 操作。
函数入栈:
保存返回地址:将返回地址(函数调用指令的下一条指令地址)压入堆栈,以便函数执行完后能够返回到调用者。
保存寄存器:将需要在函数内部使用的寄存器的值保存到堆栈中,以便在函数执行期间不会丢失这些值。
分配局部变量空间:调整堆栈指针的位置,为局部变量分配空间。
函数出栈:
恢复寄存器:将之前保存在堆栈中的寄存器值恢复回来,确保函数返回后寄存器的值与函数调用前保持一致。
释放局部变量空间:通过调整堆栈指针的位置,释放之前为局部变量分配的空间。
恢复返回地址:从堆栈中弹出之前保存的返回地址,并跳转到该地址,将控制权返回给调用者。
stp x29, x30, [sp, #-64] //将栈底寄存器x29和返回地址寄存器x30的值存储到相对于堆栈指针sp偏移-64字节的内存地址上,并更新堆栈指针的值。
mov x29, sp //将堆栈指针的值存储到栈底寄存器x29中,用于维护堆栈帧。
…
ldp x29, x30,[sp], [#64] //从堆栈中恢复之前保存的栈底寄存器x29和返回地址寄存器x30的值,并更新堆栈指针的值以移除已恢复的数据。
Ret //控制权返回到调用此函数的位置
3.4、汇编
查看对应汇编 disassemble
自动反汇编后面要执行的代码:
(gdb) set disassemble-next-line on
(gdb) n
144 in /home/main.c
=> 0x0000005555551d08 <main+72>: ff ff 07 a9 stp xzr, xzr, [sp, #120]
(gdb)
disas /m fun 命令将函数代码和汇编指令映射起来(当然编译产物是需要带有调试信息的,详情见使用准备章节);使用 disassemble /r 命令可以用16进制形式显示程序的原始机器码 ;
(gdb) disas /m xrte_init
Dump of assembler code for function xrte_init:
42 in /home/yanfan/test.c
0x0000007ff7f73c64 <+0>: stp x29, x30, [sp, #-80]!
0x0000007ff7f73c68 <+4>: adrp x0, 0x7ff7f9e000
0x0000007ff7f73c6c <+8>: mov x29, sp
0x0000007ff7f73c70 <+12>: ldr x0, [x0, #4048]
0x0000007ff7f73c74 <+16>: stp x19, x20, [sp, #16]
0x0000007ff7f73c80 <+28>: stp x21, x22, [sp, #32]
0x0000007ff7f73c84 <+32>: ldr x1, [x0]
0x0000007ff7f73c88 <+36>: str x1, [sp, #72]
0x0000007ff7f73c8c <+40>: mov x1, #0x0 // #0
43 in /home/init.c
0x0000007ff7f73c78 <+20>: adrp x20, 0x7ff7f9f000
0x0000007ff7f73c7c <+24>: add x20, x20, #0x8
0x0000007ff7f73c90 <+44>: mov x2, x20
0x0000007ff7f73c94 <+48>: mov w1, #0x1 // #1
0x0000007ff7f73c98 <+52>: mov w0, #0x2 // #2
--Type <RET> for more, q to quit, c to continue without paging--
查看某一行所对应的地址范围 i line 42
只想查看这一条语句对应的汇编代码,可以使用 disassemble [Start],[End]
(gdb) i line 156
Line 156 of "/home/main.c" starts at address 0x5555551d44 <main+132> and ends at 0x5555551d54 <main+148>.
(gdb) disassemble 0x5555551d44 , 0x5555551d54
Dump of assembler code from 0x5555551d44 to 0x5555551d54:
=> 0x0000005555551d44 <main+132>: ldr x1, [x19, #8]
0x0000005555551d48 <main+136>: adrp x0, 0x5555554000 <xipc_async_send_res_callback+164>
0x0000005555551d4c <main+140>: add x0, x0, #0xab7
0x0000005555551d50 <main+144>: bl 0x5555551c60 <printf@plt>
End of assembler dump.
3.5、查看内存
x/nfu addr
x addr
x Use the x command to examine memory.
n、f、u是可选的参数:
n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
f 表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。
3.6、设置显示选项
地址、函数参数、结构体、联合体、数组、字符等。
例如:
(gdb) p *array@len
$1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40}
如果是静态数组的话,可以直接用print数组名,就可以显示数组中所有数据的内容了。
可使用ptype i 查看变量类型。
补充:打印所有线程堆栈
在gdb中使用 thread apply all bt 查看所用线程堆栈信息
三、gdb调试实战
1、在线调试
2、离线调试coredump
查看函数调用栈,查看寄存器、查看汇编、分析挂哪里、梳理代码,解bug