這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在[第一部分](https://studygolang.com/articles/12553)裡,我們首先介紹了開發環境並且實現了一個簡單的調試器(tracer),它可以使子進程(tracee)在最開始處停止運行,然後繼續執行,並顯示它的標準輸出。現在是擴充這個程式的時候了。通常,調試器允許逐步執行被調試的代碼,這個可以通過 [ptrace](http://man7.org/linux/man-pages/man2/ptrace.2.html) 的 PTRACE_SINGLESTEP 命令實現,它告訴 tracee 執行完一條指令後停止運行。```gopackage mainimport ("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.Stdoutcmd.Stderr = os.Stderrcmd.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.Pidpgid, 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 := 1for {var ws syscall.WaitStatuswpid, 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/hello2017/06/09 19:54:42 State: stop signal: trace/breakpoint traphello world2017/06/09 19:54:49 Steps: 297583```程式的前半部分和上一篇文章裡的一樣,新加的地方是對 [ syscall.PtraceSingleStep](https://golang.org/pkg/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](https://www.gnu.org/software/libc/) 的初始化過程)。我們可以寫一個非常簡單的程式來驗證我們的調試器工作是正常的。讓我們建立一個彙編檔案 src/github.com/mlowicki/hello/hello.asm:```asmsection .datamsg db "hello, world!", 0xAlen equ $ — msgsection .textglobal _start_start:mov rax, 1 ; write syscall (https://linux.die.net/man/2/write)mov rdi, 1 ; stdoutmov rsi, msgmov rdx, len; Passing parameters to `syscall` instruction described in; https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux#syscallsyscallmov rax, 60 ; exit syscall (https://linux.die.net/man/2/exit)mov rdi, 0 ; exit codesyscall```在容器中構建我們的 "hello world" 程式,看一下執行了多少條指令```shell> pwd/go> apt-get install nasm> nasm -f elf64 -o hello.o src/github.com/mlowicki/hello/hello.asm && ld -o hello hello.o> ./hellohello, world!> debugger ./hello2017/06/17 17:58:43 State: stop signal: trace/breakpoint traphello, world!2017/06/17 17:58:43 Steps: 8```輸出結果很好,正好等於 hello.asm 中指令的數量到目前為止,我們已經知道怎樣讓程式在一開始停止,如何一步一步的執行代碼並查看 進程/線程 的狀態,現在是在需要的地方設定斷點,監視像變數值這樣的進程狀態的時候了。讓我們從一個簡單的例子開始,hello.go 中有一個 main 函數```gopackage mainimport "fmt"func main() {fmt.Println("hello world")}```怎樣在這個函數的一開始設定斷點呢?我們的程式經過編譯連結後,最終產生的是一系列機器指令。怎樣在只包含了一些二進位代碼(只有 CPU 能理解的格式)的源檔案裡表示我們要設定一個斷點呢?## lineTableGolang 內建有一些功能,可以訪問編譯產生的二進位檔案中的調試資訊。 維護 指令計數器 ( [PC](https://en.wikipedia.org/wiki/Program_counter) ) 和程式碼行的映射關係的結構叫做[行表](https://golang.org/pkg/debug/gosym/#LineTable),讓我們通過一個例子來看一下```gopackage mainimport ("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 []byteif 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)}```如果傳遞給上面程式的檔案中包含以下代碼```gopackage mainimport "fmt"func main() {fmt.Println("hello world")}```那麼輸出應該是這樣的```shell> go install github.com/mlowicki/linetable> go install — gcflags=”-N -l” github.com/mlowicki/hello> linetable /go/bin/hello2017/06/30 18:47:38 filename: /go/src/github.com/mlowicki/hello/hello.go2017/06/30 18:47:38 lineno: 5```ELF 是 [Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) 的縮寫,是一種可執行檔的格式```shell> 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。第一個包含了機器指令,第二個實現了指令計數器到源碼行的映射,最後一個是一個[符號表](https://en.wikipedia.org/wiki/Symbol_table)在接下來的文章中,我們會學習怎樣用「行表」在一個需要的地方設定斷點以及怎樣監視程式狀態。
via: https://medium.com/golangspec/making-debugger-in-golang-part-ii-d2b8eb2f19e0
作者:Michał Łowicki 譯者:jettyhan 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
316 次點擊 ∙ 1 贊