8. 调试工具¶
我们写程序的时候,难免会遇到程序出现bug的时候,好的调试工具可以让我们更快的找到bug所在的地方, 轻松解决bug,推荐两个好用的调试工具。
gdb
strace
注解
除了以下推荐的两个调试工具,linuix里有各种各样强大的调试工具,读者可自行进行百度搜索, 并根据自己的需求获取自己需要的调试工具
8.1. gdb¶
经典的调试工具,功能很强大,
注意此时编译的时候需要增加 -g 选项, 并用-Og进行优化。多线程下可以attach到进程来调试。
在命令行中输入 gdb 进入gdb命令行模式
gdb具有大量命令调试,这里不能将其一一列举, 这里给大家介绍一些常用的基础命令
命令 |
缩写 |
用法 |
作用 |
---|---|---|---|
start |
start |
开始执行程序,在main函数的第一条语句前面停下来 |
|
file |
file xxx |
打开调试的文件 |
|
run |
r |
ru |
运行程序(第一条语句开始运行) |
break |
b |
见下表1-2 |
设置断点 |
continue |
c |
c |
继续程序的运行,直到遇到下一个断点 |
list |
l |
见下表1-3 |
显示多行源代码 |
step |
s |
s |
执行下一条语句,如果该语句为函数调用,则进入函数执行其中的第一条语句 |
next |
n |
n |
执行下一条语句,如果该语句为函数调用,不会进入函数内部执行(即不会一步步地调试函数内部语句) |
display |
disp |
disp n |
跟踪查看某个变量,每次停下来都显示它的值 |
p |
p 或 p n |
打印内部变量值 |
|
watch |
watch |
监视变量值的变化 |
|
backtrace |
bt |
bt |
产看函数调用信息(堆栈) |
frame |
f |
f |
查看栈帧 |
quit |
q |
q |
退出GDB环境 |
kill |
k |
k |
终止正在调试的程序 |
set var |
set var=x |
设置变量的值 |
|
return |
return x |
结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。 |
|
finish |
fi |
fi |
结束当前正在执行的函数,并在跳出函数后暂停程序的执行 |
jump |
j |
j x |
结束当前调用函数井返回指定值,到上一层函数调用处停止程序执行 |
命令 |
功能 |
---|---|
break n |
设置第几行作为断点(搭配list使用) |
info breakpoints |
列出所加的断点 |
delete breakpoints n |
删除第n个断点(n为info breakpoints获得的断点号) |
disable/enable n |
表示使得编号为n的断点暂时失效或有效 |
命令 |
功能 |
---|---|
list |
默认情况下,一次显示10行,第一次使用时,从代码其实位置显示 |
list n |
显示已第n行未中心的10行代码 |
list functionname |
显示以functionname的函数为中心的10行代码 |
list - |
显示刚才打印过的源代码之前的代码 |
8.2. strace¶
strace是一个用来跟踪系统调用的简易工具。
它最简单的用途就是跟踪一个程序整个生命周期里所有的系统调用,并把调用参数和返回值以文本的方式输出。Strace还可以跟踪发给进程的信号。 支持attach正在运行的进程 strace -p <pid>, 当多线程环境下,需要跟踪某个线程的系统调用, 可以先ps -efL | grep <Process Name> 查找出该进程下的线程, 然后调用strace -p <pid>进行分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | strace ./hello
cat@lubancat:~/test$ strace -f ./hello
execve("./hello", ["./hello"], 0x7fe8174088 /* 25 vars */) = 0
brk(NULL) = 0x55846da000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa2baa000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=58163, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 58163, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa2b65000
close(3) = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\340u\2\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=1637400, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 1805928, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa29ac000
mmap(0x7fa29b0000, 1740392, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x7fa29b0000
munmap(0x7fa29ac000, 16384) = 0
munmap(0x7fa2b59000, 48744) = 0
mprotect(0x7fa2b38000, 61440, PROT_NONE) = 0
mmap(0x7fa2b47000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x187000) = 0x7fa2b47000
mmap(0x7fa2b4d000, 48744, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa2b4d000
close(3) = 0
set_tid_address(0x7fa2baaf30) = 5104
set_robust_list(0x7fa2baaf40, 24) = 0
rseq(0x7fa2bab600, 0x20, 0, 0xd428bc00) = 0
mprotect(0x7fa2b47000, 16384, PROT_READ) = 0
mprotect(0x55574aa000, 4096, PROT_READ) = 0
mprotect(0x7fa2bae000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fa2b65000, 58163) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0xf4, 0), ...}, AT_EMPTY_PATH) = 0
ioctl(1, TCGETS, {B115200 opost isig icanon echo ...}) = 0
getrandom("\x3f\xba\x63\x44\xc2\xc6\x01\x30", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x55846da000
brk(0x55846fb000) = 0x55846fb000
write(1, "hello, world! This is a C progra"..., 35hello, world! This is a C program.
) = 35
write(1, "output i=0\n", 11output i=0
) = 11
write(1, "output i=1\n", 11output i=1
) = 11
write(1, "output i=2\n", 11output i=2
) = 11
write(1, "output i=3\n", 11output i=3
) = 11
write(1, "output i=4\n", 11output i=4
) = 11
write(1, "output i=5\n", 11output i=5
) = 11
write(1, "output i=6\n", 11output i=6
) = 11
write(1, "output i=7\n", 11output i=7
) = 11
write(1, "output i=8\n", 11output i=8
) = 11
write(1, "output i=9\n", 11output i=9
) = 11
exit_group(10) = ?
+++ exited with 10 +++
|
第4-35行配置程序的运行环境,以及调用相应的库,
/lib/aarch64-linux-gnu/libc.so.6
就是我们前面章节所说的glibc的库文件, 这些配置环境的操作,保护了我们的系统免受程序的干扰, 比如,有些软件想改写我们的系统文件或者破坏我们的系统, 在这些保护环境下,系统不会允许程序运行超过系统规定的运行空间, 即便是程序无意改写系统的文件(超出了运行空间),系统会阻止它。 如果程序进入死循环或崩溃了, 系统可以依赖这些保护措施免受干扰并可以直接终止程序的运行。第36-57就是我们的程序主体部分了,代码的30行-51行的输出格式有误, 可能是多线程导致格式有点问题,但是整体经过修改是没有问题的, 正确的格式如下
1 2 3 4 5
write(1, "hello, world! This is a C program.\n", 35) = 35 hello, world! This is a C program. write(1, "output i=0\n", 11) = 11 output i=0
为什么write(1, “hello, world! This is a C program.n”, 35)=35就可以向屏幕输出文字呢, 因为linux系统把输出到命令行抽象成文件描述符,操作文件描述符就可以操作该设备
1 2 3 4 5 | #查看描述符
cat@lubancat:~/test$ ls -l /dev/std*
lrwxrwxrwx 1 root root 15 Apr 21 20:54 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Apr 21 20:54 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Apr 21 20:54 /dev/stdout -> /proc/self/fd/1
|
stdin标准输入–fd=0–从终端设备输入内容
stdout标准输出–fd=1–从终端设备输出内容
stderr标准错误–fd=2–从终端设备输出命令的错误消息
1 2
write(1, "output i=0\n", 11) = 11 output i=0
write:写入东西
1:文件描述符,这里表示标准输出,即向终端输出东西
“output i=0n”:要输出的内容
11:输出内容的字节大小,加上空格和换行符刚好11个
= 11:write函数返回值,表示写入了多少个字节
我们也可以用write代替printf试一试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | cat@lubancat:~/test$ cat hello_sys.c
#include <unistd.h>
int main()
{
write(1, "hello, world! This is a C program.\n", 35);
write(1, "output i=0\n", 11);
return 0;
}
cat@lubancat:~/test$ gcc hello_sys.c -o hello_sys
cat@lubancat:~/test$ ./hello_sys
hello, world! This is a C program.
output i=0
|
结果和printf一样。