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

50次阅读

共计 6695 个字符,预计需要花费 17 分钟才能阅读完成。

零碎调用

在咱们日常 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_DETAC H 从该过程断开并完结跟踪。在调用该指令后,被跟踪过程会发送信号给跟踪者过程,跟踪者过程须要应用 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 章零碎调用

正文完
 0