自己动手写一个 strace

这次主要分享一下一个动手的东西,就是自己动手写一个 strace 工具。

用过 strace 的同学都知道,strace 是用来跟踪进程调用的 系统调用,还可以统计进程对 系统调用 的统计等。strace 的使用方式有两种,如下:

  • strace 执行的程序

  • strace -p 进程pid

第一种用于跟踪将要执行的程序,而第二种用于跟踪一个运行中的进程。

下图就是使用 strace 对 ls 命令跟踪的结果:

e4153de1cf2e7da3992b1088f0cb8368.png

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, &regs); // 获取被跟踪进程寄存器的值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, &regs) 来获取进程寄存器的值,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, &regs); // 获取被跟踪进程寄存器的值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, &regs); // 获取被跟踪进程寄存器的值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」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

58408a12cf3bdea5f5e6e9a4a133e9ac.png

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/464863.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Asp.net网页中使用接口

在开发Asp.net时&#xff0c;我们会经常有应用MasterPage或是WebUserControl。这样会遇上一个问题&#xff0c;需要在aspx去找MasterPage或是WebUserControl内的对象&#xff0c;或是从aspx传值给它们。比如一个WebUserControl被aspx调用之后&#xff0c;它产生的ID会随着aspx的…

xss绕过尖括号和双括号_xss挑战平台练习

-------------------------XSS挑战之旅-------------------------最近在学习xss&#xff0c;找到了一个xss练习平台&#xff0c;在线地址&#xff1a;http://test.xss.tv/实验环境也可以本地搭建&#xff0c;不过需要phpmysql的环境&#xff1a;xss通关小游戏&#xff1a;https…

上世纪八九十年代的收录放音机拆解

头条看到刘工发的收录放音机美图&#xff0c;发上来大家回忆下&#xff0c;大家跟这些老古董有什么故事&#xff1f;推荐阅读&#xff1a;专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈关注公众号&#xff0c;后台回复「1024」获取学习资料网盘链接。欢迎点赞&…

String in Java

转载于:https://www.cnblogs.com/ywxt/p/java_string.html

dfs hdfs 修改文件名称_CDH6.3.2生产更换HDFS 数据目录

事情是这样的&#xff0c;楼主刚入职一家新公司&#xff0c;刚开始搭建的集群时ecs只有一个系统盘&#xff0c;集群安装完成运行几天后&#xff0c;分分钟就要爆了&#xff0c;于是申请增加磁盘&#xff0c;就有了下面的操作&#xff0c;生怕操作失误&#xff0c;集群挂了&…

c++thread里暂停线程_多线程技术

1.程序程序(Program)”是一个静态的概念&#xff0c;一般对应于操作系统中的一个可执行文件&#xff0c;比如&#xff1a;我们要启动酷狗听音乐&#xff0c;则对应酷狗的可执行程序。当我们双击酷狗&#xff0c;则加载程序到内存中&#xff0c;开始执行该程序&#xff0c;于是产…

出差CVTE

去CVTE和连总吃饭周一晚上11点&#xff0c;我给我们领导打电话&#xff0c;说有个问题一定要去CVTE。晚上到家的时候是12点多&#xff0c;没洗澡&#xff0c;我跟小云说我早上要6点起来去广州&#xff0c;小云问我说去广州干嘛&#xff0c;我说&#xff0c;去出差解决问题。​然…

html实现 左图右文_让CSS flex布局最后一行左对齐的N种方法

作者&#xff1a;张鑫旭https://www.zhangxinxu.com/wordpress/2019/08/css-flex-last-align/前言小伙伴们是否还记得&#xff0c;之前小编也发布了几篇关于CSS相关文章不妨一起来回顾回顾&#xff1a;《手把手整理CSS3知识汇总【思维导图】》《关于前端CSS写法104个知识点汇总…

定位到元素后获取其属性_Selenium界面自动化测试(4)(Python):元素定位及操作...

在操作Web元素之前&#xff0c;需要先找到该元素&#xff0c;这个查找的过程称之为元素定位。Selenium支持8种元素定位方法&#xff1a;ID&#xff1a;根据元素的id属性值来定位元素。Name&#xff1a;根据元素的name属性值来定位元素。Class Name&#xff1a;根据元素的class属…

芯片公司急聘嵌入式软件精英人才

薪酬待遇及联系方式薪酬待遇&#xff1a;40~150万不等&#xff0c;另有股票期权等&#xff0c;待遇丰厚&#xff0c;详细面议工作地点&#xff1a;北京市朝阳区联系人&#xff1a;杨先生微信&#xff1a;MichaelYao7PS&#xff1a;推荐成功入职者有推荐费嵌入式软件工程师岗位职…

这一年就要过去

「远方钟声响起&#xff0c;这一年就要过去&#xff0c;快乐时光不停留......」楠哥从幼儿园新学习了一首新年小曲。今年已经是12月份了&#xff0c;2021年就要结束&#xff0c;即将到来的是2022年的新的一年。2021年这一年里非常感谢读者的一路支持&#xff0c;当然这不是一篇…

unc 隐藏共享文件夹_你真的了解任务栏吗?win10任务栏居然隐藏了这么多小窍门...

平常使用电脑&#xff0c;最常用的是桌面&#xff0c;桌面上最常用的大约是任务栏&#xff0c;但是你真的了解任务栏吗&#xff1f;除了一小部分朋友熟悉之外&#xff0c;大部分人估计都是没有留意到&#xff0c;其实任务栏就功能角度来说&#xff0c;的确算一个宝藏之地。什么…

手机充电IC设置电流与实测电流不一致问题剖析

大家好&#xff0c;我是写代码的篮球球痴&#xff0c;转一篇我朋友记得诚的文章本文转载来自一位基带大佬&#xff0c;解决问题的思路很重要。原文链接&#xff1a;https://blog.csdn.net/AirCity123/article/details/104428325?spm1001.2014.3001.5502某手机的充电架构如下&a…

Spring系列(六) Spring Web MVC 应用构建分析

DispatcherServlet DispatcherServlet 是Spring MVC的前端控制器名称, 用户的请求到达这里进行集中处理, 在Spring MVC中, 它的作用是为不同请求匹配对应的处理器, 将结果传递给视图解析器最终呈现给客户端. 前端控制器模式&#xff08;Front Controller Pattern&#xff09;是…

做个好人,加个晚班

我和建平在腾讯加班的日子前几天&#xff0c;他让我给他发照片&#xff0c;因为公司里年末要发照片墙&#xff0c;他说要把我的和他的照片放上去&#xff0c;然后我硬是找了几张觉得不错的给他&#xff0c;然后他也真的放上去了。再看那些照片&#xff0c;拍出来的效果都不咋样…

中ridge_10种线性代数在数据科学中的强大应用(内附多种资源)

原文选自 | Analytics Vidhya作者 | Khyati Mahendru本文转载自 TalkingData数据学堂 &#xff0c;未经允许禁止转载本文摘要线性代数为各种各样的数据科学算法和应用提供支持在这里&#xff0c;我会向您介绍通过线性代数帮助您成为更好的数据科学家的10种实际应用我们已将这些…

语言与golang语言运行速度_Golang语言情怀第13期 Go 语言设计模式 介绍

设计模式是什么俗话说&#xff1a;站在别人的肩膀上&#xff0c;我们会看得更远。设计模式的出现可以让我们站在前人的肩膀上&#xff0c;通过一些成熟的设计方案来指导新项目的开发和设计&#xff0c;以便于我们开发出具有更好的灵活性和可扩展性&#xff0c;也更易于复用的软…

苦练IoT应用开发,还能加速变现,这个机会别错过

都说人间大事&#xff0c;不过吃喝二字。厨房经济近年来显示出了巨大发展潜力&#xff0c;智能厨电已成为潮流趋势。智慧厨电究竟是如何——让厨房小白做出一顿可口大餐&#xff1f;让懒人摆脱厨房油烟和洗碗的困扰&#xff1f;让怕冷的人喝到永远55℃的热水&#xff1f;……在…

android人脸识别demo_零门槛解决Windows人脸识别应用开发难题

自人脸识别免费SDK——ArcFace3.0上线以来&#xff0c;凭借对人脸识别、活体检测、年龄检测、性别检测等核心算法模型进行全面升级&#xff0c;大幅提升算法鲁棒性&#xff0c;显著降低接入门槛&#xff0c;同时支持Windows、iOS、Android&#xff08;包含Android10&#xff09…

Visual Studio会让嵌入式开发变得更香

在几个月之前&#xff0c;我一直非常喜欢用Source Insight看代码&#xff0c;主要是习惯了原来的风格。从Source Insight 转到vscode 的原因是&#xff0c;在腾讯使用samba连接Source Insight看代码非常非常卡&#xff0c;让我觉得很难受。然后是在同事的建议下更换了vscode,里…