Golang 與系統調用

【導讀】本文介紹了使用 golang 和系統調用實現一個 strace 的操作。

GopherCon2017 中的一個視頻講解了如何用 golang 實現一個簡單的 strace,本文是基於此演講整理而來。

什麼是系統調用

先看下 wiki 的定義:

In computing, a system call is the programmatic way in which a computer program requests a service from the kernel of the operating system it is executed on. This may include hardware-related services (for example, accessing a hard disk drive), creation and execution of new processes, and communication with integral kernel services such as process scheduling. System calls provide an essential interface between a process and the operating system.

系統調用是程序向操作系統內核請求服務的過程,通常包含硬件相關的服務 (例如訪問硬盤), 創建新進程等。系統調用提供了一個進程和操作系統之間的接口。

syscall 無處不在

只要在 os 上寫程序,就無法避免和 syscall 打交道。舉個最常用的例子, fmt.Println("hello world"), 這裏就用到了系統調用 write, 我們翻一下源碼。

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    // writer 是 stdout
    n, err = w.Write(p.buf)
    p.free()
    return
}

Stdout = NewFile(uintptr(syscall.Stdout)"/dev/stdout")

func (f *File) write([]byte) (n int, err error) {
    if len(b) == 0 {
        return 0, nil
    }
    // 實際的write方法,就是調用syscall.Write()
    return fixCount(syscall.Write(f.fd, b))
}

Zero-Copy

再舉一個例子,我們常聽到的 zero-copy,我們看看 zero-copy 是用來解決什麼問題的。

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

借用一張圖來說明問題

  1. 第一步,read()導致上下文切換 (context switch),從用戶模式進入內核模式,DMA(Direct memory access) engine 從磁盤中讀取內容,存入內核地址 buffer。

  2. 第二步,數據從內核 buffer 拷貝入用戶 buffer,read()返回,上下文切換回用戶態。

  3. 第三步,write()上下文切換,把 buffer 拷貝到內核地址 buffer。

  4. 第四步,write()返回,第四次上下文切換,DMA engine 把數據從內核 buffer 傳給協議引擎,一般是進入隊列,等待傳輸。

我們看到,這裏數據在用戶空間和內核空間來回拷貝,其實是不必要的。

解決的辦法有: mmapsendfile, 具體可以參考這篇文章

到這裏我們應該對系統調用有了一定的認識了。

Strace

strace 是用於查看進程系統調用的工具, 一般使用方法如下

strace <bin>
strace -p <pid>
// 用於統計各個系統調用的次數
strace -c <bin>

// 例如
strace -c echo hello
hello
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0         3           open
  0.00    0.000000           0         5           close
  0.00    0.000000           0         4           fstat
  0.00    0.000000           0         7           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    34         3 total

stace 的實現原理是系統調用 ptrace, 我們來看下 ptrace 是什麼。

Ptrace

man page 描述如下:

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 debuggingand system call tracing.

簡單來說有三大能力:

接口

int ptrace(int request, pid_t pid, caddr_t addr, int data);

request包含:
PTRACE_ATTACH
PTRACE_SYSCALL
PTRACE_PEEKTEXT, PTRACE_PEEKDATA
等

tracer 使用 PTRACE_ATTACH 命令,指定需要追蹤的 PID。緊接着調用 PTRACE_SYSCALL
tracee 會一直運行,直到遇到系統調用,內核會停止執行。此時,tracer 會收到 SIGTRAP 信號,tracer 就可以打印內存和寄存器中的信息了。

接着,tracer 繼續調用 PTRACE_SYSCALL, tracee 繼續執行,直到 tracee 退出當前的系統調用。
需要注意的是,這裏在進入 syscall 和退出 syscall 時,tracer 都會察覺。

myStrace

瞭解以上內容後,presenter 現場實現了一個 go 版本的 strace, 需要在 linux amd64 環境編譯。
github

// strace.go

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    var err error
    var regs syscall.PtraceRegs
    var ss syscallCounter
    ss = ss.init()

    fmt.Println("Run: ", os.Args[1:])

    cmd := exec.Command(os.Args[1], os.Args[2:]...)
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout
    cmd.Stdin = os.Stdin
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Ptrace: true,
    }

    cmd.Start()
    err = cmd.Wait()
    if err != nil {
        fmt.Printf("Wait err %v \n", err)
    }

    pid := cmd.Process.Pid
    exit := true

    for {
        // 記得 PTRACE_SYSCALL 會在進入和退出syscall時使 tracee 暫停,所以這裏用一個變量控制,RAX的內容只打印一遍
        if exit {
            err = syscall.PtraceGetRegs(pid, ®s)
            if err != nil {
                break
            }
            //fmt.Printf("%#v \n",regs)
            name := ss.getName(regs.Orig_rax)
            fmt.Printf("name: %s, id: %d \n", name, regs.Orig_rax)
            ss.inc(regs.Orig_rax)
        }
        // 上面Ptrace有提到的一個request命令
        err = syscall.PtraceSyscall(pid, 0)
        if err != nil {
            panic(err)
        }
        // 猜測是等待進程進入下一個stop,這裏如果不等待,那麼會打印大量重複的調用函數名
        _, err = syscall.Wait4(pid, nil, 0, nil)
        if err != nil {
            panic(err)
        }

        exit = !exit
    }

    ss.print()
}

// 用於統計信息的 counter, syscallcounter.go

package main

import (
    "fmt"
    "os"
    "text/tabwriter"

    "github.com/seccomp/libseccomp-golang"
)

type syscallCounter []int

const maxSyscalls = 303

func (s syscallCounter) init() syscallCounter {
    s = make(syscallCounter, maxSyscalls)
    return s
}

func (s syscallCounter) inc(syscallID uint64) error {
    if syscallID > maxSyscalls {
        return fmt.Errorf("invalid syscall ID (%x)", syscallID)
    }

    s[syscallID]++
    return nil
}

func (s syscallCounter) print() {
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 8, ' ', tabwriter.AlignRight|tabwriter.Debug)
    for k, v := range s {
        if v > 0 {
            name, _ := seccomp.ScmpSyscall(k).GetName()
            fmt.Fprintf(w, "%d\t%s\n", v, name)
        }
    }
    w.Flush()
}

func (s syscallCounter) getName(syscallID uint64) string {
    name, _ := seccomp.ScmpSyscall(syscallID).GetName()
    return name
}

最後結果:

Run:  [echo hello]
Wait err stop signal: trace/breakpoint trap
name: execve, id: 59
name: brk, id: 12
name: access, id: 21
name: mmap, id: 9
name: access, id: 21
name: open, id: 2
name: fstat, id: 5
name: mmap, id: 9
name: close, id: 3
name: access, id: 21
name: open, id: 2
name: read, id: 0
name: fstat, id: 5
name: mmap, id: 9
name: mprotect, id: 10
name: mmap, id: 9
name: mmap, id: 9
name: close, id: 3
name: mmap, id: 9
name: arch_prctl, id: 158
name: mprotect, id: 10
name: mprotect, id: 10
name: mprotect, id: 10
name: munmap, id: 11
name: brk, id: 12
name: brk, id: 12
name: open, id: 2
name: fstat, id: 5
name: mmap, id: 9
name: close, id: 3
name: fstat, id: 5
hello
name: write, id: 1
name: close, id: 3
name: close, id: 3
        1|read
        1|write
        3|open
        5|close
        4|fstat
        7|mmap
        4|mprotect
        1|munmap
        3|brk
        3|access
        1|execve
        1|arch_prctl

對比一下結果,可以發現和 strace 是一樣的。

轉自:

segmentfault.com/a/1190000010630859

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GuOjKHMgB0sZoNmyc_W-6Q