1. 程式人生 > 其它 >推薦幾個檢視 Go 彙編原始碼的工具使用技巧

推薦幾個檢視 Go 彙編原始碼的工具使用技巧

推薦幾個檢視 Go 彙編原始碼的工具使用技巧

今天介紹幾個常用的檢視 Go 彙編程式碼、除錯 Go 程式的命令和工具,既可以在平時和同事、網友抬槓時使用,還能在關鍵時刻打他們的臉。

比如,有同事說這段程式碼:

package main

type Student struct {
 Class int
}

func main() {
 var a = &Student{1}
 println(a)
}

的執行效率要高於下面這段程式碼:

package main

type Student struct {
 Class int
}

func main() {
 var a = Student{1}
 var b = &a
 println(b)
}

並且給你講了一通道理,你好像沒法辯贏他。怎麼辦?

直接用一行命令生成彙編程式碼,馬上可以戳穿他,打他的臉。

go tool 生成彙編

其實很簡單,有兩個命令可以做到:

go tool compile -S main.go

和:

go build main.go && go tool objdump ./main

前者是編譯,即將原始碼編譯成 .o 目標檔案,並輸出彙編程式碼。

後者是反彙編,即從可執行檔案反編譯成彙編,所以要先用 go build 命令編譯出可執行檔案。

二者不盡相同,但都能看到前面兩個示例程式碼對應的彙編程式碼是一致的。同事的“謠言”不攻自破,臉都被你打疼了。

找到 runtime 原始碼

Go 是一門有 runtime 的語言,什麼是 runtime?其實就是一段輔助程式,使用者沒有寫的程式碼,runtime 替我們寫了,比如 Go 排程器的程式碼。

我們只需要知道用 go 關鍵字建立 goroutine,就可以瘋狂堆業務了。至於 goroutine 是怎麼被排程的,根本不需要關心,這些是 runtime 排程器的工作。

那我們自己寫的程式碼如何和 runtime 裡的程式碼對應起來呢?

前面介紹的方法就可以做到,只需要加一個 grep 就可以。

例如,我想知道 go 關鍵字對應 runtime 裡的哪個函式,於是寫了一段測試程式碼:

package main

func main() {
 go func() {
  println(1+2)
 }()
}

因為 go func(){}() 那一行程式碼在第 4 行,所以,grep 的時候加一個條件:

go tool compile -S main.go | grep "main.go:4"

// 或

go build main.go && go tool objdump ./main | grep "main.go:4"
go func

馬上就能看到 go func(){}() 對應 newproc() 函式,這時再深入研究下 newproc() 函式就大概知道 goroutine 是如何被建立的。

用 dlv 除錯

那有同學問了,有沒有其他可以除錯 Go、以及和 Go 程式互動的方法呢?其實是有的!這就是我們要介紹的 dlv 除錯工具,目前它對除錯 Go 程式的支援是最好的。

之前沒我怎麼研究它,只會一些非常簡單的命令,這次學會了幾個進階的指令,威力挺大,也進一步加深了對 Go 的理解。

下面我們帶著一個任務來講解 dlv 如何使用。

我們知道,向一個 nil 的 slice append 元素,不會有任何問題。但是向一個 nil 的 map 插入新元素,馬上就會報 panic。這是為什麼呢?又是在哪 panic 呢?

首先寫出讓 map 產生 panic 的示例程式:

package main

func main() {
 var m map[int]int
 m[1] = 1
}

接著用 go build 命令編譯生成可執行檔案:

go build a.go

然後,使用 dlv 進入除錯狀態:

dlv exec ./a

使用 b 這個命令打斷點,有三種方法:

  1. b + 地址
  2. b + 程式碼行數
  3. b + 函式名

我們要在對 map 賦值的地方加個斷點。先找到程式碼位置:

cat -n a.go

看到:

hello.go

賦值的地方在第 5 行,加斷點:

(dlv) b a.go:5
Breakpoint 1 set at 0x45e55d for main.main() ./a.go:5

執行 c 命令,直接執行到斷點處:

執行到斷點處

執行 disass 命令,可以看到彙編指令:

disass

這時使用 si 命令,執行單條指令,多次執行 si,就會執行到 map 賦值函式 mapassign_fast64

mapassign_fast64

這時再用單步命令 s,就會進入判斷 h 的值為 nil 的分支,然後執行 panic 函式:

panic

至此,向 nil 的 map 賦值時,產生 panic 的程式碼就被我們找到了。接著,按圖索驥找到對應 runtime 原始碼的位置,就可以進一步探索了。

除此之外,我們還可以使用 bt 命令看到呼叫棧:

呼叫棧

使用 frame 1 命令可以跳轉到相應位置。這裡 1 對應圖中的 a.go:5,也就是我們前面打斷點的地方,是不是非常酷炫。

上面這張圖裡我們也能清楚地看到,使用者 goroutine 其實是被 goexit 函式一路呼叫過來的。當用戶 goroutine 執行完畢後,就會回到 goexit 函式做一些收尾工作。當然,這是題外話了。

另外,用 dlv 也能幹第二部分“找到 runtime 原始碼”活。

總結

今天系統地講了幾招通過命令和工具檢視使用者程式碼對應的 runtime 原始碼或者彙編程式碼的方法,非常實用。最後再彙總一下:

  1. go tool compile

  2. go tool objdump

  3. dlv

使用這些命令和工具,可以讓你在看 Go 原始碼的過程中事半功倍