目录

一、前言-strace工具简介

二、使用场景-何时使用strace

1、排查程序启动失败或立刻退出

2、诊断进程卡死,无响应,假死

3、分析拒绝权限根源

4、网络连接问题

5、依赖库,动态链接问题

6、追踪多进程/子进程行为异常

7、性能瓶颈和耗时统计

8、逆向陌生程序行为

9、容器,部署环境差异

三、核心原理讲解-工作原理深度解析

四、命令参数-常用选项详解

五、使用实战-实际案例分析

六、常见问题-疑难解答

七、总结 - 要点回顾


一、前言-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操作:

操作 作用

PTRACE_TRACEME

子进程请求被父进程跟踪(strace cmd

PTRACE_ATTACH

附加到已运行进程(strace -p PID

PTRACE_SYSCALL

在 每次 syscall 进入和退出 时停止

PTRACE_PEEKDATA / PTRACE_PEEKUSER

读取 tracee 的内存、寄存器

PTRACE_CONT

让 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

execve — 加载并运行新程序

参数1

要执行的二进制路径

参数2

argv["./strace_demo"]

参数3

环境变量(23 个,如 PATHHOME 等)

返回值 0

加载成功(对 strace 而言这是 被跟踪进程 的第一条记录)

阶段2:动态链接器加载libc

syscall 作用

access ld.so.preload

检查预加载库配置;ENOENT 正常(多数系统没有该文件)

open ld.so.cache

打开动态链接器缓存,加速找 .so

open libc.so.6

打开 C 标准库

read ... ELF

读 ELF 头(\177ELF = 0x7f 'E' 'L' 'F')

mmap

把 libc 映射进内存(代码只读+可执行,数据可读写)

mprotect

设置内存保护(如 GOT/PLT 页)

阶段3:demo_open_close()-文件读写

对应 C 代码 说明

write(1, ...)

printf → write(stdout)

fd 1 = 标准输出

open(..., O_RDWR|O_CREAT|O_TRUNC, 0644) = 3

open(DEMO_FILE, ...)

创建/截断文件,返回 fd=3

write(3, "hello...", 18) = 18

write(fd, ...)

写入 18 字节

lseek(3, 0, SEEK_SET) = 0

lseek(fd, 0, SEEK_SET)

文件指针回到开头

read(3, ..., 127) = 18

read(fd, buf, ...)

读回 18 字节

close(3) = 0

close(fd)

关闭 fd

阶段4:demo_malloc_free()-内存分配

现象 解释

malloc(64) → brk

小对象在 堆 上,通过 brk 扩堆

malloc(1MB) → mmap ~1MB

大块内存 glibc 常用 mmap 分配(约 1052672 ≈ 1MB+ 管理开销)

free → munmap

释放大块时 munmap 归还;小块可能只 brk 调整,不一定每条 free 都有 syscall

阶段5:demo_access_stat()-检查文件

项目 含义

access(..., F_OK) = 0

文件 存在(阶段 3 刚创建)

若为 -1 ENOENT

文件不存在

阶段6:demo_fork() 创建子进程

含义

clone(...) = 961946

Linux 上 fork() 底层是 clone;返回 子进程 PID

wait4(961946, ..., WEXITSTATUS=42)

父进程等待子进程,退出码 42(对应子进程 _exit(42)

SIGCHLD

子进程退出时内核发给父进程的信号(strace 会显示)

阶段7:程序退出

项目 含义

exit_group(0)

终止 整个进程组(所有线程),退出码 0

+++ exited with 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 尤其敏感。

七、总结-要点回顾

  1. strace 跟踪的是 syscall,不是业务逻辑——它回答「程序在向内核要什么」。
  2. 原理是 ptrace,代价是慢和改时序——调试利器,不是生产监控方案。
  3. 先窄后宽、先过滤后全量——配合 errno、-c-T 和 dmesg/ftrace,效率最高。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐