5. 调试工具¶
在Linux C/C++开发过程中,程序出现错误是不可避免的。语法错误可通过编译器的错误提示快速定位修复,但逻辑错误, 如变量值异常、循环条件错误,运行时错误,如段错误、内存泄漏往往隐藏较深,仅依靠代码排查难以高效解决。 此时,就需要借助专业的调试工具,跟踪程序执行流程、查看变量值变化、定位错误位置,大幅提升问题排查效率。
本章将详细介绍Linux下最常用的调试工具,重点讲解GDB、Valgrind、strace工具, GDB专注于程序执行流程和变量调试,Valgrind专注于内存问题排查,strace专注于系统调用跟踪, 实际开发中可根据错误类型灵活选择合适的工具,甚至组合使用。
5.1. GDB工具¶
5.1.1. GDB简介¶
GDB(GNU Debugger)是Linux下最常用、最核心的调试工具,支持C、C++、Fortran等多种编程语言, 可运行在Linux、Windows、macOS等多个平台。GDB的核心功能是跟踪程序执行流程、设置断点、查看变量值、单步执行程序, 能够精准定位程序中的逻辑错误和运行时错误,是Linux开发必备的调试工具。
GDB通常与GCC/G++配合使用,编译程序时需添加-g选项,生成调试信息,否则GDB无法调试该程序。 Makefile构建工具章节的test10项目的多版本构建中,调试版已添加-g选项,可直接用于GDB调试。
5.1.1.1. GDB核心功能¶
断点调试:在程序指定行、指定函数处设置断点,程序执行到断点时暂停,便于查看当前程序状态;
单步执行:逐行执行程序,或逐过程执行程序,跟踪程序执行流程;
变量查看:查看普通变量、数组、指针、结构体等变量的实时值,支持修改变量值,验证逻辑是否正确;
堆栈跟踪:查看程序调用堆栈,定位程序崩溃的具体位置和调用路径;
程序控制:启动程序、暂停程序、继续执行程序、退出程序,灵活控制调试流程;
核心转储调试:当程序崩溃时,生成核心转储core文件,通过GDB分析core文件,定位崩溃原因。
5.1.1.2. GDB安装与启动¶
可通过以下命令验证安装并查看版本:
1 2 | # 查看GDB版本
gdb --version
|
若提示“command not found”,执行以下命令安装:
1 | sudo apt update && sudo apt install gdb -y
|
在命令行中输入 gdb 进入gdb命令行模式,以Makefile构建工具章节的test10项目编译得到的hello_main_debug为例:
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 | #进入配套代码对应目录
cd lubancat_rk_code_storage/base_linux/makefile/test10
#编译
make
#启动GDB,指定调试的可执行程序
gdb ./hello_main_debug
#信息输出如下
GNU gdb (Debian 8.2.1-2+b3) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./hello_main_debug...done.
(gdb)
|
启动成功后,终端会显示GDB版本信息,提示符变为(gdb),此时即可输入GDB命令进行调试。
5.1.2. GDB命令参数¶
GDB命令较多,以下梳理最常用、最核心的命令,按“调试流程”分类,标注命令格式、功能说明,便于记忆和使用。
启动与退出命令
命令 |
格式 |
功能说明 |
|---|---|---|
gdb启动 |
gdb 可执行程序名 |
启动GDB并加载指定的可执行程序,进入调试模式 |
run(简写r) |
r [参数] |
启动程序,若有参数可直接跟在后面,如r 10 20,传递两个参数 |
quit(简写q) |
q |
退出GDB调试模式,返回终端 |
断点相关命令
命令 |
格式 |
功能说明 |
|---|---|---|
break(简写b) |
b 行号 / b 函数名 / b 文件名:行号 |
设置断点 |
info breakpoints(简写info b) |
info b |
查看所有已设置的断点,显示断点编号、位置、状态 |
delete(简写d) |
d 断点编号 / d 所有 |
删除断点 |
disable(简写dis) |
dis 断点编号 |
禁用断点,后续可通过enable重新启用 |
enable(简写en) |
en 断点编号 |
启用被禁用的断点 |
clear |
clear 行号 / clear 函数名 |
删除指定行或指定函数的断点 |
单步执行命令
命令 |
格式 |
功能说明 |
|---|---|---|
next(简写n) |
n |
单步执行,跳过函数内部细节,即“逐行执行,不进入函数” |
step(简写s) |
s |
单步执行,进入函数内部,即“逐行执行,进入函数” |
finish(简写f) |
f |
执行完当前函数,返回到函数调用处,同时显示函数返回值 |
continue(简写c) |
c |
从当前断点处继续执行程序,直到下一个断点或程序结束 |
until(简写u) |
u 行号 |
快速执行到指定行,跳过中间代码 |
变量查看与修改命令
命令 |
格式 |
功能说明 |
|---|---|---|
print(简写p) |
p 变量名 / p *指针 / p 数组名 |
查看变量值 |
display(简写disp) |
disp 变量名 |
设置“自动显示变量”,每次单步执行后,自动显示该变量的最新值 |
undisplay(简写undisp) |
undisp 显示编号 |
取消自动显示变量,通过info display查看显示编号 |
set var |
set var 变量名 = 值 |
修改变量值,用于验证逻辑 |
x |
x /格式 内存地址 |
查看指定内存地址的内容,格式:x(十六进制)、d(十进制)、s(字符串) |
堆栈跟踪命令
命令 |
格式 |
功能说明 |
|---|---|---|
backtrace(简写bt) |
bt |
查看程序调用堆栈,显示当前函数的调用路径,用于定位程序崩溃位置 |
frame(简写f) |
f 堆栈编号 |
切换到指定的堆栈帧,查看该帧的变量和代码 |
up |
up |
向上切换堆栈帧,从当前函数切换到调用它的函数 |
down |
down |
向下切换堆栈帧,从当前函数切换到它调用的函数 |
其他常用命令
命令 |
格式 |
功能说明 |
|---|---|---|
list(简写l) |
l / l 行号 / l 函数名 |
查看程序代码 |
info locals |
info locals |
查看当前函数内的所有局部变量及其值 |
info args |
info args |
查看当前函数的参数及其值 |
core-file |
core-file core文件名 |
加载程序崩溃时生成的core文件,分析崩溃原因 |
5.1.3. GDB实操示例¶
5.1.3.1. 启动GDB并加载程序¶
以下以Makefile构建工具章节的test10项目编译得到的hello_main_debug为例,演示GDB的完整调试流程, 模拟排查“循环输出异常”的问题。
1 2 3 4 5 6 7 8 | #进入配套代码对应目录
cd lubancat_rk_code_storage/base_linux/makefile/test10
#编译
make
#启动GDB,指定调试的可执行程序
gdb ./hello_main_debug
|
5.1.3.2. 设置断点并启动程序¶
假设我们怀疑hello_func函数的循环逻辑有误,在该函数入口和循环内部设置断点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 1. 在hello_func函数入口设置断点1
(gdb) b hello_func
Breakpoint 1 at 0x7ac: file sources/hello_func.c, line 6.
# 2. 在hello_func函数的循环行(第8行)设置断点2
(gdb) b sources/hello_func.c:8
Breakpoint 2 at 0x7c0: file sources/hello_func.c, line 8.
# 3. 启动程序
(gdb) r
Starting program: /home/cat/test10/hello_main_debug
Breakpoint 1, hello_func () at sources/hello_func.c:6
6 printf("hello, world! This is a C program.\n");
(gdb)
|
程序执行到hello_func函数入口在断点1暂停,此时可查看函数相关信息。
5.1.3.3. 单步执行并查看变量¶
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 | # 1. 进入hello_func函数内部(step命令)
(gdb) s
hello, world! This is a C program.
7 for (int i=0; i<10; i++ ) {
(gdb) s
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) s
output i=0
7 for (int i=0; i<10; i++ ) {
# 2. 查看循环变量i的值,此时i刚定义,值为0
(gdb) p i
$1 = 0
# 3. 单步执行到循环体内部
(gdb) s
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
# 4. 查看i的值,确认是否正确
(gdb) p i
$2 = 1
# 5. 执行循环体,查看输出
(gdb) n
output i=1
7 for (int i=0; i<10; i++ ) {
# 6. 查看i的值,确认是否自增
(gdb) p i
$3 = 1
# 7. 继续单步执行,观察i的变化
(gdb) n
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) p i
$4 = 2
(gdb) n
output i=2
7 for (int i=0; i<10; i++ ) {
(gdb) p i
$5 = 2
(gdb)
|
通过单步执行和变量查看,可确认循环变量i的自增逻辑是否正确,若发现i的值异常,如不自增、自增过快,可定位到循环条件错误。
5.1.3.4. 修改变量值验证逻辑¶
假设我们想验证“i=5时的输出是否正常”,可修改i的值,继续执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 修改循环变量i的值为5
(gdb) set var i=5
# 查看修改后的值
(gdb) p i
$6 = 5
# 继续执行循环体
(gdb) n
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) n
output i=6
7 for (int i=0; i<10; i++ ) {
(gdb) p i
$7 = 6
(gdb)
|
通过修改变量值,可快速验证不同场景下的程序逻辑,无需重新编译程序。
5.1.3.5. 堆栈跟踪与程序退出¶
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 | # 查看当前调用堆栈,此时在hello_func函数中
(gdb) bt
#0 hello_func () at sources/hello_func.c:7
#1 0x0000005555555800 in main () at sources/hello_main.c:5
# 切换到main函数的堆栈帧,堆栈编号0是当前函数,1是调用者main
(gdb) f 1
#1 0x0000005555555800 in main () at sources/hello_main.c:5
5 hello_func();
# 查看main函数的局部变量,无局部变量,所以输出为空
(gdb) info locals
No locals.
# 继续执行程序,直到结束
(gdb) c
Continuing.
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) c
Continuing.
output i=7
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) c
Continuing.
output i=8
Breakpoint 2, hello_func () at sources/hello_func.c:8
8 printf("output i=%d\n",i);
(gdb) c
Continuing.
output i=9
[Inferior 1 (process 24760) exited normally]
# 退出GDB调试
(gdb) q
|
5.1.3.6. 核心转储调试¶
若程序崩溃,如段错误,可通过core文件定位崩溃原因,步骤如下:
1 2 3 4 5 6 7 8 9 10 11 | # 1. 开启core文件生成,临时生效,重启终端失效
ulimit -c unlimited
# 2. 运行程序,若崩溃会生成core文件,如core.12345
./hello_main_debug
# 3. 用GDB加载core文件
gdb ./hello_main_debug core.12345
# 4. 查看崩溃位置
(gdb) bt
|
通过bt命令,可快速定位程序崩溃的具体行和调用路径,便于修复错误。
5.2. Valgrind工具¶
5.2.1. Valgrind简介¶
Valgrind是Linux下一款强大的内存调试工具,核心功能是检测程序中的内存问题,包括内存泄漏、内存越界、使用未初始化的内存、双重释放等。 内存问题往往隐藏较深,运行时可能不报错,但会导致程序崩溃、运行结果异常,Valgrind可精准定位这类问题,是C/C++开发中排查内存问题的首选工具。
Valgrind的核心组件是Memcheck(内存检查工具),默认使用该组件,无需额外配置,只需在Valgrind命令中指定要调试的程序即可。
5.2.1.1. Valgrind核心功能¶
内存泄漏检测:检测程序中分配的内存未被释放的情况,给出内存泄漏的位置和大小;
内存越界检测:检测数组、指针访问时的越界行为;
未初始化内存使用检测:检测使用未初始化的变量;
双重释放检测:检测同一内存块被多次释放;
内存分配/释放不匹配检测:检测malloc与free、new与delete不匹配的情况。
5.2.1.2. Valgrind安装与启动¶
Valgrind通常需要手动安装,执行以下命令进行安装:
1 | sudo apt update && sudo apt install valgrind -y
|
验证安装并查看版本:
1 | valgrind --version
|
Valgrind启动方式:
1 2 | # 基本格式:
valgrind [选项] 可执行程序 [程序参数]
|
5.2.2. Valgrind命令参数¶
Valgrind的命令参数较多,以下梳理最常用、最核心的参数,重点关注内存检测相关选项,程序以hello_main_debug为例:
–leak-check=full :开启完整的内存泄漏检测,显示所有内存泄漏的位置、大小、类型
1 | valgrind --leak-check=full ./hello_main_debug
|
–show-leak-kinds=all :显示所有类型的内存泄漏
1 | valgrind --leak-check=full --show-leak-kinds=all ./hello_main_debug
|
–log-file=文件名 :将调试日志输出到指定文件,避免终端输出过多,便于后续分析
1 | valgrind --leak-check=full --log-file=valgrind.log ./hello_main_debug
|
–vgdb=yes :开启GDB联动调试,可通过GDB连接Valgrind,跟踪内存问题的执行流程
1 | valgrind --vgdb=yes --leak-check=full ./hello_main_debug
|
–track-origins=yes :跟踪未初始化内存的来源,便于定位“使用未初始化变量”的问题
1 | valgrind --track-origins=yes ./hello_main_debug
|
–tool=工具名 :指定Valgrind的工具,默认是Memcheck,可指定其他工具如Cachegrind、Callgrind
1 | valgrind --tool=Memcheck ./hello_main_debug
|
5.2.3. Valgrind实操示例¶
以下以Makefile构建工具章节的test10项目编译得到的hello_main_debug为例,演示Valgrind的使用流程,包括检测内存泄漏、内存越界问题。
5.2.3.1. 检测正常程序的内存情况¶
test10项目的hello_main_debug程序无内存分配/释放操作,理论上无内存泄漏,使用Valgrind检测验证:
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 | #进入配套代码对应目录
cd lubancat_rk_code_storage/base_linux/makefile/test10
#编译
make
# 使用Valgrind检测调试版程序
valgrind --leak-check=full --show-leak-kinds=all ./hello_main_debug
#信息输出如下
==24659== Memcheck, a memory error detector
==24659== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==24659== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==24659== Command: ./hello_main_debug
==24659==
hello, world! This is a C program.
output i=0
output i=1
output i=2
output i=3
output i=4
output i=5
output i=6
output i=7
output i=8
output i=9
==24659==
==24659== HEAP SUMMARY:
==24659== in use at exit: 0 bytes in 0 blocks
==24659== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==24659==
==24659== All heap blocks were freed -- no leaks are possible
==24659==
==24659== For counts of detected and suppressed errors, rerun with: -v
==24659== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
|
结果解读:
All heap blocks were freed – no leaks are possible:所有堆内存都已释放,无内存泄漏;
ERROR SUMMARY: 0 errors:无任何内存错误。
5.2.3.2. 模拟内存泄漏并检测¶
为了演示Valgrind的内存泄漏检测功能,修改test10项目的hello_func.c文件,添加内存分配(malloc)但不释放的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <stdio.h>
//引入malloc头文件
#include <stdlib.h>
#include "hello_func.h"
void hello_func(void)
{
// 分配100字节内存,不释放,模拟内存泄漏
char *ptr = (char *)malloc(100);
printf("hello, world! This is a C program.\n");
for (int i=0; i<10; i++ ) {
printf("output i=%d\n",i);
}
// 故意不写free(ptr); 模拟内存泄漏
}
|
重新编译并使用Valgrind检测内存泄漏:
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 | #清除编译输出
make clean
#编译
make
#Valgrind检测内存泄漏
valgrind --leak-check=full --show-leak-kinds=all ./hello_main_debug
#信息输出如下
==5879== Memcheck, a memory error detector
==5879== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5879== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==5879== Command: ./hello_main_debug
==5879==
hello, world! This is a C program.
output i=0
output i=1
output i=2
output i=3
output i=4
output i=5
output i=6
output i=7
output i=8
output i=9
==5879==
==5879== HEAP SUMMARY:
==5879== in use at exit: 100 bytes in 1 blocks
==5879== total heap usage: 2 allocs, 1 frees, 1,124 bytes allocated
==5879==
==5879== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5879== at 0x4847DD4: malloc (in /usr/lib/aarch64-linux-gnu/valgrind/vgpreload_memcheck-arm64-linux.so)
==5879== by 0x108803: hello_func (hello_func.c:9)
==5879== by 0x10885B: main (hello_main.c:5)
==5879==
==5879== LEAK SUMMARY:
==5879== definitely lost: 100 bytes in 1 blocks
==5879== indirectly lost: 0 bytes in 0 blocks
==5879== possibly lost: 0 bytes in 0 blocks
==5879== still reachable: 0 bytes in 0 blocks
==5879== suppressed: 0 bytes in 0 blocks
==5879==
==5879== For counts of detected and suppressed errors, rerun with: -v
==5879== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
|
结果解读:
definitely lost: 100 bytes in 1 block:确认存在内存泄漏,泄漏大小为100字节;
泄漏位置:hello_func (hello_func.c:9),即hello_func.c第9行的malloc操作未释放内存;
在hello_func函数末尾添加free(ptr);,重新编译后再次检测,内存泄漏即可消失。
5.2.3.3. 模拟内存越界并检测¶
为了演示Valgrind的内存越界并检测功能,修改test10项目的hello_func.c文件,添加数组越界访问的代码,模拟内存越界问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h>
#include "hello_func.h"
void hello_func(void)
{
int arr[5] = {1,2,3,4,5}; // 数组长度为5
printf("hello, world! This is a C program.\n");
// 模拟数组越界:访问arr[5],超出数组长度
printf("arr[5] = %d\n", arr[5]);
for (int i=0; i<10; i++ ) {
printf("output i=%d\n",i);
}
}
|
重新编译并使用Valgrind检测内存泄漏:
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 60 61 62 63 64 65 66 67 68 | # 清除编译输出
make clean
# 编译
make
# Valgrind检测内存越界
valgrind --leak-check=full ./hello_main_debug
# 信息输出如下
==4214== Memcheck, a memory error detector
==4214== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4214== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==4214== Command: ./hello_main_debug
==4214==
hello, world! This is a C program.
==4214== Conditional jump or move depends on uninitialised value(s)
==4214== at 0x48D04A8: vfprintf (vfprintf.c:1637)
==4214== by 0x48D6D43: printf (printf.c:33)
==4214== by 0x1087EB: hello_func (hello_func.c:11)
==4214== by 0x108833: main (hello_main.c:5)
==4214==
==4214== Use of uninitialised value of size 8
==4214== at 0x48CD288: _itoa_word (_itoa.c:179)
==4214== by 0x48CFF47: vfprintf (vfprintf.c:1637)
==4214== by 0x48D6D43: printf (printf.c:33)
==4214== by 0x1087EB: hello_func (hello_func.c:11)
==4214== by 0x108833: main (hello_main.c:5)
==4214==
==4214== Conditional jump or move depends on uninitialised value(s)
==4214== at 0x48CD290: _itoa_word (_itoa.c:179)
==4214== by 0x48CFF47: vfprintf (vfprintf.c:1637)
==4214== by 0x48D6D43: printf (printf.c:33)
==4214== by 0x1087EB: hello_func (hello_func.c:11)
==4214== by 0x108833: main (hello_main.c:5)
==4214==
==4214== Conditional jump or move depends on uninitialised value(s)
==4214== at 0x48D0B70: vfprintf (vfprintf.c:1637)
==4214== by 0x48D6D43: printf (printf.c:33)
==4214== by 0x1087EB: hello_func (hello_func.c:11)
==4214== by 0x108833: main (hello_main.c:5)
==4214==
==4214== Conditional jump or move depends on uninitialised value(s)
==4214== at 0x48D0014: vfprintf (vfprintf.c:1637)
==4214== by 0x48D6D43: printf (printf.c:33)
==4214== by 0x1087EB: hello_func (hello_func.c:11)
==4214== by 0x108833: main (hello_main.c:5)
==4214==
arr[5] = 0
output i=0
output i=1
output i=2
output i=3
output i=4
output i=5
output i=6
output i=7
output i=8
output i=9
==4214==
==4214== HEAP SUMMARY:
==4214== in use at exit: 0 bytes in 0 blocks
==4214== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==4214==
==4214== All heap blocks were freed -- no leaks are possible
==4214==
==4214== For counts of detected and suppressed errors, rerun with: -v
==4214== Use --track-origins=yes to see where uninitialised values come from
==4214== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)
|
结果解读:
越界位置:hello_func (hello_func.c:11),即hello_func.c第11行访问arr[5]导致越界;
修改数组访问下标,确保不超出0-4的范围,重新编译后检测,错误消失。
5.3. strace工具¶
strace是Linux下一款系统调用跟踪工具,可实时监控程序执行过程中调用的所有系统调用, 并显示每个系统调用的参数、返回值、执行时间等信息。 当程序出现“无法打开文件”“权限不足”“读取失败”等与系统交互相关的错误时,strace可快速定位问题原因。
5.3.1. strace简介¶
5.3.1.1. strace核心功能¶
系统调用跟踪:跟踪程序执行过程中调用的所有系统调用,显示系统调用的名称、参数、返回值;
信号跟踪:跟踪程序接收和处理的信号,如Ctrl+C发送的SIGINT信号;
时间统计:统计每个系统调用的执行时间、程序总执行时间,便于分析程序性能瓶颈;
文件描述符跟踪:跟踪程序打开的文件描述符,定位“文件无法打开”等问题;
输出重定向:将跟踪日志输出到文件,便于后续分析。
5.3.1.2. strace安装与启动¶
strace通常需要手动安装,执行以下命令进行安装:
1 | sudo apt update && sudo apt install strace -y
|
验证安装并查看版本:
1 | strace --version
|
strace启动方式:
1 2 | # 基本格式:
strace [选项] 可执行程序 [程序参数]
|
5.3.2. strace命令参数¶
strace的命令参数较多,以下梳理最常用、最核心的参数,程序以hello_main_debug为例:
-o 文件名:将跟踪日志输出到指定文件,避免终端输出过多
1 | strace -o strace.log ./hello_main_debug
|
-t:在每个系统调用前显示当前时间,精确到秒
1 | strace -t ./hello_main_debug
|
-tt:显示当前时间,精确到微秒
1 | strace -tt ./hello_main_debug
|
-T:显示每个系统调用的执行时间
1 | strace -T ./hello_main_debug
|
-e 系统调用名:只跟踪指定的系统调用,如只跟踪write、open,过滤无关信息
1 | strace -e write ./hello_main_debug
|
-p 进程ID:跟踪正在运行的进程,需知道进程ID,适用于后台运行的程序
1 | strace -p 12345
|
-f:跟踪程序创建的子进程,如fork创建的子进程,默认不跟踪子进程
1 | strace -f ./hello_main_debug
|
5.3.3. strace实操示例¶
以下以Makefile构建工具章节的test10项目编译得到的hello_main_debug为例,演示strace的使用流程, 包括跟踪程序的系统调用、定位文件访问问题。
5.3.3.1. 跟踪程序的所有系统调用¶
使用strace跟踪hello_main_debug程序的所有系统调用,查看程序与系统的交互过程:
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 | #进入配套代码对应目录
cd lubancat_rk_code_storage/base_linux/makefile/test10
#编译
make
# 使用strace检测调试版程序
strace ./hello_main_debug
#信息输出如下
execve("./hello_main_debug", ["./hello_main_debug"], 0x7fdff9b4f0 /* 18 vars */) = 0
brk(NULL) = 0x5589d9c000
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
fstat(3, {st_mode=S_IFREG|0644, st_size=144434, ...}) = 0
mmap(NULL, 144434, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f88c72000
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\360\16\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1435448, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f88cc1000
mmap(NULL, 1507424, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f88b01000
mprotect(0x7f88c59000, 61440, PROT_NONE) = 0
mmap(0x7f88c68000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x157000) = 0x7f88c68000
mmap(0x7f88c6e000, 12384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f88c6e000
close(3) = 0
mprotect(0x7f88c68000, 16384, PROT_READ) = 0
mprotect(0x5567d3f000, 4096, PROT_READ) = 0
mprotect(0x7f88cc5000, 4096, PROT_READ) = 0
munmap(0x7f88c72000, 144434) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL) = 0x5589d9c000
brk(0x5589dbd000) = 0x5589dbd000
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 +++
|
结果解读:
execve:启动程序的核心系统调用,用于加载并执行hello_main_debug可执行文件,返回值为0表示执行成功;
openat、read、close:程序运行时读取头文件hello_func.h的系统调用,openat打开文件返回文件描述符3,read读取文件内容,close关闭文件;
write(1, …):write系统调用用于向标准输出(文件描述符1)输出内容,后面的数字表示输出的字节数,与程序的printf输出对应;
exit_group(0):程序正常退出,返回值0表示程序无错误终止。
5.3.3.2. 过滤指定系统调用¶
当程序输出异常时,可只跟踪write系统调用,过滤无关信息,快速定位输出相关问题:
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 | # 只跟踪write系统调用,查看程序的输出过程
strace -e write ./hello_main_debug
# 信息输出如下
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
+++ exited with 0 +++
|
结果解读:仅显示与输出相关的write系统调用,可快速确认每个printf语句是否正常执行、输出字节数是否正确, 若某条write调用返回值异常(如-1),则说明该输出操作失败。
5.3.3.3. 定位文件访问失败问题¶
修改hello_func.c文件,添加“读取不存在的文件”的代码,模拟文件访问失败场景,用strace定位问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h>
#include "hello_func.h"
void hello_func(void)
{
FILE *fp = fopen("test_not_exist.txt", "r"); // 打开不存在的文件
if (fp == NULL) {
printf("file open failed!\n");
} else {
fclose(fp);
}
printf("hello, world! This is a C program.\n");
for (int i=0; i<10; i++ ) {
printf("output i=%d\n",i);
}
}
|
重新编译并使用strace检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #清除编译输出
make clean
#编译
make
#使用strace跟踪程序,定位文件打开失败的原因
strace ./hello_main_debug
#信息输出如下
...(省略前面系统调用)
openat(AT_FDCWD, "test_not_exist.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "file open failed!\n", 18file open failed!
) = 18
...(省略后续正常系统调用)
|
结果解读:
openat(AT_FDCWD, “test_not_exist.txt”, O_RDONLY) = -1 ENOENT:openat系统调用尝试以只读模式打开test_not_exist.txt文件,返回值-1表示失败,错误原因ENOENT(文件不存在);
创建test_not_exist.txt文件,或修改代码中的文件名,重新编译后运行即可正常打开文件。
5.3.3.4. 跟踪后台运行程序的系统调用¶
若程序后台运行,如守护进程,可通过进程ID跟踪其系统调用。
修改hello_func.c文件,增大循环次数和添加延时,让程序执行时间更长:
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h>
#include <unistd.h>
#include "hello_func.h"
void hello_func(void)
{
printf("hello, world! This is a C program.\n");
for (int i=0; i<10000; i++ ) {
printf("output i=%d\n",i);
sleep(1);
}
}
|
重新编译并使用strace检测:
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 | #清除编译输出
make clean
#编译
make
# 后台运行hello_main_debug程序,记录进程ID
./hello_main_debug &
[1] 30274 # 进程ID为30274
# 打开新终端用strace跟踪该后台进程
strace -p 30274
#信息输出如下
strace: Process 30274 attached
restart_syscall(<... resuming interrupted nanosleep ...>) = 0
write(1, "output i=27\n", 12) = 12
nanosleep({tv_sec=1, tv_nsec=0}, 0x7fef3e3a48) = 0
write(1, "output i=28\n", 12) = 12
nanosleep({tv_sec=1, tv_nsec=0}, 0x7fef3e3a48) = 0
write(1, "output i=29\n", 12) = 12
nanosleep({tv_sec=1, tv_nsec=0}, 0x7fef3e3a48) = 0
write(1, "output i=30\n", 12) = 12
....
# 跟踪结束后,终止后台进程
kill 30274
|
5.3.3.5. 将跟踪日志输出到文件¶
当程序执行过程中系统调用较多时,终端输出会刷屏,可将日志输出到文件,后续逐步分析:
1 2 3 4 5 | # 将strace跟踪日志输出到strace_hello.log文件
strace -o strace_hello.log ./hello_main_debug
# 查看日志文件
less strace_hello.log
|
日志文件中会完整记录程序执行过程中的所有系统调用,可通过搜索关键词,如“open”“write”“error”快速定位问题。
5.4. 三款工具对比总结¶
GDB、Valgrind、strace三款工具各有侧重,覆盖Linux下C/C++程序的大部分调试场景,三款工具对比如下:
工具名称 |
核心功能 |
适用场景 |
|---|---|---|
GDB |
断点调试、单步执行、变量查看、堆栈跟踪、core文件分析 |
程序逻辑错误、崩溃定位、变量异常、函数调用流程排查 |
Valgrind |
内存泄漏、内存越界、未初始化内存、双重释放等内存问题检测 |
C/C++程序内存相关错误,尤其是隐藏较深的内存泄漏、越界问题 |
strace |
系统调用跟踪、信号跟踪、文件描述符跟踪、时间统计 |
程序与系统交互错误、文件访问失败、权限问题、后台进程异常 |
使用建议:
程序崩溃、逻辑异常:优先使用GDB,通过断点和堆栈跟踪定位错误位置;
程序运行正常但偶尔崩溃、结果异常,优先使用Valgrind,排查内存问题;
程序无法打开文件、权限不足、后台卡死,优先使用strace,跟踪系统调用定位问题;
复杂问题可结合三款工具使用。
