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

跟踪查看某个变量,每次停下来都显示它的值

print

p

p 或 p n

打印内部变量值

watch

watch

监视变量值的变化

backtrace

bt

bt

产看函数调用信息(堆栈)

frame

f

f

查看栈帧

quit

q

q

退出GDB环境

kill

k

k

终止正在调试的程序

设置变量

set var=x

设置变量的值

return

return x

结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。

finish

fi

fi

结束当前正在执行的函数,并在跳出函数后暂停程序的执行

jump

j

j x

结束当前调用函数井返回指定值,到上一层函数调用处停止程序执行

表1-2

命令

功能

break n

设置第几行作为断点(搭配list使用)

info breakpoints

列出所加的断点

delete breakpoints 1

删除第n个断点(n为info breakpoints获得的断点号)

disable/enable n

表示使得编号为n的断点暂时失效或有效

表1-3

命令

功能

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
strace ./hello

cat@lubancat:~/test$ strace -f ./hello
execve("./hello", ["./hello"], 0x7fe43d6ac8 /* 48 vars */) = 0
brk(NULL)                               = 0x558e1ee000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = 0
openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
close(3)                                = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=77810, ...}) = 0
mmap(NULL, 77810, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb76a7000
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`\17\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1450832, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb76e7000
mmap(NULL, 1519552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb7534000
mprotect(0x7fb768f000, 61440, PROT_NONE) = 0
mmap(0x7fb769e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15a000) = 0x7fb769e000
mmap(0x7fb76a4000, 12224, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb76a4000
close(3)                                = 0
mprotect(0x7fb769e000, 16384, PROT_READ) = 0
mprotect(0x555d37a000, 4096, PROT_READ) = 0
mprotect(0x7fb76eb000, 4096, PROT_READ) = 0
munmap(0x7fb76a7000, 77810)             = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x3), ...}) = 0
brk(NULL)                               = 0x558e1ee000
brk(0x558e20f000)                       = 0x558e20f000
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(0)                           = ?
+++ exited with 0 +++
  • 第3-29行配置程序的运行环境,以及调用相应的库, /lib/aarch64-linux-gnu/libc.so.6 就是我们前面章节所说的glibc的库文件, 这些配置环境的操作,保护了我们的系统免受程序的干扰, 比如,有些软件想改写我们的系统文件或者破坏我们的系统, 在这些保护环境下,系统不会允许程序运行超过系统规定的运行空间, 即便是程序无意改写系统的文件(超出了运行空间),系统会阻止它。 如果程序进入死循环或崩溃了, 系统可以依赖这些保护措施免受干扰并可以直接终止程序的运行。

  • 第30-52就是我们的程序主体部分了,代码的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一样。