個人經驗分享如何閱讀Go語言原始碼

asong 發表於 2022-05-17
Go

原文連結:分享如何閱讀Go語言原始碼

前言

哈嘍,大家好,我是asong;最近在看Go語言排程器相關的原始碼,發現看原始碼真是個技術活,所以本文就簡單總結一下該如何檢視Go原始碼,希望對你們有幫助。

Go原始碼包括哪些?

以我個人理解,Go原始碼主要分為兩部分,一部分是官方提供的標準庫,一部分是Go語言的底層實現,Go語言的所有原始碼/標準庫/編譯器都在src目錄下:https://github.com/golang/go/...,想看什麼庫的原始碼任君選擇;

觀看Go標準庫 and Go底層實現的原始碼難易度也是不一樣的,我們一般也可以先從標準庫入手,挑選你感興趣的模組,把它吃透,有了這個基礎後,我們在看Go語言底層實現的原始碼會稍微輕鬆一些;下面就針對我個人的一點學習心得分享一下如何檢視Go原始碼;

檢視標準庫原始碼

標準庫的原始碼看起來稍容易些,因為標準庫也屬於上層應用,我們可以藉助IDE的幫忙,其在IDE上就可以跳轉到原始碼包,我們只需要不斷來回跳轉檢視各個函式實現做好筆記即可,因為一些原始碼設計的比較複雜,大家在看時最好通過畫圖輔助一下,個人覺得畫UML是最有助於理解的,能更清晰的理清各個實體的關係;

有些時候只看程式碼是很難理解的,這時我們使用線上除錯輔助我們理解,使用IDE提供的偵錯程式或者GDB都可以達到目的,寫一個簡單的demo,斷點一打,單步除錯走起來,比如你要檢視fmt.Println的原始碼,開局一個小紅點,然後就是點點點;

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/020ee5d660264362b42dab2a142bc369~tplv-k3u1fbpfcp-zoom-1.image" />

檢視Go語言底層實現

人都是會對未知領域充滿好奇,當使用一段時間Go語言後,就想更深入的搞明白一些事情,例如:Go程式的啟動過程是怎樣的,goroutine是怎麼排程的,map是怎麼實現的等等一些Go底層的實現,這種直接依靠IDE跳轉追溯程式碼是辦不到的,這些都屬於Go語言的內部實現,大都在src目錄下的runtime包內實現,其實現了垃圾回收,併發控制, 棧管理以及其他一些 Go 語言的關鍵特性,在編譯Go程式碼為機器程式碼時也會將其也編譯進來,runtime就是Go程式執行時候使用的庫,所以一些Go底層原理都在這個包內,我們需要藉助一些方式才能檢視到Go程式執行時的程式碼,這裡分享兩種方式:分析彙編程式碼、dlv除錯;

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43155623d60042d98539d4885bb73020~tplv-k3u1fbpfcp-zoom-1.image" alt="" />

分析彙編程式碼

前面我們已經介紹了Go語言實現了runtime庫,我們想看到一些Go語言關鍵字特性對應runtime裡的那個函式,可以檢視彙編程式碼,Go語言的彙編使用的plan9,與x86彙編差別還是很大,很多朋友都不熟悉plan9的彙編,但是要想看懂Go原始碼還是要對plan9彙編有一個基本的瞭解的,這裡推薦曹大的文章:plan9 assembly 完全解析,會一點彙編我們就可以看原始碼了,比如想在我們想看make是怎麼初始化slice的,這時我們可以先寫一個簡單的demo

// main.go
import "fmt"

func main() {
    s := make([]int, 10, 20)
    fmt.Println(s)
}

有兩種方式可以檢視彙編程式碼:

1. go tool compile -S -N -l main.go
2. go build main.go && go tool objdump ./main

方式一是將原始碼編譯成.o檔案,並輸出彙編程式碼,方式二是反彙編,這裡推薦使用方式一,執行方式一命令後,我們可以看到對應的彙編程式碼如下:

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6071b1f1459144f5b891c3c4d60559df~tplv-k3u1fbpfcp-zoom-1.image" />

s := make([]int, 10, 20)對應的原始碼就是 runtime.makeslice(SB),這時候我們就去runtime包下找makeslice函式,不斷追蹤下去就可檢視原始碼實現了,可在runtime/slice.go中找到:

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9905faa0ef2479b997279691ddba98f~tplv-k3u1fbpfcp-zoom-1.image" />

線上除錯

雖然上面的方法可以幫助我們定位到原始碼,但是後續的操作全靠review還是難於理解的,如果能線上除錯跟蹤程式碼可以更好助於我們理解,目前Go語言支援GDBLLDBDelve偵錯程式,但只有Delve是專門為Go語言設計開發的除錯工具,所以使用Delve可以輕鬆除錯Go彙編程式,Delve的入門文章有很多,這篇就不在介紹Delve的詳細使用方法,入門大家可以看曹大的文章:https://chai2010.cn/advanced-...,本文就使用一個小例子帶大家來看一看dlv如何除錯Go原始碼,大家都知道向一個nil的切片追加元素,不會有任何問題,在原始碼中是怎麼實現的呢?接下老我們使用dlv除錯跟蹤一下,先寫一個小demo

import "fmt"

func main() {
    var s []int
    s = append(s, 1)
    fmt.Println(s)
}

進入命令列包目錄,然後輸入dlv debug進入除錯

$ dlv debug
Type 'help' for list of commands.
(dlv)

因為這裡我們想看到append的內部實現,所以在append那行加上斷點,執行如下命令:

(dlv) break main.go:7
Breakpoint 1 set at 0x10aba57 for main.main() ./main.go:7

執行continue命令,執行到斷點處:

(dlv) continue
> main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x10aba57)
     2: 
     3: import "fmt"
     4: 
     5: func main() {
     6:         var s []int
=>   7:         s = append(s, 1)
     8:         fmt.Println(s)
     9: }

接下來我們執行disassemble反彙編命令檢視main函式對應的彙編程式碼:

(dlv) disassemble
TEXT main.main(SB) /Users/go/src/asong.cloud/Golang_Dream/code_demo/src_code/main.go
        main.go:5       0x10aba20       4c8d6424e8                      lea r12, ptr [rsp-0x18]
        main.go:5       0x10aba25       4d3b6610                        cmp r12, qword ptr [r14+0x10]
        main.go:5       0x10aba29       0f86f6000000                    jbe 0x10abb25
        main.go:5       0x10aba2f       4881ec98000000                  sub rsp, 0x98
        main.go:5       0x10aba36       4889ac2490000000                mov qword ptr [rsp+0x90], rbp
        main.go:5       0x10aba3e       488dac2490000000                lea rbp, ptr [rsp+0x90]
        main.go:6       0x10aba46       48c744246000000000              mov qword ptr [rsp+0x60], 0x0
        main.go:6       0x10aba4f       440f117c2468                    movups xmmword ptr [rsp+0x68], xmm15
        main.go:7       0x10aba55       eb00                            jmp 0x10aba57
=>      main.go:7       0x10aba57*      488d05a2740000                  lea rax, ptr [rip+0x74a2]
        main.go:7       0x10aba5e       31db                            xor ebx, ebx
        main.go:7       0x10aba60       31c9                            xor ecx, ecx
        main.go:7       0x10aba62       4889cf                          mov rdi, rcx
        main.go:7       0x10aba65       be01000000                      mov esi, 0x1
        main.go:7       0x10aba6a       e871c3f9ff                      call $runtime.growslice
        main.go:7       0x10aba6f       488d5301                        lea rdx, ptr [rbx+0x1]
        main.go:7       0x10aba73       eb00                            jmp 0x10aba75
        main.go:7       0x10aba75       48c70001000000                  mov qword ptr [rax], 0x1
        main.go:7       0x10aba7c       4889442460                      mov qword ptr [rsp+0x60], rax
        main.go:7       0x10aba81       4889542468                      mov qword ptr [rsp+0x68], rdx
        main.go:7       0x10aba86       48894c2470                      mov qword ptr [rsp+0x70], rcx
        main.go:8       0x10aba8b       440f117c2450                    movups xmmword ptr [rsp+0x50], xmm15
        main.go:8       0x10aba91       488d542450                      lea rdx, ptr [rsp+0x50]
        main.go:8       0x10aba96       4889542448                      mov qword ptr [rsp+0x48], rdx
        main.go:8       0x10aba9b       488b442460                      mov rax, qword ptr [rsp+0x60]
        main.go:8       0x10abaa0       488b5c2468                      mov rbx, qword ptr [rsp+0x68]
        main.go:8       0x10abaa5       488b4c2470                      mov rcx, qword ptr [rsp+0x70]
        main.go:8       0x10abaaa       e8f1dff5ff                      call $runtime.convTslice
        main.go:8       0x10abaaf       4889442440                      mov qword ptr [rsp+0x40], rax
        main.go:8       0x10abab4       488b542448                      mov rdx, qword ptr [rsp+0x48]
        main.go:8       0x10abab9       8402                            test byte ptr [rdx], al
        main.go:8       0x10ababb       488d35be640000                  lea rsi, ptr [rip+0x64be]
        main.go:8       0x10abac2       488932                          mov qword ptr [rdx], rsi
        main.go:8       0x10abac5       488d7a08                        lea rdi, ptr [rdx+0x8]
        main.go:8       0x10abac9       833d30540d0000                  cmp dword ptr [runtime.writeBarrier], 0x0
        main.go:8       0x10abad0       7402                            jz 0x10abad4
        main.go:8       0x10abad2       eb06                            jmp 0x10abada
        main.go:8       0x10abad4       48894208                        mov qword ptr [rdx+0x8], rax
        main.go:8       0x10abad8       eb08                            jmp 0x10abae2
        main.go:8       0x10abada       e8213ffbff                      call $runtime.gcWriteBarrier
        main.go:8       0x10abadf       90                              nop
        main.go:8       0x10abae0       eb00                            jmp 0x10abae2
        main.go:8       0x10abae2       488b442448                      mov rax, qword ptr [rsp+0x48]
        main.go:8       0x10abae7       8400                            test byte ptr [rax], al
        main.go:8       0x10abae9       eb00                            jmp 0x10abaeb
        main.go:8       0x10abaeb       4889442478                      mov qword ptr [rsp+0x78], rax
        main.go:8       0x10abaf0       48c784248000000001000000        mov qword ptr [rsp+0x80], 0x1
        main.go:8       0x10abafc       48c784248800000001000000        mov qword ptr [rsp+0x88], 0x1
        main.go:8       0x10abb08       bb01000000                      mov ebx, 0x1
        main.go:8       0x10abb0d       4889d9                          mov rcx, rbx
        main.go:8       0x10abb10       e8aba8ffff                      call $fmt.Println
        main.go:9       0x10abb15       488bac2490000000                mov rbp, qword ptr [rsp+0x90]
        main.go:9       0x10abb1d       4881c498000000                  add rsp, 0x98
        main.go:9       0x10abb24       c3                              ret
        main.go:5       0x10abb25       e8f61efbff                      call $runtime.morestack_noctxt
        .:0             0x10abb2a       e9f1feffff                      jmp $main.main

從以上內容我們看到呼叫了runtime.growslice方法,我們在這裡加一個斷點:

(dlv) break runtime.growslice
Breakpoint 2 set at 0x1047dea for runtime.growslice() /usr/local/opt/go/libexec/src/runtime/slice.go:162

之後我們再次執行continue執行到該斷點處:

(dlv) continue
> runtime.growslice() /usr/local/opt/go/libexec/src/runtime/slice.go:162 (hits goroutine(1):1 total:1) (PC: 0x1047dea)
Warning: debugging optimized function
   157: // NOT to the new requested capacity.
   158: // This is for codegen convenience. The old slice's length is used immediately
   159: // to calculate where to write new values during an append.
   160: // TODO: When the old backend is gone, reconsider this decision.
   161: // The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
=> 162: func growslice(et *_type, old slice, cap int) slice {
   163:         if raceenabled {
   164:                 callerpc := getcallerpc()
   165:                 racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
   166:         }
   167:         if msanenabled {

之後就是不斷的單步除錯可以看出來切片的擴容策略;到這裡大家也就明白了為啥向nil的切片追加資料不會有問題了,因為在容量不夠時會呼叫growslice函式進行擴容,具體擴容規則大家可以繼續追蹤,打臉網上那些瞎寫的文章。

上文我們介紹除錯彙編的一個基本流程,下面在介紹兩個我在看原始碼時經常使用的命令;

  • goroutines命令:通過goroutines命令(簡寫grs),我們可以檢視所goroutine,通過goroutine (alias: gr)命令可以檢視當前的gourtine
(dlv) grs
* Goroutine 1 - User: ./main.go:7 main.main (0x10aba6f) (thread 218565)
  Goroutine 2 - User: /usr/local/opt/go/libexec/src/runtime/proc.go:367 runtime.gopark (0x1035232) [force gc (idle)]
  Goroutine 3 - User: /usr/local/opt/go/libexec/src/runtime/proc.go:367 runtime.gopark (0x1035232) [GC sweep wait]
  Goroutine 4 - User: /usr/local/opt/go/libexec/src/runtime/proc.go:367 runtime.gopark (0x1035232) [GC scavenge wait]
  Goroutine 5 - User: /usr/local/opt/go/libexec/src/runtime/proc.go:367 runtime.gopark (0x1035232) [finalizer wait]
  • stack命令:通過stack命令(簡寫bt),我們可檢視當前函式呼叫棧資訊:
(dlv) bt
0  0x0000000001047e15 in runtime.growslice
   at /usr/local/opt/go/libexec/src/runtime/slice.go:183
1  0x00000000010aba6f in main.main
   at ./main.go:7
2  0x0000000001034e13 in runtime.main
   at /usr/local/opt/go/libexec/src/runtime/proc.go:255
3  0x000000000105f9c1 in runtime.goexit
   at /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:1581
  • regs命令:通過regs命令可以檢視全部的暫存器狀態,可以通過單步執行來觀察暫存器的變化:
(dlv) regs
   Rip = 0x0000000001047e15
   Rsp = 0x000000c00010de68
   Rax = 0x00000000010b2f00
   Rbx = 0x0000000000000000
   Rcx = 0x0000000000000000
   Rdx = 0x0000000000000008
   Rsi = 0x0000000000000001
   Rdi = 0x0000000000000000
   Rbp = 0x000000c00010ded0
    R8 = 0x0000000000000000
    R9 = 0x0000000000000008
   R10 = 0x0000000001088c40
   R11 = 0x0000000000000246
   R12 = 0x000000c00010df60
   R13 = 0x0000000000000000
   R14 = 0x000000c0000001a0
   R15 = 0x00000000000000c8
Rflags = 0x0000000000000202     [IF IOPL=0]
    Cs = 0x000000000000002b
    Fs = 0x0000000000000000
    Gs = 0x0000000000000000
  • locals命令:通過locals命令,可以檢視當前函式所有變數值:
(dlv) locals
newcap = 1
doublecap = 0

總結

看原始碼的過程是沒有捷徑可走的,如果說有,那就是可以先看一些大佬輸出的底層原理的文章,然後參照其文章一步步入門原始碼閱讀,最終還是要自己去克服這個困難,本文介紹了我自己檢視原始碼的一些方式,你是否有更簡便的方式呢?歡迎評論區分享出來~。

好啦,本文到這裡就結束了,我是asong,我們下期見。

歡迎關注公眾號:Golang夢工廠