這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。寫這個系列的目的不是為了列出 Golang 程式設計語言的調試器的所有特性。如果你想看這些內容,可以看下 [Delve](https://github.com/derekparker/delve)。在這篇文章裡我們試著去探索下調試器通常是怎樣工作的,怎麼在 Linux 上完成一個基本的調試,Linux 上比較關心 Golang 的功能,比如 [goroutine](https://golang.org/ref/spec#Go_statements) 。建立調試器沒那麼簡單。就這一個話題我們單獨寫一篇文章也講不完。相反,本篇博文是個開始,這個系列的最終目標是找到解決方案來處理最常見的情境。期間我們會討論類似 [ELF](https://pl.wikipedia.org/wiki/Executable_and_Linkable_Format), [DWARF](https://en.wikipedia.org/wiki/DWARF) 的話題,還會接觸到一些架構相關的問題。## 環境 ##整個系列文章中,我們都會使用 [Docker](https://www.docker.com/) 來擷取基於 Debian Jessie 的可複製的 playground。我使用的是 [x86-64](https://en.wikipedia.org/wiki/X86-64),這可以在一定程度上讓我們在做一些底層討論的時候起點作用。項目結構如下:```> tree.├── Dockerfile└── src└── github.com└── mlowicki├── debugger│ └── debugger.go└── hello└── hello.go```我們馬上要用到的調試器的主要檔案就是 *debugger.go*,*hello.go* 檔案包含我們整個流程中調試的 sample 程式原始碼。現在你寫最簡單的內容就可以:```gopackage mainfunc main() {}```我們先寫一個非常簡單的 Dockerfile:```FROM golang:1.8.1RUN apt-get update && apt-get install -y tree```為了編譯 Docker 鏡像,到(Dockerfile 所在的)最外層目錄,運行:```> docker build -t godebugger .```給容器加速,執行:```> docker run --rm -it -v "$PWD"/src:/go/src --security-opt seccomp=unconfined godebugger```[這裡](https://docs.docker.com/engine/security/seccomp/) 有安全運算模式(seccomp)的相關描述。現在剩下的是在容器裡編譯這這兩個程式。第一個可以這樣做:```> go install --gcflags="-N -l" github.com/mlowicki/hello```標識 --gcflag 用于禁止 [內嵌函式](https://en.wikipedia.org/wiki/Inline_expansion) (-l),編譯最佳化(-N)可以讓調試更容易。調試器如下做編譯:```> go install github.com/mlowicki/debugger```在容器的環境變數 *PATH* 中包含 `/go/bin` ,這樣不用使用完整路徑就可以運行任何剛編譯好的程式,不論是 `hello` 還是 `debugger`。## 第一步 ##我們的第一個任務很簡單。在執行任何指令之前停止程式,然後再運行起來,直到程式停止(不管是自動停止還是出現錯誤停止)。大多數調試器你都可以這樣開始使用。設定一些跟蹤點(斷點),然後執行類似 `continue` 的指令真正的跑起來,直到停在你要停的地方。我們看看 [Delve](https://github.com/derekparker/delve) 是如何工作的:```shell> cat hello.gopackage mainimport "fmt"func f() int {var n intn = 1n = 2return n}func main() {fmt.Println(f())}> dlv debugbreak Type ‘help’ for list of commands.(dlv) break main.fBreakpoint 1 set at 0x1087050 for main.f() ./hello.go:5(dlv) continue> main.f() ./hello.go:5 (hits goroutine(1):1 total:1) (PC: 0x1087050) 1: package main 2: 3: import "fmt" 4:=> 5: func f() int { 6: var n int 7: n = 1 8: n = 2 9: return n10: }(dlv) next> main.f() ./hello.go:6 (PC: 0x1087067) 1: package main 2: 3: import "fmt" 4: 5: func f() int {=> 6: var n int 7: n = 1 8: n = 2 9: return n10: }11:(dlv) print n842350461344(dlv) next> main.f() ./hello.go:7 (PC: 0x108706f) 2: 3: import "fmt" 4: 5: func f() int { 6: var n int=> 7: n = 1 8: n = 2 9: return n10: }11:12: func main() {(dlv) print n0(dlv) next> main.f() ./hello.go:8 (PC: 0x1087077) 3: import "fmt" 4: 5: func f() int { 6: var n int 7: n = 1=> 8: n = 2 9: return n10: }11:12: func main() {13: fmt.Println(f())(dlv) print n1```讓我們看看我們自己怎麼實現。第一步是需要給進程(我們的調試器)找一個機制,去控制其他進程(我們要調試的進程)。幸好在 Linux 上我們有這個-- [ptrace](http://man7.org/linux/man-pages/man2/ptrace.2.html)。這還不算。Golang 的 [syscall](https://golang.org/pkg/syscall/) 包提供了一個類似 [PtraceCont](https://golang.org/pkg/syscall/#PtraceCont) 的介面,可以重啟被跟蹤的進程。因此這裡包含了第二部分內容,但是為了有機會在程式開始執行之前設定斷點我們還得做點其他的。建立新進程的時候我們可以通過設定屬性-- [SysProcAttr](https://golang.org/pkg/syscall/#SysProcAttr) 指定進程行為。其中一個是 *Ptrace* 可以跟蹤進程,然後進程會停止並在開啟之前給父進程發送 [SIGSTOP signal](http://man7.org/linux/man-pages/man7/signal.7.html)。我們把剛才學到的內容整理成一個工作流程...```shell> cat src/github.com/mlowicki/hello/hello.gopackage mainimport "fmt"func main() {fmt.Println("hello world")}> cat src/github.com/mlowicki/debugger/debugger.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)log.Println("Restarting...")err = syscall.PtraceCont(cmd.Process.Pid, 0)if err != nil {log.Panic(err)}var ws syscall.WaitStatus_, err = syscall.Wait4(cmd.Process.Pid, &ws, syscall.WALL, nil)if err != nil {log.Fatal(err)}log.Printf("Exited: %v\n", ws.Exited())log.Printf("Exit status: %v\n", ws.ExitStatus())}> go install -gcflags="-N -l" github.com/mlowicki/hello> go install github.com/mlowicki/debugger> debugger /go/bin/hello2017/05/05 20:09:38 State: stop signal: trace/breakpoint trap2017/05/05 20:09:38 Restarting...hello world2017/05/05 20:09:38 Exited: true2017/05/05 20:09:38 Exit status: 0```第一版的調試器實現方式很簡單。啟動了一個被跟蹤的進程,然後進程在執行第一條指令前停止,並向父進程發送了一個 signal。父進程等待這個 signal,打出日誌 `log.Printf("State: %v\n", err)`。之後程式重啟,父進程等待其終止。這種方式可以讓我們有機會提前設定斷點,啟動程式,等一會到達指定跟蹤點,看看類似堆棧或註冊表裡的當前值,檢查下進程狀態。哪怕知道一點點,我們也可以做一些很贊的事情。這些都會為今後的提高和實踐(不久的將來)奠定基礎。給我們點個贊吧,讓更多人能看到這篇文章。如果你想獲得新博文的更新或者在工作中獲得提高,請關注我們吧。
via: https://medium.com/golangspec/making-debugger-for-golang-part-i-53124284b7c8
作者:Michał Łowicki 譯者:ArisAries 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
628 次點擊 ∙ 1 贊