关于linux:用Go看到进程中的系统调用

零碎调用

在咱们日常coding时候,个别写的都是用户层的代码,内核对于咱们而言如同是通明的,并未关注过。然而程序却无时无刻在与内核打交道,比方当读取文件时候的read,又或者在写文件的Write,都会通过内核。用户程序不会间接与磁盘等硬件打交道,所以不能间接对文件进行操作,所以须要内核这层”垫片”,用户程序既然要拜访内核,就免不了要执行零碎调用。

当要执行零碎调用时候,CPU会切换到内核态,执行零碎调用函数。

因为内核实现了很多的零碎调用函数,所以内核须要为每个函数提供一个标识,代表要调用的内核函数,这个零碎调用号在不同的内核架构也不同。(异样以及中断同样会使CPU切换到内核态,不开展形容)

通常一个零碎调用的执行流如下

  • 用户程序调用c库或者间接通过本身的汇编指令进行零碎调用,须要传递的变量以及零碎调用编号保留在cpu寄存器中
  • 过程进入内核态通过寄存器保留的零碎调用编号识别系统函数并执行零碎调用
  • 零碎调用完结,后果以及返回值以及参数保留在寄存器,用户程序从中获取后果

在晚期,零碎调用是通过软中断触发的,如32位的x86,零碎调用的中断号是128,所以会通过INT 0x80指令触发软中断进入零碎调用从而进入内核态,读取寄存器存储的值并在零碎调用表中找到对应的零碎调用并执行,因为应用软中断的模式触发零碎调用开销较大,所以慢慢退出视线,取而代之的是应用了汇编指令SYSENTER或SYSCALL的模式触发零碎调用,相比软中断触发的形式缩小了查问中断向量表等系列操作,晋升了性能。

咱们能够通过strace命令来取得一个过程的零碎调用,罕用用法如下

$ strace -p <pid> #查看某个过程的零碎调用
$ strace <commond> #查看某条commond指令或过程的零碎调用

如写一个很简略的打印函数调试,(后续会应用该程序作为被追踪程序)

#include <unistd.h>
#include <stdio.h>
int main(){
   for(;;){
       printf("pid=%dn", getpid());
       sleep(2);
  }
   return 0;
}
$ gcc -o print print.c

通过strace查看这个过程,能够看到零碎调用状况

centos@xxxxxx:/app/gowork/stramgrpc/c$ strace ./print 
execve("./print", ["./print"], [/* 51 vars */]) = 0
.......
getpid() = 23419
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
brk(NULL) = 0x55e3343e2000
brk(0x55e334403000) = 0x55e334403000
write(1, "pid=23419n", 10pid=23419
) = 10
nanosleep({tv_sec=2, tv_nsec=0}, 0x7ffd2a6d37f0) = 0
getpid() = 23419
write(1, "pid=23419n", 10pid=23419
) = 10
nanosleep({tv_sec=2, tv_nsec=0}, ^Cstrace: Process 23419 detached
<detached ...>

strace命令是c语言实现的,基于Ptrace零碎调用。因为服务器体系不同,零碎调用机制也会随之变动,所以在strace源码外面有大量的预处理器代码,浏览起来非常吃力不讨好????????????????

既然Golang封装了零碎调用的包,能够间接通过汇编执行零碎调用,也能够用Golang实现一个简略的ptrace工具来监控过程的零碎调用,咱们在这里次要专一x86_64的Linux syscall。

ptrace

要实现一个ptrace工具,首先对ptrace做一些理解,看一下c规范库的定义规定。

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

ptrace在须要传入四个参数:

  • pid用于传入指标过程,也就是要跟性过程的pid;
  • addrdata用于传入内存地址和附加地址,通常会在零碎调用完结后读取传入的参数获取零碎调用后果,会因操作的不同而不同。
  • request用于抉择一个符号标记,内核会依据这个标记决定要选用那个内核函数来执行,接下来介绍一下重点要应用的几个符号标记。

request的可选值

  • PTRACE_ATTACH收回一个申请,连贯到一个过程并开始跟踪,相同,PTRACE_DETACH从该过程断开并完结跟踪。在调用该指令后,被跟踪过程会发送信号给跟踪者过程,跟踪者过程须要应用waitpid获取该信号,并进行后续的零碎调用跟踪。
  • PTRACE_SYSCALL收回零碎调用追踪的指令,当应用了该选项时候,被追踪的过程就会在进入零碎调用之前或者完结后停下来,这时候追踪者过程能够应用waitpid零碎调用时候收到被追踪者发来的告诉,从而剖析此时的地址空间以及零碎调用相干等信息;
  • PTRACE_GETREGSPTRACE_SETREGS用来设置和读取CPU寄存器,在x86_64的Linux上,零碎调用的编号存储在orig_rax寄存器,其余参数是在rdi、rsi、rdx等寄存器,在返回时,返回值存储在rax寄存器;
  • PTRACE_TRACEME:此过程容许被其父过程跟踪(用于strace+命令模式)。
  • ……其余的应用形式还有很多,感兴趣同学能够读下《深刻了解Linux内核与架构 13.3.3追踪零碎调用》

go的实现

go中提供了syscall包能够间接调用汇编代码进行零碎调用,本案例基于go1.13.5的syscall包。

实现过程的跟踪,就须要两个过程,一个是被跟踪者(tracee),一个是跟踪者(tracer),用于打印出tracee过程产生的零碎调用。咱们用go实现一个tracer,而tracee应用下面的c代码。

思路
  • 开启一个过程作为被跟踪的过程tracee。

tracer的实现原理

  • 首先应用PTRACE_ATTACH去跟踪tracee过程,紧接着应用wait零碎调用去获取被跟踪者收回的信号。此时tracer过程和tracee过程曾经在内核建立联系。
//在go中对应的库函数如下
func PtraceAttach(pid int) (err error) {...}
func Wait4(pid int, wstatus *WaitStatus, options int, rusage *Rusage) (wpid int, err error) {...}
  • 接下来tracer过程通过一个有限循环读取tracee的零碎调用

读取的过程

  • 首先通过PTRACE_SYSCALL来期待被跟踪过程进入零碎调用,通过wait期待被跟踪过程进入冀望状态,此时,被跟踪过程还未陷入零碎调用,相当于在零碎调用的入口暂停住了。
//在go中对应的库函数如下
func PtraceSyscall(pid int, signal int) (err error) {...}
func Wait4(pid int, wstatus *WaitStatus, options int, rusage *Rusage) (wpid int, err error) {...}
  • 接下来通过PTRACE_GETREGS获取寄存器参数,包含零碎调用编号以及其余参数等。
func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) {...}
  • 接下来应用另一个PTRACE_SYSCALL,以及wait获取零碎调用期待零碎调用返回,此时tracee过程陷入内核态执行零碎调用,零碎调用返回后tracer过程也就能够获取返回后果了;
  • 应用PTRACE_GETREGS通过寄存器参数获取返回的后果
  • 进入下一个循环
  • 出现异常,应用PTRACE_DETACH断开跟踪状态
实现
type syscallTask struct {
  ID uint64
  Name string
}
//x86_64上零碎调用编号对应的零碎调用名称
var sTask = []syscallTask{
  {0, "read"},
  {1, "write"},
  {2, "open"},
  {3, "close"},
  {4, "stat"},
  ......//过多省略
}
func main() {
  //寄存器状态数据
  var regs syscall.PtraceRegs
  //wait的期待状态
  var wsstatus syscall.WaitStatus
  //被跟踪过程pid
  pid := 13070
  fmt.Println(pid)
  var err error
    //对PTRACE_ATTACH的封装,应用attach连贯并跟踪过程
  err = syscall.PtraceAttach(pid)
  if err != nil{
    fmt.Println(err)
    return
  }
  syscall.Wait4(pid,&wsstatus,0,nil)
    //如果异样退出,则断开分割
  defer func() {
        //对PTRACE_DETACH的封装,断开与跟踪者的连贯
    err = syscall.PtraceDetach(pid)
    if err != nil{
      fmt.Println("PtraceDetach err :",err)
      return
    }
    syscall.Wait4(pid,&wsstatus,0,nil)
  }()
  //循环获取
  for {
    fmt.Println("")
    //期待tracee进入零碎调用
    syscall.PtraceSyscall(pid,0)
        //应用wait零碎调用,并传入期待的状态指针
    _, err := syscall.Wait4(pid, &wsstatus, 0, nil)
    if err != nil{
      fmt.Println("line 501",err)
      return
    }
        //如果tracee退出,打印过程的退出码
    if wsstatus.Exited(){
      fmt.Println("------exit status",wsstatus.ExitStatus())
            return
    }
    //依据wsstatus判断tracee是否收到的是中断信号,如键盘的ctrl+C诸如此类等
    //如果有,则传递该信号到tracee
    if wsstatus.StopSignal().String() == "interrupt"{
      syscall.PtraceSyscall(pid, int(wsstatus.StopSignal()))
      fmt.Println("send interrupt sig to pid ")
      //打印tracee退出码
      fmt.Println("------exit status",wsstatus.ExitStatus())
      return
    }
    //对PTRACE_GETREGS的封装,获取寄存器的数据保留到regs中
    err = syscall.PtraceGetRegs(pid, &regs)
    if err != nil{
      fmt.Println("PtraceGetRegs err :",err.Error())
      return
    }
    //打印零碎调用名称
    fmt.Println("in syscall :",sTask[regs.Orig_rax].Name)
    //第二组PTRACE_SYSCALL与waitpid,期待tracee零碎调用返回
    //用于获取零碎调用返回后的参数
    syscall.PtraceSyscall(pid, 0)
    _ ,err = syscall.Wait4(pid,&wsstatus,0,nil)
    if err != nil{
      fmt.Println("line 518",err)
      return
    }
    //如果tracee退出,打印过程的退出码
    if wsstatus.Exited(){
      fmt.Println("------exit status",wsstatus.ExitStatus())
            return
    }
    //同上,判断过程是否被信号打断
    if wsstatus.StopSignal().String() == "interrupt"{
      syscall.PtraceSyscall(pid, int(wsstatus.StopSignal()))
      fmt.Println("send interrupt sig to pid ")
      fmt.Println("------exit status",wsstatus.ExitStatus())
    }
    //获取返回后的寄存器状态
    err = syscall.PtraceGetRegs(pid, &regs)
    if err != nil{
      fmt.Println("PtraceGetRegs err :",err.Error())
      return
    }
        //打印寄存器中存储的返回值参数
    fmt.Println("syscall return:" ,regs.Rax)
  }

应用该用例测试上述的demo

$ ./print
$ go build -o gostrace main.go
$ sudo ./gostrace

输入后果:

centos@XXXXXXX:/app/gowork/gostraces# sudo ./gostrace 
20533
in syscall : restart_syscall
syscall return: 0
in syscall : getpid
syscall return: 20533
in syscall : write
syscall return: 10
in syscall : nanosleep
syscall return: 0
in syscall : getpid
syscall return: 20533 #在此处ctrl+c中断了上述print过程
send interrupt sig to pid 
------exit status -1
PtraceDetach err : no such process

比照下通过strace获取的零碎调用状况

guozhaocoder@guozhaocoder-PC:/app/GoWork/stramgrpc$ sudo strace -p 27579
strace: Process 27579 attached
restart_syscall(<... resuming interrupted nanosleep ...>) = 0
getpid()                                = 27579
write(1, "pid=27579\n", 10)             = 10
nanosleep({tv_sec=2, tv_nsec=0}, 0x7ffeda284d00) = 0
getpid()                                = 27579
write(1, "pid=27579\n", 10)             = 10
nanosleep({tv_sec=2, tv_nsec=0}, {tv_sec=1, tv_nsec=173442353}) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
+++ killed by SIGINT +++

能够看到最简版本strace预期性能曾经实现了,相比于strace少了一个零碎调用参数,传参的性能就须要针对具体的零碎调用读取寄存器中的数据,有趣味的同学能够本人思考实现下。

参考文章

  • 《深刻了解Linux内核与架构》第13章零碎调用
  • 《深刻了解Linux内核》第10章零碎调用

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理