跟踪系统调用

本节将介绍一种非常先进的技术,它使用syscall包,并允许你监视在Go程序中执行的系统调用。

本节不包含在我的《Go系统编程》书籍中,Packt出版社,2017年

Go程序名为traceSyscall.go,并分为五部分。traceSyscall.go第一部分代码如下:

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "strings"
  8. "syscall"
  9. )
  10. var maxSyscalls = 0
  11. const SYSCALLFILE = "SYSCALLS"

你将很快学习SYSCALLFILE变量的作用。

traceSyscall.go第二部分代码如下:

  1. func main() {
  2. var SYSTEMCALLS []string
  3. f, err := os.Open(SYSCALLFILE)
  4. defer f.Close()
  5. if err != nil {
  6. fmt.Println(err)
  7. return
  8. }
  9. scanner := bufio.NewScanner(f)
  10. for scanner.Scan() {
  11. line := scanner.Text()
  12. line = strings.Replace(line, " ", "", -1)
  13. line = strings.Replace(line, "SYS_", "", -1)
  14. temp := strings.ToLower(strings.Split(line, "=")[0])
  15. SYSTEMCALLS = append(SYSTEMCALLS, temp)
  16. maxSyscalls++
  17. }

请注意,SYSCALLS文件的信息取自syscall包的文档,它将每个系统调用与一个数字相关联,该数字是系统调用的内部Go表示形式。该文件主要用于打印被跟踪程序所使用的系统调用的名称。

SYSCALLS文件的格式如下:

  1. SYS_READ = 0
  2. SYS_WRITE = 1
  3. SYS_OPEN = 2
  4. SYS_CLOSE = 3
  5. SYS_STAT = 4

在读取文本文件后,程序创建名为SYSTEMCALLS的切片来存储信息。

traceSyscall.go第三部分代码如下:

  1. COUNTER := make([]int, maxSyscalls)
  2. var regs syscall.PtraceRegs
  3. cmd := exec.Command(os.Args[1], os.Args[2:]...)
  4. cmd.Stdin = os.Stdin
  5. cmd.Stdout = os.Stdout
  6. cmd.Stderr = os.Stderr
  7. cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
  8. err = cmd.Start()
  9. err = cmd.Wait()
  10. if err != nil {
  11. fmt.Println("Wait:", err)
  12. }
  13. pid := cmd.Process.Pid
  14. fmt.Println("Process ID:", pid)

COUNTER切片存储在被跟踪的程序中每个系统调用的次数。

traceSyscall.go的第四部分代码如下:

  1. before := true
  2. forCount := 0
  3. for {
  4. if before {
  5. err := syscall.PtraceGetRegs(pid, &regs)
  6. if err != nil {
  7. break
  8. }
  9. if regs.Orig_rax > uint64(maxSyscalls) {
  10. fmt.Println("Unknown:", regs.Orig_rax)
  11. return
  12. }
  13. COUNTER[regs.Orig_rax]++
  14. forCount++
  15. }
  16. err = syscall.PtraceSyscall(pid, 0)
  17. if err != nil {
  18. fmt.Println("PtraceSyscall:", err)
  19. return
  20. }
  21. _, err = syscall.Wait4(pid, nil, 0, nil)
  22. if err != nil {
  23. fmt.Println("Wait4:", err)
  24. return
  25. }
  26. before = !before
  27. }

syscall.PtraceSyscall()函数的作用是:告诉Go继续执行正在被跟踪的程序,但是当程序进入或退出系统调用时停止执行,这正是我们想要的!由于每个系统调用在被调用之前和完成其工作之后都会被跟踪,因此我们使用before变量来计算每个系统调用仅一次。

traceSyscall.go的最后一部分代码如下:

  1. for i, x := range COUNTER {
  2. if x != 0 {
  3. fmt.Println(SYSTEMCALLS[i], "->", x)
  4. }
  5. }
  6. fmt.Println("Total System Calls:", forCount)
  7. }

在这一部分中,我们打印切片COUNTER的内容。切片SYSTEMCALLS用于在知道系统调用的Go数字表示时,来查找系统调用的名称。

macOS High Sierra机器上执行traceSyscall.go会创建如下的输出:

  1. $ go run traceSyscall.go
  2. # command-line-arguments
  3. ./traceSyscall.go:36:11: undefined: syscall.PtraceRegs
  4. ./traceSyscall.go:57:11: undefined: syscall.PtraceGetRegs
  5. ./traceSyscall.go:70:9: undefined: syscall.PtraceSyscall

同样,traceSyscall.go程序不能在macOSMac OS X上运行。

Debian Linux机器上执行程序会创建如下的输出:

  1. $ go run traceSyscall.go ls /tmp/
  2. Wait: stop signal: trace/breakpoint trap
  3. Process ID: 5657
  4. go-build084836422 test.go upload_progress_cache
  5. read -> 11
  6. write -> 1
  7. open -> 37
  8. close -> 27
  9. stat -> 1
  10. fstat -> 25
  11. mmap -> 39
  12. mprotect -> 16
  13. munmap -> 4
  14. brk -> 3
  15. rt_sigaction -> 2
  16. rt_sigprocmask -> 1
  17. ioctl -> 2
  18. access -> 9
  19. execve -> 1
  20. getdents -> 2
  21. getrlimit -> 1
  22. statfs -> 2
  23. arch_prctl -> 1
  24. futex -> 1
  25. set_tid_address -> 1
  26. openat -> 1
  27. set_robust_list -> 1
  28. Total System Calls: 189

在程序结束时,traceSyscall.go打印程序中调用每个系统调用的次数!traceSyscall.go的正确性通过strace -c程序的输出进行验证。

  1. $ strace -c ls /tmp
  2. test.go upload_progress_cache
  3. % time seconds usecs/call calls errors syscall
  4. ------ ----------- ----------- --------- --------- ----------------
  5. 0.00 0.000000 0 11 read
  6. 0.00 0.000000 0 1 write
  7. 0.00 0.000000 0 37 13 open
  8. 0.00 0.000000 0 27 close
  9. 0.00 0.000000 0 1 stat
  10. 0.00 0.000000 0 25 fstat
  11. 0.00 0.000000 0 39 mmap
  12. 0.00 0.000000 0 16 mprotect
  13. 0.00 0.000000 0 4 munmap
  14. 0.00 0.000000 0 3 brk
  15. 0.00 0.000000 0 2 rt_sigaction
  16. 0.00 0.000000 0 1 rt_sigprocmask
  17. 0.00 0.000000 0 2 ioctl
  18. 0.00 0.000000 0 9 9 access
  19. 0.00 0.000000 0 1 execve
  20. 0.00 0.000000 0 2 getdents
  21. 0.00 0.000000 0 1 getrlimit
  22. 0.00 0.000000 0 2 2 statfs
  23. 0.00 0.000000 0 1 arch_prctl
  24. 0.00 0.000000 0 1 futex
  25. 0.00 0.000000 0 1 set_tid_address
  26. 0.00 0.000000 0 1 openat
  27. 0.00 0.000000 0 1 set_robust_list
  28. ------ ----------- ----------- --------- --------- -------------
  29. 100.00 0.000000 189 24 total