深入linux c/c++调试工具之strace
目录
一、前言-strace工具简介
strace 是 Linx 下强大的系统调用跟踪工具,可监控进程与内核的交互,包括 系统调用 (system calls) 和 信号 (signals)。它常用于调试、性能分析、权限问题排查以及文件/网络访问诊断。
二、使用场景-何时使用strace
1、排查程序启动失败或立刻退出
典型现象:命令执行后,程序无任何输出便直接退出;或执行后报错,常见错误包括 Permission denied(权限拒绝)、No such file(文件不存在)、Segmentation fault(段错误)等。
2、诊断进程卡死,无响应,假死
典型现象:进程还在,但是不工作;cpu不高,服务不响应。
3、分析拒绝权限根源
典型现象:文件明明存在,却打不开;权限看起来够,但是仍然失败。
4、网络连接问题
典型现象:连接超时,拒绝,客户端连不上。
5、依赖库,动态链接问题
典型现象:版本不对,符号找不到。
6、追踪多进程/子进程行为异常
典型现象:多线程程序只跑一部分;fork后子进程没起来。
7、性能瓶颈和耗时统计
典型现象:程序运行慢,不知问题是系统层还是业务层。
8、逆向陌生程序行为
典型现象:使用了没有源码的第三方二进制,不知道读了哪些文件,连了哪些地址,调用了哪些命令。
9、容器,部署环境差异
典型现象:程序本机正常,容器异常;路径、权限、环境变量不同。
三、核心原理讲解-工作原理深度解析
strace的底层依赖ptrace接口,进程跟踪机制,strace 用来跟踪进程与内核之间的 系统调用(syscall) 及收到的 信号。理解它的核心,就是理解:用户程序如何陷入内核,以及跟踪器如何在不改源码的情况下截获这次陷入。
基本流程如下:

关键ptrace操作:
| 操作 | 作用 |
|---|---|
|
|
子进程请求被父进程跟踪( |
|
|
附加到已运行进程( |
|
|
在 每次 syscall 进入和退出 时停止 |
|
|
读取 tracee 的内存、寄存器 |
|
|
让 tracee 继续运行 |
x86_64 上,用户态通过 syscall 指令 进入内核。被 ptrace 跟踪的进程在 syscall 前后会收到 SIGTRAP,内核将其挂起,tracer 即可:读寄存器(syscall 号、参数),从 tracee 地址空间读字符串、结构体,打印日志,放行,等待下一次 syscall。
四、命令参数-常用选项详解
strace命令基本用法
usage: strace [-CdffhiqrtttTvVwxxy] [-I n] [-e expr]...
[-a column] [-o file] [-s strsize] [-P path]...
-p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]
or: strace -c[dfw] [-I n] [-e expr]... [-O overhead] [-S sortby]
-p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]
| 参数 | 说明 |
| -a | 对齐列宽:syscall 结果显示的对齐列位置(默认 40) |
| -i | 打印发生 syscall 时的 指令指针 |
| -o file | 将跟踪输出写入 FILE,而不是 stderr |
| -q | 静默模式:不显示 attach/detach 等提示信息 |
| -r | 打印 相对时间戳(相对上一条 syscall 的时间间隔) |
| -s strsize | 限制打印字符串的最大长度为 STRSIZE 字符(默认 32) |
| -t | 打印 绝对时间戳 |
| -tt | 打印带 微秒 的绝对时间戳 |
| -T | 打印 每次 syscall 内部耗时 |
| -x | 以 十六进制 打印非 ASCII 字符串 |
| -xx | 以 十六进制 打印 所有 字符串 |
| -y | 打印与 文件描述符(fd) 参数关联的路径 |
| -yy | 打印与 socket fd 相关的 协议详细信息 |
| -c | 统计每个 syscall 的 时间、调用次数、错误数,并输出汇总报告 |
| -C | 类似 -c,但 同时打印 常规逐条跟踪输出 |
| -O overhead | 设置跟踪 syscall 的 开销校正值 为 OVERHEAD 微秒 |
| -S sortby | 对 syscall 统计结果排序,可选:time(时间)、calls(次数)、name(名称)、nothing(不排序);默认按 time |
| -W | 汇总 syscall 延迟(默认统计系统时间) |
| -e rxpr | 过滤表达式,格式:option=[!]all 或 option=[!]val1[,val2]... |
| -P path | 只跟踪对 path 的访问 |
| -b execve | 在 execve syscall 时 detach(不再跟踪 exec 后的新程序) |
| -D | 以 分离的孙进程 方式运行 tracer,而不是作为被跟踪进程的父进程 |
| -f | 跟踪 fork 产生的子进程 |
| -ff | 跟踪 fork,且 每个 PID 输出到单独文件(需配合 -o) |
| -I interruptible | 控制跟踪过程中 信号阻塞 行为 |
| -E var | 从 command 的 环境中删除 变量 var |
| -E var=val | 在 command 的 环境中设置 var=val |
| -p pid | 跟踪指定 PID 的进程;可 重复使用(-p 1234 -p 5678) |
| -u username | 以 username 身份运行 command,并正确处理 setuid/setgid |
| -d | 启用调试输出到 stderr(strace 自身调试信息) |
| -v | 详细模式:打印 未缩写 的 argv、stat、termios 等参数 |
| -V | 打印 strace 版本号 |
五、使用实战-实际案例分析
示例代码strace_demo.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#define DEMO_FILE "/tmp/strace_demo_test.txt"
static void demo_open_close(void)
{
int fd;
char buf[128];
ssize_t n;
printf("[1] open / read / write / close\n");
fd = open(DEMO_FILE, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return;
}
if (write(fd, "hello strace demo\n", 18) < 0)
perror("write");
if (lseek(fd, 0, SEEK_SET) < 0)
perror("lseek");
n = read(fd, buf, sizeof(buf) - 1);
if (n < 0) {
perror("read");
} else {
buf[n] = '\0';
printf(" read back: %s", buf);
}
if (close(fd) < 0)
perror("close");
}
static void demo_malloc_free(void)
{
char *p1, *p2;
size_t i;
printf("[2] malloc / free (可能触发 brk 或 mmap)\n");
p1 = malloc(64);
p2 = malloc(1024 * 1024); /* 1MB,较容易看到 mmap */
if (!p1 || !p2) {
perror("malloc");
free(p1);
free(p2);
return;
}
for (i = 0; i < 64; i++)
p1[i] = (char)('A' + (i % 26));
p1[63] = '\0';
printf(" p1(64B) = %s\n", p1);
memset(p2, 0x5a, 1024 * 1024);
printf(" p2(1MB) filled\n");
free(p1);
free(p2);
}
static void demo_fork(void)
{
pid_t pid;
int status;
printf("[3] fork / wait (子进程会 open 另一个文件)\n");
pid = fork();
if (pid < 0) {
perror("fork");
return;
}
if (pid == 0) {
/* 子进程 */
int fd = open("/tmp/strace_demo_child.txt",
O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write(fd, "from child\n", 11);
close(fd);
}
_exit(42); /* 子进程退出码 42 */
}
/* 父进程 */
if (waitpid(pid, &status, 0) < 0) {
perror("waitpid");
return;
}
if (WIFEXITED(status))
printf(" child exit code = %d\n", WEXITSTATUS(status));
}
static void demo_access_stat(void)
{
printf("[4] access / stat (文件是否存在)\n");
if (access(DEMO_FILE, F_OK) == 0)
printf(" %s exists\n", DEMO_FILE);
else
printf(" %s missing: %s\n", DEMO_FILE, strerror(errno));
}
int main(void)
{
printf("=== strace demo start (pid=%d) ===\n", getpid());
demo_open_close();
demo_malloc_free();
demo_access_stat();
demo_fork();
printf("=== strace demo end ===\n");
return 0;
}
运行命令strace ./strace_demo,查看结果
execve("./strace_demo", ["./strace_demo"], [/* 23 vars */]) = 0
brk(NULL) = 0x108a9000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76fefc201000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=39835, ...}) = 0
mmap(NULL, 39835, PROT_READ, MAP_PRIVATE, 3, 0) = 0x76fefc1f7000
close(3) = 0
open("/lib64/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>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2156592, ...}) = 0
mmap(NULL, 3985920, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x76fefbc13000
mprotect(0x76fefbdd7000, 2093056, PROT_NONE) = 0
mmap(0x76fefbfd6000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) = 0x76fefbfd6000
mmap(0x76fefbfdc000, 16896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x76fefbfdc000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76fefc1f6000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76fefc1f4000
arch_prctl(ARCH_SET_FS, 0x76fefc1f4740) = 0
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/sysconfig/strcasecmp-nonascii", F_OK) = -1 ENOENT (No such file or directory)
mprotect(0x76fefbfd6000, 16384, PROT_READ) = 0
mprotect(0x601000, 4096, PROT_READ) = 0
mprotect(0x76fefc202000, 4096, PROT_READ) = 0
munmap(0x76fefc1f7000, 39835) = 0
getpid() = 961945
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76fefc200000
write(1, "=== strace demo start (pid=96194"..., 39=== strace demo start (pid=961945) ===
) = 39
write(1, "[1] open / read / write / close\n", 32[1] open / read / write / close
) = 32
open("/tmp/strace_demo_test.txt", O_RDWR|O_CREAT|O_TRUNC, 0644) = 3
write(3, "hello strace demo\n", 18) = 18
lseek(3, 0, SEEK_SET) = 0
read(3, "hello strace demo\n", 127) = 18
write(1, " read back: hello strace demo"..., 33 read back: hello strace demo
) = 33
close(3) = 0
write(1, "[2] malloc / free (\345\217\257\350\203\275\350\247\246\345\217\221 "..., 46[2] malloc / free (可能触发 brk 或 mmap)
) = 46
brk(NULL) = 0x108a9000
brk(0x108ca000) = 0x108ca000
brk(NULL) = 0x108ca000
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76fefc0f3000
write(1, " p1(64B) = ABCDEFGHIJKLMNOPQ"..., 79 p1(64B) = ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJK
) = 79
write(1, " p2(1MB) filled\n", 20 p2(1MB) filled
) = 20
munmap(0x76fefc0f3000, 1052672) = 0
write(1, "[4] access / stat (\346\226\207\344\273\266\346\230\257\345\220\246\345"..., 39[4] access / stat (文件是否存在)
) = 39
access("/tmp/strace_demo_test.txt", F_OK) = 0
write(1, " /tmp/strace_demo_test.txt ex"..., 37 /tmp/strace_demo_test.txt exists
) = 37
write(1, "[3] fork / wait (\345\255\220\350\277\233\347\250\213\344\274\232 op"..., 52[3] fork / wait (子进程会 open 另一个文件)
) = 52
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x76fefc1f4a10) = 961946
wait4(961946, [{WIFEXITED(s) && WEXITSTATUS(s) == 42}], 0, NULL) = 961946
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=961946, si_uid=1017, si_status=42, si_utime=0, si_stime=0} ---
write(1, " child exit code = 42\n", 25 child exit code = 42
) = 25
write(1, "=== strace demo end ===\n", 24=== strace demo end ===
) = 24
exit_group(0) = ?
+++ exited with 0 +++
过程分析:
总览:程序生命周期

阶段1:程序加载execve
| 项目 | 含义 |
|---|---|
|
syscall |
|
|
参数1 |
要执行的二进制路径 |
|
参数2 |
|
|
参数3 |
环境变量(23 个,如 |
|
返回值 0 |
加载成功(对 strace 而言这是 被跟踪进程 的第一条记录) |
阶段2:动态链接器加载libc
| syscall | 作用 |
|---|---|
|
|
检查预加载库配置;ENOENT 正常(多数系统没有该文件) |
|
|
打开动态链接器缓存,加速找 |
|
|
打开 C 标准库 |
|
|
读 ELF 头( |
|
|
把 libc 映射进内存(代码只读+可执行,数据可读写) |
|
|
设置内存保护(如 GOT/PLT 页) |
阶段3:demo_open_close()-文件读写
| 行 | 对应 C 代码 | 说明 |
|---|---|---|
|
|
|
fd 1 = 标准输出 |
|
|
|
创建/截断文件,返回 fd=3 |
|
|
|
写入 18 字节 |
|
|
|
文件指针回到开头 |
|
|
|
读回 18 字节 |
|
|
|
关闭 fd |
阶段4:demo_malloc_free()-内存分配
| 现象 | 解释 |
|---|---|
|
|
小对象在 堆 上,通过 |
|
|
大块内存 glibc 常用 mmap 分配(约 1052672 ≈ 1MB+ 管理开销) |
|
|
释放大块时 munmap 归还;小块可能只 |
阶段5:demo_access_stat()-检查文件
| 项目 | 含义 |
|---|---|
|
|
文件 存在(阶段 3 刚创建) |
|
若为 |
文件不存在 |
阶段6:demo_fork() 创建子进程
| 行 | 含义 |
|---|---|
|
|
Linux 上 |
|
|
父进程等待子进程,退出码 42(对应子进程 |
|
|
子进程退出时内核发给父进程的信号(strace 会显示) |
阶段7:程序退出
| 项目 | 含义 |
|---|---|
|
|
终止 整个进程组(所有线程),退出码 0 |
|
|
strace 提示:被跟踪进程正常结束 |
六、常见问题-疑难解答
1、跟踪 setuid / 高权限程序失败
系统禁止 ptrace 跟踪 提升过权限 的程序,防止泄露敏感信息。不要用 strace 直接跟踪 setuid 程序。
2、输出太多,程序极慢,日志巨大
strace 对 每次 syscall 停两次(进入 + 退出),高频 read/write/futex 的程序开销巨大。先窄范围跟踪,确认方向后再扩大。
3、路径被截断,看不出完整文件名
默认 -s 32,字符串参数只显示 32 字节。排查 路径/配置错误 时,务必加大 -s。
4、多线程程序日志混乱
多线程共享进程空间,默认输出 不按线程分开。跟踪所有线程,设置每个tid单独日志文件,设置带时间戳。
5、程序「卡住」,strace 也停在某一行
strace 会改变时序,偶发死锁可能 strace 下不复现;去掉 strace 后问题又出现,需结合 -T、-r 或 gdb 进一步查。
6、看不到 printf / malloc 等函数调用
strace 只跟踪 系统调用,不跟踪 库函数。纯用户态计算、printf 缓冲未 flush、内存分配(走已有 brk/mmap)等,可能 没有新 syscall。
7、strace 下正常,不用就异常
ptrace 会 减慢执行、改变竞态时序,多线程/超时/锁相关 bug 尤其敏感。
七、总结-要点回顾
- strace 跟踪的是 syscall,不是业务逻辑——它回答「程序在向内核要什么」。
- 原理是 ptrace,代价是慢和改时序——调试利器,不是生产监控方案。
- 先窄后宽、先过滤后全量——配合 errno、
-c、-T和 dmesg/ftrace,效率最高。
更多推荐


所有评论(0)