Linux 开发工具(3)
本章我们将学习Linux中的调试工具——gdb。
一.gdb初识
1.gdb基本概念
gdb是一种调试工具,调试这个概念我们在使用vs2022时已经有所耳闻,而gdb是Linux端一款功能强大的调试工具。
问题1:调试是什么?
1.找出问题。gdb并不负责为我们解决问题,但是要解决问题的前提是得发现问提,而我们可以通过打断点的方式把程序分成多块,依次排查问题。
2.发现某个区域有问题,查看上下文。
我们将围绕这两个话题对gdb进行讲解。
2.快速认识gdb
在进行调试之前,我们需要了解一个前置知识:在Linux中,被gcc/g++编译后的文件是release版本的。这里我们用一个求和函数为例。
wujiahao@VM-12-14-ubuntu:~/gdb$ vim mycode.c
wujiahao@VM-12-14-ubuntu:~/gdb$ gcc mycode.c -o mycode
wujiahao@VM-12-14-ubuntu:~/gdb$ readelf -S mycode |grep -i debug
wujiahao@VM-12-14-ubuntu:~/gdb$
可以看到当前的可执行文件mycode并不包含debug信息,而程序要调试,就必须以debug模式编译,也就是说——必须包含debug信息才行。所以我们接下来用该指令来生成debug版本的可执行程序。
gcc mycode.c -o mycode -std=c99 -g
编译之后再查看是否包含debug信息:已经出现。
wujiahao@VM-12-14-ubuntu:~/gdb$ readelf -S mycode |grep -i debug[28] .debug_aranges PROGBITS 0000000000000000 00003036[29] .debug_info PROGBITS 0000000000000000 00003066[30] .debug_abbrev PROGBITS 0000000000000000 000031a1[31] .debug_line PROGBITS 0000000000000000 00003275[32] .debug_str PROGBITS 0000000000000000 00003301[33] .debug_line_str PROGBITS 0000000000000000 000033f7
接着我们一边进行调试一边讲解gdb。
3.更好的gdb
为了更好的体验,我们这里使用cgdb,其各种指令和用法与gdb没有区别,但是它可以将可执行程序和调试信息分开,便于调试。首先我们用以下指令调试可执行程序。
cgdb mycode
进入之后的界面如下。如果想退出,可以输入
quit
4.gdb使用
gdb基础指令如下,我们根据实际例子讲解重点。
命令 | 作⽤ | 样例 |
---|---|---|
list/l | 显⽰源代码,从上次位置开始,每次列出10⾏ | list/l 10 |
list/l 函数名 | 列出指定函数的源代码 | list/l main |
list/l ⽂件名:⾏号 | 列出指定⽂件的源代码 | list/l mycmd.c:1 |
r/run | 从程序开始连续执⾏ | run |
---|---|---|
n/next | 单步执⾏,不进⼊函数内部, 逐过程 F10 | next |
s/step | 单步执⾏,进⼊函数内部, 逐语句 F11 | step |
break/b [⽂件名:]⾏号 | 在指定⾏号设置断点 | break 10break test.c:10 |
break/b 函数名 | 在函数开头设置断点 | break main |
info break/b | 查看当前所有断点的信息 | info break |
finish | 执⾏到当前函数返回,然后停⽌ | finish |
print/p 表达式 | 打印表达式的值 | print start+end |
p 变量 | 打印指定变量的值 | p x |
set var 变量=值 | 修改变量的值 | set var i=10 |
continue/c | 从当前位置开始连续执⾏程序 | continue |
删除所有断点 | delete breakpoints | |
删除序号为n的断点 | delete breakpoints 1 | |
disable breakpoints | 禁⽤所有断点 | disable breakpoints |
enable breakpoints | 启⽤所有断点 | enable breakpoints |
info/i breakpoints | 查看当前设置的断点列表 | info breakpoints |
display 变量名 | 跟踪显⽰指定变量的值(每次停⽌时) | display x |
undisplay 编号 | 取消对指定编号的变量的跟踪显⽰ | undisplay 1 |
until X⾏号 | 执⾏到指定⾏号 | until 20 |
backtrace/bt | 查看当前执⾏栈的各级函数调⽤及参数 | backtrace |
info/i locals | 查看当前栈帧的局部变量值 | info locals |
quit | 退出GDB调试器 | quit |
我们可以顺手把mycode.c的Makefile写好。
mycode:mycode.c2 gcc -o $@ $^ -std=c99 -g3 .PHONY:clean4 clean:5 rm -f mycode
mycode.c的内容如下。
wujiahao@VM-12-14-ubuntu:~/gdb$ cat mycode.c
#include <stdio.h>int Sum(int s, int e)
{int result = 0;for(int i = s; i <= e; i++){result += i;}return result;
} int main()
{int start = 1;int end = 100;printf("I will begin\n");int n = Sum(start, end);printf("running done, result is: [%d-%d]=%d\n", start, end, n);return 0;
}
1.打断点
b +filename +n:在filename函数的第n行打断点,这里是在20行处打断点
b +filename +funcname:在filename函数的funcname函数处打断点
我们可以使用指令查看当前存在的断点信息。
info b
Make breakpoint pending on future shared library load? (y or [n]) b 20
Please answer y or [n].
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (mycode 20) pending.
Make breakpoint pending on future shared library load? (y or [n]) b main
Please answer y or [n].
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 2 (mycode.c main) pending.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y <PENDING> mycode 20
2 breakpoint keep y <PENDING> mycode.c main
2.删除断点
d +Num
这里需要注意,其实对断点的操作,除了创建断点之外使用行号,其余大部分都是使用“断点编号”。
(gdb) d 1
(gdb) d 2
(gdb) info b
No breakpoints or watchpoints.
3.开始调试
next/n:逐过程,跳过函数内部——我们可以理解为黑盒测试
step/s:逐语句,进入函数内部——我们可以理解为白盒测试
只要断点打好,只要输入r就会再次回到这个断点处。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/wujiahao/gdb/mycode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
I will beginBreakpoint 4, main () at mycode.c:19
19 int n = Sum(start, end);
bt查看调用栈
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
I will beginBreakpoint 4, main () at mycode.c:19
19 int n = Sum(start, end);
(gdb) bt
#0 main () at mycode.c:19
现在的程序都是以main函数为入口调用其他函数的。
finish执行到当前函数的返回并停止。
这里我们可以观察一个现象:对于Sum函数,局部变量result如何把值返回给外部的其他变量?
通过寄存器返回的。寄存器在cpu中,寄存器不可被引用不可被修改,这也就是为什么它是有常性的。
我们可以反汇编拿到这个可执行程序的汇编代码。
objdump -S mycode>mycode.s
可以明显看到有一个存放到寄存器的操作。也就是说,在main中的这个语句做了两件事:
调用Sum函数+将寄存器的返回值存到变量n中。
int n=Sum(start,end)
我们可以观察临时变量的值,使用以下指令。
p name
(gdb) n
I will begin
19 int n = Sum(start, end);
(gdb) p n
$3 = 21845
(gdb) nBreakpoint 1, main () at mycode.c:20
20 printf("running done, result is: [%d-%d]=%d\n", start, end, n);
(gdb) p n
$4 = 5050
可以看到,变量n在接收Sum的返回值之前一直是存储着随机值的状态。当执行到20行,n拿到寄存器中的值,变为我们期望的结果:5050.
4.补充知识
断点可以被使能。我们可以把已存在的断点像开关一样打开或关闭。
disable+Num
例如这里我们对第16行代码的断点关闭,可以看到断点信息中Num2的断点Enb选项变为了n
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551d8 in main at mycode.c:20breakpoint already hit 1 time
2 breakpoint keep y 0x00005555555551a9 in main at mycode.c:16breakpoint already hit 1 time
(gdb) disable 2
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551d8 in main at mycode.c:20breakpoint already hit 1 time
2 breakpoint keep n 0x00005555555551a9 in main at mycode.c:16breakpoint already hit 1 time
而在被调试的代码中可以看的更清楚:开着的断点为红色,关着的断点为黄色。
5.如何调试,如何理解调试
找出问题。打断点会把我们的代码分块,如果在运行过程中没有问题,说明问题不在当前块中。所以可以直接运行到下个断点处(continue)进行进一步排查。
finish—->确认问题是否在函数内
until line ——>将当前代码逻辑跑完,直接跳转到line行,局部快速执行
2.发现某个区域有问题,查看上下文
这里我们假设要根据start和end设置这个flag值的正负,但是不慎设置为0,我们要通过调试发现问题在哪。首先跟这个代码逻辑相关肯定在调用这个函数的地方,可以现在第20行打断点。
(gdb) b 14
Breakpoint 1 at 0x1198: file mycode.c, line 14.
(gdb) b 20
Breakpoint 2 at 0x11b7: file mycode.c, line 20.
然后我们可以使用display进行变量监控。
display name
(gdb) b 22
Breakpoint 1 at 0x11cd: file mycode.c, line 22.
(gdb) r
Starting program: /home/wujiahao/gdb/mycode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
I will beginBreakpoint 1, main () at mycode.c:22
22 int n = Sum(start, end);
(gdb) s
Sum (s=1, e=100) at mycode.c:8
8 int result = 0;(gdb) n
9 for(int i = s; i <= e; i++)
1: i = 1
2: result = 1
不想用这个监控的话可以根据编号
undisplay Num
我们接着说。
在循环里执行代码都没有发现问题,我们直接until+line跳转到当前函数的line处。
(gdb) until 14
Sum (s=1, e=100) at mycode.c:14
14 return result*flag;
2: result = 5050
此时临时变量i已经销毁,但我们发现:result事实上显示的是正确的绝对值。
我们继续运行。
$2 = 21845
(gdb) n
23 printf("running done, result is: [%d-%d]=%d\n", start, end, n);
(gdb) n
running done, result is: [1-100]=0
24 return 0;
奇怪的是,一退出Sum函数,结果就立马变成0了,我们合理怀疑嫌疑人是flag。
(gdb) p flag
$3 = 0
果然,就是因为flag导致我们的输出结果为0。
tips:可以通过info locals查看一个函数内所有的临时变量。
二.调试技巧
1.watch:执行时监视一个表达式或变量的值。
可以看到watch的本质是创建了一个watchpoint。
(gdb) watch result
Hardware watchpoint 2: result(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551cd in main at mycode.c:22breakpoint already hit 1 time
2 hw watchpoint keep y result
tips:如果你有一些变量不想被修改,但是你怀疑是因为它修改引起了问题,你就可以watch它,如果发生变化gdb会通知你。
2.set var:在调试过程中遇到的需要修改的变量。例如我们这里把可能有错的flag修改为1,修改之后结果正确。
(gdb) set var flag=1
(gdb) p flag
$6 = 1
(gdb) n
23 printf("running done, result is: [%d-%d]=%d\n", start, end, n);
(gdb) n
running done, result is: [1-100]=5050
24 return 0;
3.条件断点
如下面的第11行断点,只有i的值为10时才会在此中断。等i增加到大于10时,断点不会再被触发,会直接结束函数执行。
(gdb) b 11 if i==10
Breakpoint 3 at 0x555555555186: file mycode.c, line 11.
(gdb) n
9 for(int i = s; i <= e; i++)
1: i = 10
2: result = 55
(gdb) n
11 result += i;
除此之外,我们也可以给已有的普通断点添加条件。
condition Num ....
tips:cgdb分屏操作:按ESC进入代码屏,i回到gdb屏。