自己动手写一个 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…

18.设计模式

单例模式 保证类在内存中只有一个对象使用原则 私有构造方法(不让其它类来创建本类对象) 在本类中创建一个本类对象或向外提供一个创建本类对象的共有方法编码方式 饿汉模式 class Singleton { //1,私有构造函数 private Singleton(){} //2,创建本类对象 private static Single…

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

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

String in Java

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

mysql性能测试

1.测试环境 服务器:CPU:2GHZ,内存:4G,物理机系统:centos 5 32位网络:100Mbps 局域网客户端:CPU:2GHZ,内存:4G系统:centos 5 32位数据量:1000万条记录mysql版本:5.1.51-log测试方法:把mysql挂到DNS软件后面&#xff0c;用bind的测试工具querypref测试测试时&#xff0c;启动五个…

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

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

在知乎上泡了这么久,工作、工资、生活还是没有起色是什么原因?

陆游同学在八百多年前就回答了这个问题&#xff1a;纸上觉来终觉浅&#xff0c;绝知此事要躬行。重新解读一遍题主的问题&#xff0c;题主其实想表达的是&#xff1a;泡知乎这么久了生活都没有起色&#xff0c;我TM这么辛苦虔诚地天天泡知乎有毛用&#xff1f;而我们都知道人生…

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

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

Cron表达式 详解

Cron表达式是一个字符串&#xff0c;字符串以5或6个空格隔开&#xff0c;分为6或7个域&#xff0c;每一个域代表一个含义&#xff0c;Cron有如下两种语法格式&#xff1a; &#xff08;1&#xff09; 7个域&#xff1a; Seconds Minutes Hours DayofMonth Month DayofWeek Year…

出差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个知识点汇总…

数据库SQL ServerSQL Server教程:详细学习游标

游标(Cursor)是处理数据的一种方法&#xff0c;为了查看或者处理结果集中的数据&#xff0c;游标提供了在结果集中一次以行或者多行前进或向后浏览数据的能力。我们可以把游标当作一个指针&#xff0c;它可以指定结果中的任何位置&#xff0c;然后允许用户对指定位置的数据进行…

编码能力被公司破制度给限制了

大家好&#xff0c;我是写代码的篮球今天跟很久前一个同事聊了会天&#xff0c;他和我一样做嵌入式软件开发&#xff0c;聊天中没有什么寒暄和吐槽&#xff0c;或许就是简简单单的知晓了双方的近况便早早结束了。当同事之间没有了太多的交际也就没有那么的话题了吧。不过那时候…

Django (八) 中间件验证码富文本缓存

中间件&验证码&富文本&缓存 1. 中间件&AOP 中间件&#xff1a;是一个轻量级的&#xff0c;底层的插件&#xff0c;可以介入Django的请求和响应过程&#xff08;面向切面编程&#xff09; ​ 中间件的本质就是一个python类 ​ 面向切面编程&#xff08;Aspect O…

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

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

数据库定时导出和互备一例

环境&#xff1a;内网中有三台服务器做测试用&#xff0c;两台SUSE LINUX 10 和一台RHEL 5.5&#xff0c;都安装有ORACLE11G 需求&#xff1a;测试环境搭建完毕&#xff0c;因没有外部存储进行统一备份而修改和更新又比较频繁&#xff0c;所以需要在三台服务器间搭建互备以实现…

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

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

Python进程学习笔记-multiprocessing模块

如果你打算编写多进程的服务程序&#xff0c;Unix/Linux无疑是正确的选择。由于Windows没有fork调用&#xff0c;难道在Windows上无法用Python编写多进程的程序&#xff1f; 由于Python是跨平台的&#xff0c;自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨…

这一年就要过去

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