gdb 調試 Go 源碼

Go 的源碼是用 Go 寫的。可以很方便的閱讀。

一、源碼編譯安裝

  1. Go 的源碼編譯需要一個 >= 1.4 版本的編譯好的二進制文件來編譯,去 golang.org 下載編譯包和源碼包。
go1.14.6.src.tar.gz
go1.16.4.linux-amd64.tar.gz

當然也可以下載不同版本的源碼包和編譯包。

  1. 解壓編譯包和源碼包
tar -zxvf go1.16.4.linux-amd64.tar.gz
mv go go_bin
tar -zxvf go1.14.6.src.tar.gz

這樣得到了兩個目錄,一個是編譯好的 Go,一個源碼 Go。查看一下編譯好的 Go:

./go_bin/bin/go version

#go version go1.16.4 linux/amd64
  1. 設置編譯用的目錄和參數
export GOROOT_BOOTSTRAP=~/go_bin // 改成自己 go_bin 所在目錄
export GO_GCFLAGS="-N -l"

GOROOT_BOOTSTRAP 是放編譯好的 Go 二進制文件的目錄。 GO_GCFLAGS 是編譯用的參數,"-N":表示不需要優化,"-l":表示不需要內聯,禁止優化和內聯可以讓運行時(runtime)的函數更易於調試。

  1. 編譯 Go 源碼
#進入源碼
cd ./go/src
#執行編譯(調用 make.bash)
./all.bash

編譯完成,go/bin 下生成兩個可執行文件

go
gofmt
  1. 設置環境變量
vim ~/.bashrc
# go 環境
export GOROOT=/home/shijie/go
export GOPATH=/home/shijie/work
export PATH=$PATH:$GOROOT/bin
export PATH=$PATH:$GOPATH/bin

export GO111MODULE=on
export GOPROXY=https://goproxy.io

#應用環境
source ~/.bashrc

Go

go version

#go version go1.16.4 linux/amd64

編譯完成

二、gdb 調試源碼

ok,編譯完源碼,寫個小 demo。

package main
import "fmt"

func main(){
    strSlice := []string{"bye world","say hi","i hit it"}
    newStrSlice := append(strSlice,"fuck u")
    fmt.Println(newStrSlice)
}

ok,編譯一下 demo,記得不要優化內聯

go build -gcflags "-N -l"

編譯完,目錄下回生成一個 main 的可執行文件。用 gdb 執行它,並開始調試

gdb ./main
# 進入 gdb 調試模式,加斷點
# 在main.go 第 6 行斷點
(gdb)b main.go:6 
# 運行
(gdb)r 
# Thread 1 "main" hit Breakpoint 1, main.main () at /home/shijie/work/test/main.go:6
# 24		newStrSlice := append(strSlice,"fuck u")

# 進入 append 源碼
(gdb)s 
# runtime.growslice (et=0x4a31a0, old=..., cap=7, ~r3=...) at /home/shijie/file/software/go/src/runtime/slice.go:125
# 125	func growslice(et *_type, old slice, cap int) slice {

# 查看代碼
(gdb)l
#120	// NOT to the new requested capacity.
#121	// This is for codegen convenience. The old slice's length is used immediately
#122	// to calculate where to write new values during an append.
#123	// TODO: When the old backend is gone, reconsider this decision.
#124	// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
#125	func growslice(et *_type, old slice, cap int) slice {
#126		if raceenabled {
#127			callerpc := getcallerpc()
#128			racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
#129}

# 這裏可以看到進入了源碼,在 src/runtime/slice.go 裏,append 的動態擴展。

# 查看反彙編
(gdb)disas

Dump of assembler code for function runtime.growslice:
=> 0x0000000000448500 <+0>:	mov    %fs:0xfffffffffffffff8,%rcx
   0x0000000000448509 <+9>:	cmp    0x10(%rcx),%rsp
   0x000000000044850d <+13>:	jbe    0x448c4f <runtime.growslice+1871>
   0x0000000000448513 <+19>:	sub    $0x60,%rsp
   0x0000000000448517 <+23>:	mov    %rbp,0x58(%rsp)
   0x000000000044851c <+28>:	lea    0x58(%rsp),%rbp
   0x0000000000448521 <+33>:	mov    0x88(%rsp),%rdx
   0x0000000000448529 <+41>:	mov    0x80(%rsp),%rbx
   0x0000000000448531 <+49>:	cmp    %rbx,%rdx
   0x0000000000448534 <+52>:	jl     0x448c32 <runtime.growslice+1842>
   0x000000000044853a <+58>:	mov    0x68(%rsp),%rsi
   0x000000000044853f <+63>:	mov    (%rsi),%rdi
   0x0000000000448542 <+66>:	test   %rdi,%rdi
   0x0000000000448545 <+69>:	je     0x448b1b <runtime.growslice+1563>
   0x000000000044854b <+75>:	lea    (%rbx,%rbx,1),%r8
   0x000000000044854f <+79>:	cmp    %r8,%rdx
   0x0000000000448552 <+82>:	jg     0x448b13 <runtime.growslice+1555>
   0x0000000000448558 <+88>:	nopl   0x0(%rax,%rax,1)
   0x0000000000448560 <+96>:	cmp    $0x400,%rbx
   0x0000000000448567 <+103>:	jge    0x448578 <runtime.growslice+120>

還可以 p var 來查看變量的值,gdb 的用法可以網上搜索。在 append 加斷點,一直 s 可以看到從 main 裏調用 append,整個全過程,從 go 源碼到彙編所有的函數調用。

當然你可以修改源碼文件,只要不報錯,重新編譯就可以了。比如:

vim src/runtime/slice.go
// 找到 function growslice,添加 print("test")

然後,進入 go/src 重新編譯

./all.bash

在執行 go run main.go,除了輸出 clice,還會輸出 test。

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