自己动手写一个 strace
这次主要分享一下一个动手的东西,就是自己动手写一个 strace
工具。
用过 strace
的同学都知道,strace
是用来跟踪进程调用的 系统调用
,还可以统计进程对 系统调用
的统计等。strace
的使用方式有两种,如下:
strace
执行的程序
strace -p
进程pid
第一种用于跟踪将要执行的程序,而第二种用于跟踪一个运行中的进程。
下图就是使用 strace
对 ls
命令跟踪的结果:
ptrace系统调用
要自己动手写 strace
的第一步就是了解 ptrace()
系统调用的使用,我们来看看 ptrace()
系统调用的定义:
int ptrace(long request, long pid, long addr, long data);
ptrace()
系统调用用于跟踪进程的运行情况,下面介绍一下其各个参数的含义:
request
:指定跟踪的动作。也就是说,通过传入不同的request
参数可以对进程进行不同的跟踪操作。其可选值有:PTRACE_TRACEME
PTRACE_PEEKTEXT
PTRACE_POKETEXT
PTRACE_CONT
PTRACE_SINGLESTEP
...
pid
:指定要跟踪的进程PID。addr
:指定要读取或者修改的内存地址。data
:对于不同的request
操作,data
有不同的作用,下面会介绍。
前面介绍过,使用 strace
跟踪进程有两种方式,一种是通过 strace
命令启动进程,另外一种是通过 -p
指定要跟踪的进程。
ptrace()
系统调用也提供了两种 request
来实现上面两种方式:
第一种通过
PTRACE_TRACEME
来实现第二种通过
PTRACE_ATTACH
来实现
本文我们主要介绍使用第一种方式。由于第一种方式使用跟踪程序来启动被跟踪的程序,所以需要启动两个进程。通常要创建新进程可以使用 fork()
系统调用,所以自然而然地我们也使用 fork()
系统调用。
我们新建一个文件 strace.c
,输入代码如下:
int main(int argc, char *argv[])
{pid_t child;child = fork();if (child == 0) {// 子进程...} else {// 父进程...}return 0;
}
上面的代码通过调用 fork()
来创建一个子进程,但是没有做任何事情。之后,我们就会在 子进程
中运行被跟踪的程序,而在 父进程
中运行跟踪进程的代码。
运行被跟踪程序
前面说过,被跟踪的程序需要在子进程中运行,而要运行一个程序,可以通过调用 execl()
系统调用。所以可以通过下面的代码,在子进程中运行 ls
命令:
#include <unistd.h>
#include <stdlib.h>int main(int argc, char *argv[])
{pid_t child;child = fork();if (child == 0) {execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {// 父进程...}return 0;
}
execl()
用于执行指定的程序,如果执行成功就不会返回,所以 execl(...)
的下一行代码 exit(0)
不会被执行到。
由于我们需要跟踪 ls
命令,所以在执行 ls
命令前,必须调用 ptrace(PTRACE_TRACEME, 0, NULL, NULL)
来告诉系统需要跟踪这个进程,代码如下:
#include <sys/ptrace.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char *argv[])
{pid_t child;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {// 父进程...}return 0;
}
这样,被跟踪进程部分的代码就完成了,接下来开始编写跟踪进程部分代码。
编写跟踪进程代码
如果编译运行上面的代码,会发现什么效果也没有。这是因为当在子进程调用 ptrace(PTRACE_TRACEME, 0, NULL, NULL)
后,并且调用 execl()
系统调用,那么子进程会发送一个 SIGCHLD
信号给父进程(跟踪进程)并且自己停止运行,直到父进程发送调试命令,才会继续运行。
由于上面的代码中,父进程(跟踪进程)并没有发送任何调试命令就退出运行,所以子进程(被跟踪进程)在没有运行的情况下就跟着父进程一起退出了,那么就不会看到任何效果。
现在我们开始编写跟踪进程的代码。
由于被跟踪进程会发送一个 SIGCHLD
信息给跟踪进程,所以我们先要在跟踪进程的代码中接收 SIGCHLD
信号,接收信号通过使用 wait()
系统调用完成,代码如下:
#include <sys/ptrace.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(int argc, char *argv[])
{pid_t child;int status;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号}return 0;
}
上面的代码通过调用 wait()
系统调用来接收被跟踪进程发送过来的 SIGCHLD
信号,接下来需要开始向被跟踪进程发送调试命令,来对被跟踪进程进行调试。
由于本文介绍怎么跟踪进程调用了哪些 系统调用
,所以我们需要使用 ptrace()
的 PTRACE_SYSCALL
命令,代码如下:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(int argc, char *argv[])
{pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号}return 0;
}
从上面的代码可以发现,我们调用了两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL)
,这是因为跟踪系统调用时,需要跟踪系统调用前的环境(比如获取系统调用的参数)和系统调用后的环境(比如获取系统调用的返回值),所以就需要调用两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL)
。
获取进程寄存器的值
Linux系统调用是通过 CPU寄存器
来传递参数的,所以要想获取调用了哪个系统调用,必须获取进程寄存器的值。获取进程寄存器的值,可以通过 ptrace()
系统调用的 PTRACE_GETREGS
命令来实现,代码如下:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(int argc, char *argv[])
{pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号ptrace(PTRACE_GETREGS, child, 0, ®s); // 获取被跟踪进程寄存器的值orig_rax = regs.orig_rax; // 获取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号}return 0;
}
上面的代码通过调用 ptrace(PTRACE_GETREGS, child, 0, ®s)
来获取进程寄存器的值,PTRACE_GETREGS
命令需要在 data
参数传入类型为 user_regs_struct
结构的指针,user_regs_struct
结构定义如下(在文件 sys/user.h
中):
struct user_regs_struct {unsigned long r15,r14,r13,r12,rbp,rbx,r11,r10;unsigned long r9,r8,rax,rcx,rdx,rsi,rdi,orig_rax;unsigned long rip,cs,eflags;unsigned long rsp,ss;unsigned long fs_base, gs_base;unsigned long ds,es,fs,gs;
};
其中 user_regs_struct
结构的 orig_rax
保存了系统调用号,所以我们可以通过 orig_rax
的值来知道调用了哪个系统调用。
编译运行上面的代码,会输出结果:orig_rax: 12
,就是说当前调用的是编号为 12 的系统调用。那么编号为 12 的系统调用是哪个系统调用呢?可以通过下面链接来查看:
https://www.cnblogs.com/gavanwanggw/p/6920826.html
通过查阅系统调用表,可以知道编号 12 的系统调用为 brk()
,如下:
系统调用号 函数名 入口点 源码
...
12 brk sys_brk mm/mmap.c
...
上面的程序只跟踪了一个系统调用,那么怎么跟踪所有的系统调用呢?很简单,只需要把跟踪的代码放到一个无限循环中即可。代码如下:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(int argc, char *argv[])
{pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号while (1) {// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号if (WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 获取被跟踪进程寄存器的值orig_rax = regs.orig_rax; // 获取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号if (WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪break;}}}return 0;
}
if (WIFEXITED(status)) ...
这行代码用于判断子进程(被跟踪进程)是否已经退出,如果退出了就停止跟踪。现在可以编译并运行这个程序,输出结果如下:
[root@localhost liexusong]$ ./strace
orig_rax: 12
orig_rax: 9
orig_rax: 21
orig_rax: 2
orig_rax: 5
orig_rax: 9
orig_rax: 3
orig_rax: 2
orig_rax: 0
orig_rax: 5
orig_rax: 9
orig_rax: 10
orig_rax: 9
orig_rax: 9
orig_rax: 3
orig_rax: 2
orig_rax: 0
orig_rax: 5
orig_rax: 9
orig_rax: 10
...
从执行结果来看,只是打印系统调用号不太直观,那么我们怎么优化呢?
我们可以定义一个系统调用号与系统调用名的对应表来实现更清晰的输出结果,如下:
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>struct syscall {int code;char *name;
} syscall_table[] = {{0, "read"},{1, "write"},{2, "open"},{3, "close"},{4, "stat"},{5, "fstat"},{6, "lstat"},{7, "poll"},{8, "lseek"},...{-1, NULL},
}char *find_syscall_symbol(int code) {struct syscall *sc;for (sc = syscall_table; sc->code >= 0; sc++) {if (sc->code == code) {return sc->name;}}return NULL;
}int main(int argc, char *argv[])
{pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号while (1) {// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 获取被跟踪进程寄存器的值orig_rax = regs.orig_rax; // 获取rax寄存器的值printf("syscall: %s()\n", find_syscall_symbol(orig_rax)); // 打印系统调用// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪break;}}}return 0;
}
上面例子添加了一个函数 find_syscall_symbol()
来获取系统调用号对应的系统调用名,实现也比较简单。编译运行后输出结果如下:
[root@localhost liexusong]$ ./strace
syscall: brk()
syscall: mmap()
syscall: access()
syscall: open()
syscall: fstat()
syscall: mmap()
syscall: close()
syscall: open()
syscall: read()
syscall: fstat()
syscall: mmap()
syscall: mprotect()
syscall: mmap()
syscall: mmap()
syscall: close()
...
从执行结果来看,现在可以打印系统调用的名字了,但我们知道 strace
命令还会打印系统调用参数的值,我们可以通过 ptrace()
系统调用的 PTRACE_PEEKTEXT
和 PTRACE_PEEKDATA
来获取参数的值,所以有兴趣的就自己实现这个效果了。
本文完整代码在:
https://github.com/liexusong/build-strace-by-myself/blob/main/strace.c
推荐阅读:
专辑|Linux文章汇总
专辑|程序人生
专辑|C语言
我的知识小密圈
关注公众号,后台回复「1024」获取学习资料网盘链接。
欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~
嵌入式Linux
微信扫描二维码,关注我的公众号
自己动手写一个 strace相关推荐
- java 手编线程池_死磕 java线程系列之自己动手写一个线程池
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...
- 自己动手写一个印钞机 第四章
2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...
- Spring Boot 动手写一个 Start
我们在使用SpringBoot 项目时,引入一个springboot start依赖,只需要很少的代码,或者不用任何代码就能直接使用默认配置,再也不用那些繁琐的配置了,感觉特别神奇.我们自己也动手写一 ...
- 自己动手写一个nodejs的日志生成器
自己动手写一个nodejs的logger 最近正在边学边用node.js开发个人应用的server,由于有用到websocket相关,想对websocket的通信选择性的做下日志记录,所以萌发了自己动 ...
- 自己动手写一个印钞机 第二章
2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...
- 学习较底层编程:动手写一个C语言编译器
动手编写一个编译器,学习一下较为底层的编程方式,是一种学习计算机到底是如何工作的非常有效方法. 编译器通常被看作是十分复杂的工程.事实上,编写一个产品级的编译器也确实是一个庞大的任务.但是写一个小巧可 ...
- java 同步锁_死磕 java同步系列之自己动手写一个锁Lock
问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...
- 吕文翰 php,自己动手写一个 iOS 网络请求库(三)——降低耦合
自己动手写一个 iOS 网络请求库(三)--降低耦合 2015-5-22 / 阅读数:16112 / 分类: iOS & Swift 本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实 ...
- 自己动手写一个操作系统——MBR(1)
文章目录 前言 MBR 1) 512 字节镜像 2) 0x55 和 0xAA qemu 运行 参考 前言 上篇<自己动手写一个操作系统--我们能做什么,我们需要做什么>我们介绍到 BIOS ...
最新文章
- 初学图论-Bellman-Ford单源最短路径算法
- C++结构体,联合体
- docker-compose报错:(root) Additional property mail-service is not allowed
- 最新综述:用于文本分类的数据增强方法
- Excel多因素可重复方差分析
- java ipmitool_ipmitool使用手册
- 17-基于51单片机的银行排队叫号系统设计
- 魔兽单机服务器修改升级属性,魔兽世界单机版怎么调整人物级别(用户使用)?20分...
- 细说 Java 中的浅克隆与深克隆
- 秒赚大钱_容易记住,赚大钱
- 苹果电脑win10蓝牙音响卡顿_如何修复Windows 10蓝牙扬声器的声音延迟问题
- 基于中文维基百科的词向量构建及可视化
- 【天光学术】诉讼法论文:论交通肇事罪的认定与处理【开题报告 法学硕士研究生毕业论文】
- 迁移学习---迁移学习领域各位大佬的ppt,视频下载(百度云链接)
- C#编程学习35:对MDB数据库的操作
- bpftrace 指南
- 为什么C语言中int的表示范围是-32768~32767
- 四波混频在波导上的应用
- 《社会契约论》读后感
- 4.5 集成运放的种类及选择