实现一个 Golang 调试器(第二部分)

jettyhan · 2018-04-14 10:19:15 · 2408 次点击 · 预计阅读时间 6 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-04-14 10:19:15 的文章,其中的信息可能已经有所发展或是发生改变。

第一部分里,我们首先介绍了开发环境并且实现了一个简单的调试器(tracer),它可以使子进程(tracee)在最开始处停止运行,然后继续执行,并显示它的标准输出。现在是扩展这个程序的时候了。

通常,调试器允许单步执行被调试的代码,这个可以通过 ptrace 的 PTRACE_SINGLESTEP 命令实现,它告诉 tracee 执行完一条指令后停止运行。

package main

import (
    "flag"
    "log"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    flag.Parse()
    input := flag.Arg(0)
    cmd := exec.Command(input)
    cmd.Args = []string{input}
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    }
    err = cmd.Wait()
    log.Printf("State: %v\n", err)
    wpid := cmd.Process.Pid
    pgid, err := syscall.Getpgid(cmd.Process.Pid)
    if err != nil {
        log.Panic(err)
    }
    err = syscall.PtraceSetOptions(cmd.Process.Pid, syscall.PTRACE_O_TRACECLONE)
    if err != nil {
        log.Fatal(err)
    }
    err = syscall.PtraceSingleStep(wpid)
    if err != nil {
        log.Fatal(err)
    }
    steps := 1

    for {
        var ws syscall.WaitStatus
        wpid, err = syscall.Wait4(-1*pgid, &ws, syscall.WALL, nil)
        if wpid == -1 {
            log.Fatal(err)
        }
        if wpid == cmd.Process.Pid && ws.Exited() {
            break
        }
        if !ws.Exited() {
            err := syscall.PtraceSingleStep(wpid)
            if err != nil {
                log.Fatal(err)
            }
            steps += 1
        }
    }
    log.Printf("Steps: %d\n", steps)
}

构建并运行这个段代码,输出应该像下面这样(每次调用显示的步数可能不一样)

> go install -gcflags="-N -l" github.com/mlowicki/hello
> go install github.com/mlowicki/debugger
> debugger /go/bin/hello
2017/06/09 19:54:42 State: stop signal: trace/breakpoint trap
hello world
2017/06/09 19:54:49 Steps: 297583

程序的前半部分和上一篇文章里的一样,新加的地方是对 syscall.PtraceSingleStep 的调用,它使被调试的程序(在这里是 hello )执行完一条指令后停止。

PTRACE_O_TRACECLONE 选项也被设定了

PTRACE_O_TRACECLONE (since Linux 2.5.46) Stop the tracee at the next clone(2) and automatically start tracing the newly cloned process...

(http://man7.org/linux/man-pages/man2/ptrace.2.html)

由于我们的调试器知道新线程什么时间开始并且可以跳过它,所以最后显示的步数是通过所有进程执行的指令总数

被执行的指令数量可能相当多,但是里面包含了 Go 运行时中其它一些初始化代码(有 C 语言开始经验的人应该了解 libc 的初始化过程)。我们可以写一个非常简单的程序来验证我们的调试器工作是正常的。让我们创建一个汇编文件 src/github.com/mlowicki/hello/hello.asm:

section .data
    msg db "hello, world!", 0xA
    len equ $ — msg
section .text
    global _start
_start:
    mov rax, 1 ; write syscall (https://linux.die.net/man/2/write)
    mov rdi, 1 ; stdout
    mov rsi, msg
    mov rdx, len
    ; Passing parameters to `syscall` instruction described in
    ; https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#syscall
    syscall
    mov rax, 60 ; exit syscall (https://linux.die.net/man/2/exit)
    mov rdi, 0 ; exit code
    syscall

在容器中构建我们的 "hello world" 程序,看一下执行了多少条指令

> pwd
/go
> apt-get install nasm
> nasm -f elf64 -o hello.o src/github.com/mlowicki/hello/hello.asm && ld -o hello hello.o
> ./hello
hello, world!
> debugger ./hello
2017/06/17 17:58:43 State: stop signal: trace/breakpoint trap
hello, world!
2017/06/17 17:58:43 Steps: 8

输出结果很好,正好等于 hello.asm 中指令的数量

到目前为止,我们已经知道怎样让程序在一开始停止,如何一步一步的执行代码并查看 进程/线程 的状态,现在是在需要的地方设置断点,监视像变量值这样的进程状态的时候了。

让我们从一个简单的例子开始,hello.go 中有一个 main 函数

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

怎样在这个函数的一开始设置断点呢?我们的程序经过编译链接后,最终生成的是一系列机器指令。怎样在只包含了一些二进制代码(只有 CPU 能理解的格式)的源文件里表示我们要设置一个断点呢?

lineTable

Golang 内置有一些功能,可以访问编译生成的二进制文件中的调试信息。 维护 指令计数器 ( PC ) 和程序代码行的映射关系的结构叫做行表,让我们通过一个例子来看一下

package main

import (
    "debug/elf"
    "debug/gosym"
    "flag"
    "log"
)

func main() {
    flag.Parse()
    path := flag.Arg(0)
    exe, err := elf.Open(path)
    if err != nil {
        log.Fatal(err)
    }
    var pclndat []byte
    if sec := exe.Section(".gopclntab"); sec != nil {
        pclndat, err = sec.Data()
        if err != nil {
            log.Fatalf("Cannot read .gopclntab section: %v", err)
        }
    }
    sec := exe.Section(".gosymtab")
    symTabRaw, err := sec.Data()
    pcln := gosym.NewLineTable(pclndat, exe.Section(".text").Addr)
    symTab, err := gosym.NewTable(symTabRaw, pcln)
    if err != nil {
        log.Fatal("Cannot create symbol table: %v", err)
    }
    sym := symTab.LookupFunc("main.main")
    filename, lineno, _ := symTab.PCToLine(sym.Entry)
    log.Printf("filename: %v\n", filename)
    log.Printf("lineno: %v\n", lineno)
}

如果传递给上面程序的文件中包含以下代码

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

那么输出应该是这样的

> go install github.com/mlowicki/linetable
> go install — gcflags=”-N -l” github.com/mlowicki/hello
> linetable /go/bin/hello
2017/06/30 18:47:38 filename: /go/src/github.com/mlowicki/hello/hello.go
2017/06/30 18:47:38 lineno: 5

ELF 是 Executable and Linkable Format 的缩写,是一种可执行文件的格式

> apt-get install file
> file /go/bin/hello
/go/bin/hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

ELF 中包含许多段,我们用到了其中三个:.text、.gopclntab 和 .gosymtab。第一个包含了机器指令,第二个实现了指令计数器到源码行的映射,最后一个是一个符号表

在接下来的文章中,我们会学习怎样用「行表」在一个需要的地方设置断点以及怎样监视程序状态。


via: https://medium.com/golangspec/making-debugger-in-golang-part-ii-d2b8eb2f19e0

作者:Michał Łowicki  译者:jettyhan  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2408 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
1 回复  |  直到 2018-04-14 11:09:13
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传