造轮子-strace(二)实现
这一篇文章会介绍strace如何工作,再稍微深入介绍一下什么是system call。再介绍一下ptrace、wait(strace依赖的system call)。最后再一起来造个轮子,动手用代码实现一个strace。聊天框回复“strace”,可以获取本文源码。
上一篇,我们介绍了strace工具,strace是非常实用的调试、分析工具,可以记录process调用system call的参数、返回值。
效果展示
下面展示一下我们实现简化版的strace的效果,每行打印一个system call。只不过没有根据system call的序号转换参数类型来打印,毕竟我们实现的目的是学习。
root@xxx:~/code/case/case20_ptrace/tracer$ ./xx_strace /usr/bin/ls
brk(0) = 0x5626edd99000
arch_prctl() = -22
access(0x7f5b2155f9e0, 4) = -2
openat(0xffffff9c, 0x7f5b2155cb80, 524288, 0) = 3
fstat(3, 0x7ffc965a7fe0) = 0
mmap(0, 33585, 1, 2, 3, 0) = 0x7f5b2152e000
close(3) = 0
openat(0xffffff9c, 0x7f5b21566e10, 524288, 0) = 3
read(3, 0x7ffc965a8188, 832) = 832
fstat(3, 0x7ffc965a8030) = 0
mmap(0, 8192, 3, 34, 0xffffffff, 0) = 0x7f5b2152c000
mmap(0, 174600, 1, 2050, 3, 0) = 0x7f5b21501000
mprotect(0x7f5b21507000, 135168, 0) = 0
mmap(0x7f5b21507000, 102400, 5, 2066, 3, 24576) = 0x7f5b21507000
mmap(0x7f5b21520000, 28672, 1, 2066, 3, 126976) = 0x7f5b21520000
mmap(0x7f5b21528000, 8192, 3, 2066, 3, 155648) = 0x7f5b21528000
mmap(0x7f5b2152a000, 6664, 3, 50, 0xffffffff, 0) = 0x7f5b2152a000
close(3) = 0
openat(0xffffff9c, 0x7f5b2152c4e0, 524288, 0) = 3
read(3, 0x7ffc965a8168, 832) = 832
1、system call
上一篇,我们简单介绍了系统调用(system call)。strace就是记录system call的工具,我们需要深入了解一下system call。
1.1、system call序号
每个system call都有一个序号,记录在
/usr/include/x86_64-linux-gnu/asm/unistd_64.h文件中。我们常见的read、write、open都在其中定义。
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
...
1.2、使用syscall直接调用system call
我们可以调用glibc封装的system call(例如close、connect、bind等),还可以使用syscall直接调用。glibc中的封装最终也是调用了syscall。
long syscall(long number, ...);
例如我们调用tgkill时
int tgkill(int tgid, int tid, int sig);
我们可以使用glibc封装的
tgkill(getpid(), tid, SIGHUP);
也可以使用syscall直接传system call 序号和对于参数来调用。
syscall(SYS_tgkill, getpid(), tid, SIGHUP);
1.3、syscall参数、返回值
我们需要了解一下调用syscall时,用户层与内核是交互交互返回值和参数的。
根据man syscall手册。不同的cpu通过不同寄存器来传递。
返回值:
Arch/ABI Instruction System Ret Ret Error Notes
call # val val2
───────────────────────────────────────────────────────────────────
arm64 svc #0 x8 x0 x1 -
x86-64 syscall rax rax rdx - 5
x32 syscall rax rax rdx - 5
x86-64位下,返回值在rax寄存器。
参数列表:
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────
arm64 x0 x1 x2 x3 x4 x5 -
x86-64 rdi rsi rdx r10 r8 r9 -
x32 rdi rsi rdx r10 r8 r9 -
x86-64位下,参数依次是rdi rsi rdx r10 r8 r9。
2、strace工作流程
首先介绍我们把tracer和tracee的概念:我们把跟踪者(strace)叫做tracer,被跟踪process叫做tracee。
strace整体工作流程如下:
- 蓝色部分:建立trace关系。
- 橙色部分:子进程执行指令。
- 绿色部分:循环跟踪tracee的system call。
3、ptrace && wait
磨刀不误砍柴工,我们也来介绍一下strace工作时两个重要函数。
3.1、ptrace
通过上面流程图,可以看出strace在建立trace关系、跟踪system call时都依赖ptrace。
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
man ptrace
The ptrace() system call provides a means by which
one process (the "tracer") may observe and control the execution of another process (the "tracee"),
and examine and change the tracee's memory and registers.
It is primarily used to implement breakpoint debugging and system call tracing.
man手册是这么描述的:ptrace可以让tracer观察并控制tracee的执行,并可以获取并修改tracee的内存和寄存器。可以用来实现调试器或system call跟踪。实际上gdb和strace都是依赖ptrace来实现的。
参数
- request:决定ptrace的行为,一会用到哪个介绍哪个。
- pid:tracee的pid,被监控者。
- addr,data:根据request不同有不同含义。
3.2、wait介绍
wait也是strace工作时也很重要,先看看man手册。
pid_t wait(int *wstatus);
man wait
wait is used to wait for state changes in a child of the calling process,
A state change is considered to be:
the child terminated;
the child was stopped by a signal;
or the child was resumed by a signal.
If a child has already changed state, then these calls return immediately.
Otherwise, they block until either a child changes state。
man手册是这么描述的:wait用来等待子进程状态改变,包括退出、stopped、resumed。
如果子进程状态已经改变了,wait会立刻返回。否则会卡住等待状态改变。
状态通过wstatus返回,wait也提供了一系列配套宏来判断状态。
strace使用wait有2个场景:
- 建立trace关系后,等待tracee变成stop状态。
- 开始跟踪,等到tracee调用system call。
4、strace实现
4.1、建立trace关系
strace工作的第一步就是建立trace关系,按照不同启动模式采取不同的方式建立。无论是哪种模式,都需要与tracee建立trace关系。才能监控system call的调用。
strace的启动模式:
- attach模式:strace -p pid,trace已经启动的进程。
- strace启动模式:strace cmd,trace新启动进程。
attach模式建立trace关系
strace调用ptrace(request=PTRACE_ATTACH)与tracee建立trace关系。
static bool xx_trace_attach(pid_t pid){
auto ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
X_CHECK(ret >=0 ,false);
return true;
}
strace启动模式建立trace关系
此模式下,strace需要先执行fork。然后父进程作为tracer,子进程作为tracee。
子进程fork以后,还需要执行ptrace(request=PTRACE_TRACEME)来建立trace关系。
static bool xx_trace_me()
{
auto ret = ptrace(PTRACE_TRACEME, 0L, 0L, 0L);
X_CHECK(ret >=0 ,false);
return true;
}
建立好trace关系以后,子进程还需要调用execv来执行tracee的逻辑。
void tracee(int argc, char *argv[])
{
xx_trace_me();
execv(argv[0], argv);
}
代码
无论是attach模式还是strace启动模式,建立trace关系后,都执行相同的逻辑,代码可以复用。
int main(int argc, char *argv[])
{
const char *spid = xx_get_arg(argc, argv, "-p");
// atach模式
if (nullptr != spid)
{
pid_t pid = atoi(spid);
xx_trace_attach(pid); // attch
tracer(pid); // 开始跟踪system call
}
// strace启动模式
else
{
pid_t pid = fork();
if (0 == pid)
{
tracee(argc - 1, argv + 1); // 执行tracee指令
}
else if (pid > 0)
{
tracer(pid); // 开始跟踪system call
}
}
return 0;
}
4.2、等待tracee进入stop状态
建立trace关系后,strace需要调用wait,来等待tracer变为stop状态。
// 等待tracee变为stop状态
int child_status = 0;
wait(&child_status);
printf("child_status=%s\n", xx_waitstate2str(child_status).c_str());
xx_waitstate2str是封装好,打印子进程状态的
static string xx_waitstate2str(int status)
{
if(WIFEXITED(status)) return "terminated normally\n";
if(WIFSIGNALED(status)) return "terminated by a signal\n";
if(WIFSTOPPED(status)) return "stopped by delivery of a signal\n";
if(WIFCONTINUED(status)) return "resumed\n";
return "state?\n";
}
屏幕输出
child_status=stopped by delivery of a signal
4.3、循环跟踪system call
建立好trace关系后,tracee是处于stop状态的。下一步开始循环跟踪tracee的system call。
strace使用ptrace 跟踪tracee的system call时会有两次拦截,一次是调用前,一次是调用完成后。
4.3.1、调用前拦截
调用前拦截时,有以下操作:
- strace唤醒:strace调用ptrace(request=PTRACE_SYSCALL),唤醒tracee继续执行。
- strace等待:strace调用wait等待,此时wait会卡住。
- tracee调用system call前:tracee会进入stop状态;strace调用wait返回,被唤醒。
- strace获取信息:stracewait返回后,可以调用ptrace(request=PTRACE_GETREGS)获取寄存器的信息。
调用前拦截时,system call还没被调用,通过寄存器信息,可以获取:
- 调用的system call的序号。
- 调用前的参数信息。(不过因为有些system call会通过参数向外传递信息,我们选择system call之后的拦截来获取参数。)
4.3.2、调用后拦截
- strace唤醒:同调用前拦截。
- strace等待:同调用前拦截。
- tracee调用system call后:同调用前拦截。
- strace获取信息:同调用前拦截。
调研后拦截时,system call已调用完毕。通过寄存器可以获取返回值,以及调用后的参数。前面1.3章节介绍了system call在不同cpu架构使用哪些寄存器。
4.3.3、拦截代码实现
下面我们来看看代码实现.
步骤1(唤醒)、2(等待)
我们封装了一个函数
void wait_syscall(pid_t child)
{
// 1.唤醒
int child_status = 0;
auto ret = ptrace(PTRACE_SYSCALL, child, 0, 0);
if (ret < 0)
{
X_P_INFO;
}
// 2. 等待
wait(&child_status);
// 3. 是否已退出?
if (WIFEXITED(child_status))
{
printf("exited in syscalls with status=%d\n", child_status);
exit(0);
}
}
循环跟踪主题
循环主题主要就是两次拦截、获取信息、打印信息。
while (1)
{
// 调用前拦截
syscall_info info;
wait_syscall(child);
{
// 获取寄存器信息
struct user_regs_struct reg;
bool get_reg = xx_trace_get_reg(child, reg);
assert(get_reg);
// 获取:system call 序号
info.set_before_call(reg);
}
// 调用后拦截
wait_syscall(child);
{
// 获取寄存器信息
struct user_regs_struct reg;
bool get_reg = xx_trace_get_reg(child, reg);
assert(get_reg);
// 获取:参数、返回值
info.set_after_call(reg);
}
// 打印信息
info.print();
}
保存system call信息
struct syscall_info
{
uint64_t syscall_no = 0;
uint64_t syscall_ret = 0;// 返回值
uint64_t para[6] = {0};// 参数
。。。
}
调用前拦截、获取system call序号
struct syscall_info
{
void set_before_call(const user_regs_struct )
{
syscall_no = reg.orig_rax;
}
}
调用后拦截、获取参数、返回值
struct syscall_info
{
void set_after_call(const user_regs_struct )
{
syscall_ret = reg.rax;
para[0] = reg.rdi;
para[1] = reg.rsi;
para[2] = reg.rdx;
para[3] = reg.r10;
para[4] = reg.r8;
para[5] = reg.r9;
}
}
不足200行代码,实现了strace基础功能。造个轮子能更好的学习,大家学会了么?
最后,东北码农,全网同名,求关注、点赞、转发,谢谢~
造轮子-strace(二)实现的更多相关文章
- 一起学习造轮子(二):从零开始写一个Redux
本文是一起学习造轮子系列的第二篇,本篇我们将从零开始写一个小巧完整的Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Red ...
- 造轮子-strace(一)
见字如面,我是东北码农. 本文是造轮子-strace的第一篇,我们先介绍strace的功能.使用.下一篇我们来用代码实现一下strace的功能,造个轮子.今天我们先观察.使用轮子. 1.什么是stra ...
- 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise
本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...
- 一起学习造轮子(三):从零开始写一个React-Redux
本文是一起学习造轮子系列的第三篇,本篇我们将从零开始写一个React-Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Re ...
- 【疯狂造轮子-iOS】JSON转Model系列之二
[疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...
- react入门之使用react-bootstrap当轮子造车(二)
react入门之使用react-bootstrap当轮子造车(二) 上一篇我们谈了谈如何配置react的webpack环境 react入门之搭配环境(一) 可能很多人已经打开过官方文档学习了react ...
- 一步一步造个IoC轮子(二),详解泛型工厂
一步一步造个Ioc轮子目录 一步一步造个IoC轮子(一):Ioc是什么 一步一步造个IoC轮子(二):详解泛型工厂 一步一步造个IoC轮子(三):构造基本的IoC容器 详解泛型工厂 既然我说IoC容器 ...
- 我为什么还要造轮子?欠踹?Monk.UI表单美化插件诞生记!
背景 目前市场上有很多表单美化的UI,做的都挺不错,但是他们都有一个共同点,那就是90%以上都是前端工程师开发的,导致我们引入这些UI的时候,很难和程序绑定.所以作为程序员的我,下了一个决定!我要自己 ...
- 自己造轮子系列之OOM框架AutoMapper
[前言] OOM框架想必大家在Web开发中是使用频率非常之高的,如果还不甚了解OOM框架,那么我们对OOM框架稍作讲解. OOM顾名思义,Object-Object-Mapping实体间相互转换.常见 ...
随机推荐
- 数据源(Data Source
数据源(Data Source)顾名思义,数据的来源,是提供某种所需要数据的器件或原始媒体.在数据源中存储了所有建立数据库连接的信息.就像通过指定文件名称可以在文件系统中找到文件一样,通过提供正确的数 ...
- 【编程思想】【设计模式】【行为模式Behavioral】chaining_method
Python版 https://github.com/faif/python-patterns/blob/master/behavioral/chaining_method.py #!/usr/bin ...
- Spring是如何保证同一事务获取同一个Connection的?使用Spring的事务同步机制解决:数据库刚插入的记录却查询不到的问题(转)
前言 关于Spring的事务,它是Spring Framework中极其重要的一块.前面用了大量的篇幅从应用层面.原理层面进行了比较全方位的一个讲解.但是因为它过于重要,所以本文继续做补充内容:Spr ...
- 【Java 8】函数式接口(一)—— Functional Interface简介
什么是函数式接口(Functional Interface) 其实之前在讲Lambda表达式的时候提到过,所谓的函数式接口,当然首先是一个接口,然后就是在这个接口里面只能有一个抽象方法. 这种类型的接 ...
- list.jsp页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %><%@tag ...
- maven依赖对zookeeper的版本冲突问题
我用的是springcloudAlibaba+zookeeper zookeeper下载后 1,修改配置文件,conf目录下的zoo_sample.cfg修改为zoo.cfg. 2,打开zoo.cfg ...
- 分布式事务之TCC事务模型
一.引言 在上篇文章<老生常谈--利用消息队列处理分布式事务>一文中留了一个坑,今天来填坑.如下图所示 如果服务A和服务B之间是同步调用,比如服务C需要按流程调服务A和服务B,服务A和服务 ...
- Jenkins监控
目录 一.Monitoring插件 二.Prometheus监控 一.Monitoring插件 Monitoring插件(monitoring)使用JavaMelody,对Jenkins进行监控.插件 ...
- 学习整理--vue篇(1)
vue学习 vue指令 模板指令: v-model:绑定data数据实现数据双向绑定 v-html:绑定模板内容,可书写标签 v-text:绑定数据实现单向绑定 可缩写为{{}} 支持逻辑运算 可结合 ...
- how2heap libc2.31学习
今天是四月十九,想在五月份之前把how2heap中的高版本(2.31)的例子过一遍.所以这个系列目前还是在更新中.如果比较简单就几句话带过了,遇到难一点的会写的详细一点. fastbin_dup 源代 ...