基本上任何使用了一段时间 Linux 的人,最后都会知道并爱上 strace 命令。strace 是系统调用跟踪器,它跟踪程序执行的进入内核以与外面的世界交互的调用。如果你还不熟悉这个令人惊奇的多才多艺的工具,我建议你看一下我的朋友和合作伙伴 Greg Price 的出色的博客 blog post 中关于这一主题的内容,然后再回到这里。

我们都爱 strace,但你是否曾经好奇它是如何工作的呢?它是如何把它自己注入到内核和用户空间程序之间的呢?这篇博客将用大约 70 行 C 代码走查一个小小的 strace 实现。它的功能不会像真的那样好,但在这个过程中,你将了解关于它使用的核心接口所需了解的大部分内容。

在 Linux(还可能在其它一些 UNIX)上 strace 使用了被称为 [ptrace](http://linux.die.net/man/2/ptrace) 的有点神秘的接口,进程追踪接口。ptrace 允许一个进程监视另一个进程的状态,并深入调查(或甚至是控制)它的内部状态。

ptrace 是一个复杂的系统调用,它接收一个神奇的 “request” 首参数,然后依赖于它的值执行完全不同的事情。它通常的原型看起来像这样:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

然而,由于不同的 request 值使用剩余的从 0 个到 3 个参数,glibc 中它的原型为可变参数函数,允许一个开发者只列出给定调用所需要的参数个数。

为了使一个进程跟踪另一个,它附到那个进程上,并临时变为那个进程的父进程。当一个进程被 ptraced,跟踪器可以请求它的子进程随时在各种事件发生时停下来,比如子进程执行了一个系统调用。当这发生时,内核将以 SIGTRAP 停止子进程。由于此时跟踪器是子进程的父进程,这样它就可以使用标准的 UNIX waitpid 系统调用观察到这一点。

我们的小型 strace 将只支持 stracestrace COMMAND 形式(对照 strace -p),并且我们将只打印系统调用号和返回值 - 不解码名字或参数或任何其它事情。因此一次简单的运行可能看起来像下面这样:

$ ./ministrace ls
…
syscall(6) = 0
syscall(54) = 0
syscall(54) = 0
syscall(5) = 3
syscall(221) = 1
syscall(220) = 272
syscall(220) = 0
syscall(6) = 0
syscall(197) = 0
syscall(192) = -1219706880

尽管不是世界上最有用的东西,但它展示了核心的跟踪工具。因此,让我们来看下代码:

#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

我们从必要的头文件开始。sys/ptrace.h 定义了 ptrace__ptrace_request 常量,我们还将需要 sys/reg.h 帮忙解码系统调用。更多相关的内容在后面。其它的你应该都认得出来。

int do_child(int argc, char **argv);
int do_trace(pid_t child);int main(int argc, char **argv) {if (argc < 2) {fprintf(stderr, "Usage: %s prog args\n", argv[0]);exit(1);}pid_t child = fork();if (child == 0) {return do_child(argc-1, argv+1);} else {return do_trace(child);}
}

我们将从入口点开始。我们检查我们被传入了一个命令,然后我们通过 fork() 创建两个进程 - 一个用于执行被跟踪的程序,而另一个跟踪它。

int do_child(int argc, char **argv) {char *args [argc+1];memcpy(args, argv, argc * sizeof(char*));args[argc] = NULL;

子进程从一些琐碎的参数整理开始,这是由于 execvp 想要一个由 NULL 终止的参数数组。

    ptrace(PTRACE_TRACEME);kill(getpid(), SIGSTOP);return execvp(args[0], args);
}

接下来,我们仅执行提供的参数列表,但首先,我们需要启动跟踪进程,以使父进程可以开始在非常早期就开始跟踪新执行的程序。

如果子进程知道它想要被跟踪,它可以执行 PTRACE_TRACEME ptrace 请求,这将启动追踪。此外,这意味着下一个发送给这个进程的信号将停止它并通知它的父进程(通过 wait),这样父进程就知道要开始跟踪了。因此,在执行了一个 TRACEME 之后,我们 SIGSTOP 我们自己,以使父进程可以通过 exec 调用继续我们的执行。

(你可能已经注意到了,strace COMMAND 输出总是以一个 execve 调用开始。现在你应该已经理解为什么了 —— 实际上,我们打算在 kill 返回后立即开始跟踪,因此我们看到了启动新程序的 execve 调用。)

int wait_for_syscall(pid_t child);int do_trace(pid_t child) {int status, syscall, retval;waitpid(child, &status, 0);

与此同时,在父进程中,我们声明了稍后需要的函数的原型,并开始跟踪。我们立即开始 waitpid 在子进程上,一旦子进程给自己发送了上面的SIGSTOP,它将返回,并准备好被跟踪。

    ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);

我前面提到 ptrace 基本上把子进程上的所有事件都转为 SIGTRAP。这很不方便,因为它意味着当你看到子进程由于 SIGTRAP 而停止时,没有很好的办法来知道它是由于它可能停止的多种原因中的哪种而停止的。

PTRACE SETOPTIONS 允许我们设置许多选项来控制我们要如何跟踪子进程。这里我们使用它来设置 PTRACE_O_TRACESYSGOOD,这意味着当子进程由于系统调用相关的原因停止时,我们实际上会看到它以信号号SIGTRAP | 0x80 停止,这样我们可以简单地从其它停止中区分出系统调用导致地停止。由于(出于这个 demo 的目的),我们只关注系统调用,这还是非常方便的。

    while(1) {if (wait_for_syscall(child) != 0) break;

现在我们进入跟踪循环。wait_for_syscall,在下面定义,将运行子进程直到进入或退出一个系统调用。如果它返回非 0,则子进程已经退出,我们终止循环。

        syscall = ptrace(PTRACE_PEEKUSER, child, sizeof(long)*ORIG_EAX);fprintf(stderr, "syscall(%d) = ", syscall);

否则,尽管,我们知道子进程进入了一个系统调用,这样我们需要解码系统调用号(以及潜在的参数,如果这是一个不那么简单的例子)。PTRACE_PEEKUSER ptrace 请求从子进程的 “user area” 读取一个字的数据,这是一个逻辑区域,它持有它所有的寄存器和其它的内部非内存状态。在 i386 上,系统调用号位于 %eax。出于各种各样的技术原因,然而,内核在此时已经破坏了子进程的 %eax,但它在一个不同的偏移量处保存了原始值,ORIG_EAX,这来自于 sys/regs.h

        if (wait_for_syscall(child) != 0) break;

一旦我们有了系统调用号,我们再次 wait_for_syscall,这应该会让我们停止在系统调用返回处。

        retval = ptrace(PTRACE_PEEKUSER, child, sizeof(long)*EAX);fprintf(stderr, "%d\n", retval);

i386 上的返回值也是在 %eax 中传递的,因此这次我们可以直接读取它,并打印返回值,然后返回到循环的顶部并等待下一次系统调用。

     }return 0;
}

一旦子进程退出,我们也返回。

int wait_for_syscall(pid_t child) {int status;while (1) {ptrace(PTRACE_SYSCALL, child, 0, 0);

wait_for_syscall 是一个简单的辅助函数。我们使用 PTRACE_SYSCALL 来继续子进程,这允许一个停止的子进程继续执行直到下一次进入或退出一个系统调用。

        waitpid(child, &status, 0);

然后我们 waitpid 等待有趣的事情发生在子进程身上。

        if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80)return 0;

由于我们上面设置的 PTRACE_O_SYSGOOD ,我们可以通过检查被停止的子进程是否由一个最高位设置了的信号停止的来探测一个系统调用停止。如果是这样,我们就返回。

        if (WIFEXITED(status))return 1;}
}

如果子进程退出,我们就完成了;否则,它是因为我们不关心的原因而停止的(例如,execve),因此我们循环再次启动它,直到它遇到系统调用。

这就是它的全部。如果你想下载并试用,你可以在 github 上找到我刚刚发布的版本。

让它更有用

虽然它可以工作,但我认为以前的版本并不是特别有用。你不得不手动解码系统调用号,且你无法获得任何系统调用参数。

把代码都包含在这篇博客中可能有点长,但我已经把一个稍微更实用的版本发布到了相同的 github 仓库的 master 。它包含一个 Python 脚本来扫描 Linux 源码以提取系统调用号及参数个数和类型,且它知道如何解码字符串参数,以使你可以看到文件名及 readwrite 的数据。

读取参数很容易 —— 在 i386 上,它们在寄存器中传递,因此,对于每一个参数,只是另一次 PTRACE_GETUSER。也许最有趣的片段就是 read_string 函数了,它用于从子进程中读取一个 NULL 结尾的字符串。(当然,以 NULL 结尾是不正确的 —— 真正的 strace 知道 read()write()count 参数,比如。但这已经足够做一个 demo 了。)

char *read_string(pid_t child, unsigned long addr) {

read_string 接收一个要读取的子进程的进程 ID,及它打算读取的字符串的地址作为参数:

    char *val = malloc(4096);int allocated = 4096, read;unsigned long tmp;

我们需要一些变量。一个拷入字符串的缓冲区,我们已经拷贝的数据及分配的数据的计数器,及一个临时变量用于读取内存。

    while (1) {if (read + sizeof tmp > allocated) {allocated *= 2;val = realloc(val, allocated);}

我们在必要时增加缓冲。我们一次一个字地读取数据。

        tmp = ptrace(PTRACE_PEEKDATA, child, addr + read);if(errno != 0) {val[read] = 0;break;}

PTRACE_PEEKDATA 返回子进程在指定偏移量处的数据工作。因为它使用返回值,所以我们需要检查 errno 来判断它是否失败。如果它失败了(可能由于子进程传递了一个无效的指针),我们仅返回我们截止目前已经获得的字符串,确保在最后添加我们自己的 NULL。

        memcpy(val + read, &tmp, sizeof tmp);if (memchr(&tmp, 0, sizeof tmp) != NULL)break;read += sizeof tmp;

然后,将我们读到的数据附加起来就很简单了,如果我们发现一个终止 NULL 就跳出循环,否则循环读取另一个字。

    }return val;
}

【原文】Write yourself an strace in 70 lines of code

用 70 行代码给你自己写一个 strace相关推荐

  1. 用70行代码实现日志分析程序​

    python又一力作,感受python的强大.用70行代码实现日志分析程序 功能介绍:可直接对文本日至进行分组和排序功能,完了输出结果粘贴到excel里就可以直接生成图表,对于排查一些生产环境问题有很 ...

  2. 爬取微信文章,用70行代码爬取了搜狗上666篇文章

    因为再看崔庆才的教程,刚好看到爬取微信文章,所以就想着自己试试.打开搜狗发现,搜狗的微信文章页面网页布局有了变化(准确来说是简单了一点). 所以分析了一下,用了70行代码实现了爬取上面['搞笑', ' ...

  3. 70行代码撸一个桌面自动翻译神器(采用Markdown格式编写)

    70行代码撸一个桌面自动翻译神器 前言 工作上经常需要与外国友人邮件沟通,奈何工作电脑没有安装有道词典一类的翻译软件,结合自己的需要,自己撸一个桌面翻译神器. 基本思路:基于PySimpleGUI开发 ...

  4. react的导出是怎么实现的_不到一百行代码,我们来实现一个简简简简简简简简简简版react库...

    good evening everybody!这是一篇关于react故事的文章,这个故事主要是讲在一个夜黑风高晚上,react从一个VDOM变成真实DOM的过程. 这个过程react经历了从JSX-& ...

  5. python画哆啦a梦图片_80行代码!用Python做一个哆来A梦分身

    原标题:80行代码!用Python做一个哆来A梦分身 对于分身术,大家想必都或多或少的从<火影忍者>的动漫上看到过,炫酷的影分身场面,每每看到都觉得非常过瘾. 今天, 小编其实是蓝胖子的铁 ...

  6. 自己动手写一个 strace

    这次主要分享一下一个动手的东西,就是自己动手写一个 strace 工具. 用过 strace 的同学都知道,strace 是用来跟踪进程调用的 系统调用,还可以统计进程对 系统调用 的统计等.stra ...

  7. 开发者70行代码破解苹果OSX远程锁定安全功能

    苹果的 Mac OS X 有一项"Find My Mac"的防盗功能,开启这项功能后用户可以通过自己的 iOS 鼠标远程锁定 Mac 机器,只有输入正确的 4 位 PIN 后才能对 ...

  8. 70行代码实现同花顺,通达信,麦语言大部分技术指标公式

    MyTT是什么? MyTT将通达信,同花顺,文华麦语言等指标公式indicators,最简移植到Python中,核心库单个文件,仅百行代码,实现所有常见指标MACD,RSI,BOLL,ATR,KDJ, ...

  9. 手写数字识别c语言作业,10 行代码,实现手写数字识别

    识别手写的阿拉伯数字,对于人类来说十分简单,但是对于程序来说还是有些复杂的. 不过随着机器学习技术的普及,使用10几行代码,实现一个能够识别手写数字的程序,并不是一件难事.这是因为有太多的机器学习模型 ...

最新文章

  1. Unable to execute dex: Multiple dex files define Lcom/myapp/R$array;
  2. Asp.net Dynamic Data之三改变编辑和操作数据的现实方式
  3. drm linux 内核,Linux内核DRM实现分析——基于i915
  4. EM Alogrithm
  5. 有关数据库MySQL的演讲_有关Mysql数据库编程的文章推荐10篇
  6. Npm如何升级package.json
  7. 主存储器与CPU的连接
  8. EasyUI加zTree使用解析 easyui修改操作的表单回显方法 验证框提交表单前验证 datagrid的load方法
  9. Windows 自启动总结《转》
  10. 精通Windows Sockets 网络开发-基于Visual C++实现
  11. python中arcsec_python – 更好的方法来计算Skyfield中两个物体的明显角度分离?
  12. css 背景图片虚化磨砂效果
  13. 数据可视化工具-Vue-DataV入门
  14. 05 共识问题:区块链如何确认记账权?
  15. cad计算机绘注意事项,CAD打印的基本操作和重要的注意事项
  16. 感谢我的数据结构老师王卓
  17. spark dataframe 一列分隔多列,一列分隔多行(scala)
  18. Web全栈~18.jQuery
  19. 多应用多平台支付模块设计-基础模块开篇
  20. HTML基础--CSS样式表(二)

热门文章

  1. MyBatis从缓存查找数据的依据
  2. Bootstrap全局css样式_表单
  3. Disruptor并发框架-1
  4. 服务器升级中不能修改信息,服务器升级页面
  5. 优化 UI 应用启动时间的方法
  6. 计算机领域中,增量是什么意思?
  7. 常用软件滤波算法---摘自:FeoTech
  8. (科普帖)电梯突然断电下坠时、一定要这么做
  9. 前端必知必会--JSON.stringify()犀利的第三个参数
  10. hibernate JPA 双向多对多   bi-directional many-to-many association