曹大帶我學 Go(3)—— 如何用匯編打同事的臉

你好,我是小 X。

曹大最近開 Go 課程了,小 X 正在和曹大學 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 源碼的過程中事半功倍。

好了,這就是今天全部的內容了~ 我是小 X,我們下期再見~


歡迎關注曹大的 TechPaper 以及碼農桃花源~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2Y3qPkysRLaWDcIyLOerZg