實現一個 Golang 調試器(第三部分)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。到目前為止我們已經知道如何逐步執行用 ptrace 暫停進程(tracee)以及如何從二進位檔案中擷取一些調試資訊(在[這裡](https://studygolang.com/articles/12794)閱讀相關內容)。接下來就是設定斷點,等待程式運行到斷點處,查看進程相關資訊的時候了。讓我們從上一篇文章中用到的彙編代碼開始```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```我們在下面這一行代碼處設定了斷點```asmmov rdi, 1```所以在執行到這行代碼的時候,程式會暫停,這時候檢查 RDI 寄存器中儲存的值是否為 0,然後逐步執行一行代碼,檢查這個值是否變成了 1。## 斷點在 x86 系統中有一條 [中斷指令](https://en.wikipedia.org/wiki/INT_%28x86_instruction%29),它可以產生一個非強制中斷。在 Linux 系統中,這個非強制中斷是通過 f.ex 調用 syscall 來實現的。在 x86-64 系統中引入了一個專用的系統指令來實現這個非強制中斷,比 x86 系統中的指令更快,這就我這所以用這個專用指令的原因了。但是我們可以用普通的 INT 中斷完成同樣的任務。我們用 INT 3 來設定一個斷點,它對應的作業碼是 0xCCINT 3 指令會產生大小為一個位元組的特殊作業碼(CC),通過它可以調用異常處理函數,(這個作業碼非常有用,因為它可以用來替換任何一條指令的第一個位元組,使之成為一個斷點,然後再加入額外的一個位元組,而不影響其它的代碼),具體資訊參見以下文檔[Intel 64 and IA-32 系統軟體使用手冊](https://software.intel.com/en-us/articles/intel-sdm)我們用 0xCC 來替換特定指令的頭一個位元組,使之成為一個斷點,一旦這個斷點被出發,我們就可以做以下的事情1. 查看進程狀態2. 把 0xCC 作業碼替換成原來的值3. 把程式的計數器值減 14. 執行一條指令我們需要處理的第一個問題是:在哪放置 0xCC,我們不知道 move rdi, 1 這條指令在記憶體中的具體位置。由於這是第二條指令,所以在程式的開始記憶體位址基礎上加上第一條指令 move rax, 1 的長度,就應該是這條指令的記憶體位址。由於 x86 系統中指令長度不是定長的,所以讓確定指令開始地址變得更加困難了。程式第一條指令的位置可以通過讓程式在沒有執行任何指令的時候停止的辦法得到(我們之前已經做過了),第一條指令的長度可以通過 objdump 命令來擷取:```shell> nasm -f elf64 -o hello.o src/github.com/mlowicki/hello/hello.asm && ld -o /go/bin/hello hello.o> objdump -d -M intel /go/bin/hello/go/bin/hello: file format elf64-x86-64Disassembly of section .text:00000000004000b0 <_start>:4000b0: b8 01 00 00 00 mov eax,0x14000b5: bf 01 00 00 00 mov edi,0x14000ba: 48 be d8 00 60 00 00 movabs rsi,0x6000d84000c1: 00 00 004000c4: ba 0e 00 00 00 mov edx,0xe4000c9: 0f 05 syscall4000cb: b8 3c 00 00 00 mov eax,0x3c4000d0: bf 00 00 00 00 mov edi,0x04000d5: 0f 05 syscall```從上面的輸出我們可以發現第一條指令的長度是 5 個位元組(4000b5 - 4000b0),所以我們要把 0xCC 放在第一條指令的位置加 5 個位元組地方,下面是代碼實現```gopackage mainimport ("flag""log""os""os/exec""syscall")func step(pid int) {err := syscall.PtraceSingleStep(pid)if err != nil {log.Fatal(err)}}func cont(pid int) {err := syscall.PtraceCont(pid, 0)if err != nil {log.Fatal(err)}}func setPC(pid int, pc uint64) {var regs syscall.PtraceRegserr := syscall.PtraceGetRegs(pid, &regs)if err != nil {log.Fatal(err)}regs.SetPC(pc)err = syscall.PtraceSetRegs(pid, &regs)if err != nil {log.Fatal(err)}}func getPC(pid int) uint64 {var regs syscall.PtraceRegserr := syscall.PtraceGetRegs(pid, &regs)if err != nil {log.Fatal(err)}return regs.PC()}func setBreakpoint(pid int, breakpoint uintptr) []byte {original := make([]byte, 1)_, err := syscall.PtracePeekData(pid, breakpoint, original)if err != nil {log.Fatal(err)}_, err = syscall.PtracePokeData(pid, breakpoint, []byte{0xCC})if err != nil {log.Fatal(err)}return original}func clearBreakpoint(pid int, breakpoint uintptr, original []byte) {_, err := syscall.PtracePokeData(pid, breakpoint, original)if err != nil {log.Fatal(err)}}func printState(pid int) {var regs syscall.PtraceRegserr := syscall.PtraceGetRegs(pid, &regs)if err != nil {log.Fatal(err)}log.Printf("RAX=%d, RDI=%d\n", regs.Rax, regs.Rdi)}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)pid := cmd.Process.Pidbreakpoint := uintptr(getPC(pid) + 5)original := setBreakpoint(pid, breakpoint)cont(pid)var ws syscall.WaitStatus_, err = syscall.Wait4(pid, &ws, syscall.WALL, nil)clearBreakpoint(pid, breakpoint, original) printState(pid)setPC(pid, uint64(breakpoint))step(pid)_, err = syscall.Wait4(pid, &ws, syscall.WALL, nil)printState(pid)}```源檔案以一些輔助函數開始,setPC 和 getPC 用來維護 [程式計數器](https://en.wikipedia.org/wiki/Program_counter),寄存器 PC 存放的是下一條要執行的指令。如果程式在沒有執行任何指令的時候被暫停,PC 中的值就是程式第一條指令的記憶體位址。維護斷點的函數(setBreakpoint 和 clearBreakpoint)負責在指令中插入或者移除作業碼 0xCC,下面是程式的輸出:```shell> go install github.com/mlowicki/breakpoint> breakpoint /go/bin/hello2017/07/16 21:06:33 State: stop signal: trace/breakpoint trap2017/07/16 21:06:33 RAX=1, RDI=02017/07/16 21:06:33 RAX=1, RDI=1```輸出和我們預期的一樣,當程式到達斷點時,RDI 寄存器沒有被設定(值 為 0 ),執行完下一條指令後(第二條指令),它的值變成了下面指令設定的值```asmmov rdi, 1```現在我們已經完成了文章一開始列出的任務。當然我們的程式還需要一些計算指令長度的函數,不過不用擔心,我們會在之後實現這些功能## REPL現在是時候實現調試器的基本架構了,這是一個簡單的命令列程式,程式迴圈等待使用者輸入像 "set a breakpoint at " 和 "go single step "這樣的命令```gopackage mainimport ("bufio""flag""fmt""io""log""os""os/exec""strings""syscall")func initTracee(path string) int {cmd := exec.Command(path)cmd.Args = []string{path}cmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrcmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}err := cmd.Start()if err != nil {log.Fatal(err)}err = cmd.Wait()// Process should be stopped here because of trace/breakpoint trapif err == nil {log.Fatal("Program exited")}return cmd.Process.Pid}func main() {flag.Parse()_ = initTracee(flag.Arg(0))for {reader := bufio.NewReader(os.Stdin)fmt.Print("> ")command, err := reader.ReadString('\n')if err != nil {if err == io.EOF {fmt.Println()break}log.Fatal(err)}command = command[:len(command)-1] // get rid of ending newline characterif strings.HasPrefix(command, "register ") {fmt.Println("register...")} else if strings.HasPrefix(command, "breakpoint ") {fmt.Println("breakpoint...")} else if command == "help" {fmt.Println("help...")} else if command == "step" {fmt.Println("step...")} else if command == "continue" {fmt.Println("continue")} else {fmt.Println("unknown command")}}}```這就是我們調試器的基礎架構,它沒有提供太多的功能(目前為上),但是它已經具備了一個 [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) 環境必要的邏輯。我們或多或少知道了一些如何?像下一步、繼續、查看變數狀態等常用調試命令,我們會在不久之後實現這些功能。在 Golang 中不太清晰的一點是斷點命令。這個會在接下來的文章中詳細解釋,為什麼在 Golang 中比想象的困難一些以及如何克服這些複雜性。

via: https://medium.com/golangspec/making-debugger-in-golang-part-iii-5aac8e49f291

作者:Michał Łowicki 譯者:jettyhan 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

380 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.