在当今的信息时代,编程语言已经成为了科技领域的基础工具之一。其中,Go语言以其简洁、高效和并发性强等优点,在开发者中赢得了广泛的好评。然而,对于初学者或是想要深入理解Go语言底层原理的人来说,直接阅读和理解其源码可能会面临一定困难。我主要从以下几个方面分享一些使用ChatGPT辅助学习Go语言底层源码的小技巧。
1、什么情况下使用ChatGPT?
首先我觉着有很多Go开发工程师和我有类似的困惑,就是想要深入学习Go语言底层原理,但自己学着学着,发现很多知识点非常深奥,又很难找到该领域的专家有足够的时间帮你答疑解惑,这个时候我想到了ChatGPT,当遇到难以理解的难题时,可以把自己的问题更加细化去问ChatGPT,让它提供更加详细的解答,直到自己理解为止。所以我的观点是在学习Go语言底层知识之前,需要自己对Go语言相关基础知识有一定的了解,然后带着疑问向ChatGPT提问,当ChatGPT给出答案时,仅供参考,需要有自己的判断,只要ChatGPT给出的解释相对合理,可以姑且相信它,也可以不断地追问加深理解。我这里主要总结了使用ChatGPT的三个方面:
- 基础的概念和原理
- 实际应用示例代码
解读代码
2、汇编基础
- 推荐阅读:https://go.dev/doc/asm
- 精简指令集
- 数据传输: MOV/LEA
- 跳转指令: CMP/TEST/JMP/JCC
- 栈指令: PUSH/POP
- 函数调用指令: CALL/RET
- 算术指令: ADD/SUB/MUL/DIV
- 逻辑指令: AND/OR/XOR/NOT
- 移位指令: SHL/SHR
- JCC有条件跳转: JEQ/JNE/JLT/JLE/JGT/JGE
- 还有针对无符号数的比较条件
- MOV指令
注:图片来源于https://chai2010.cn/asmgo-talk/#/4/6
伪寄存器
- SB: 静态基址指针, 全局符号
- FP: 帧指针, 参数和局部变量
- SP: 栈指针, 栈的顶端
- PC: 程序计数器, 跳转和分支
- 注:伪寄存器仅仅存在于Go汇编中
伪寄存器用法
- GLOBL text(SB),$1: 全局变量
- MOVQ a+0(FP) AX: 函数参数
- MOVQ b+8(SP) AX: 局部变量
- JMP 2(PC): 向前跳转, 常用于宏函数
- JMP -2(PC): 向后跳转, 常用于宏函数
3、代码调试工具
3.1 dlv 简介
- dlv(delve 的简写) 是一个用于 Go 源代码级调试器。
- dlv 通过控制进程的执行、计算变量、提供线程/协程的状态、CPU寄存器状态等信息,可以方便地与程序进行交互。
- 这个工具的目标是为调试 Go 程序提供一个简单但功能强大的界面。
dlv 将标志传递给正在调试的程序,例如:
dlv exec ./hello -- server --config conf/config.toml
- macOs 使用
brew install dlv
命令下载安装即可。
3.2 dlv debug 常用命令
- b 打断点,例如使用
b main.main
来打断点 - p 打印变量
- n: 执行到下一行
- c: 跳过此断点
- args: 打印所有的方法参数
- locals 打印出所有的本地变量
- l 列出断点最近几行的代码
- bp: 展示出所有的断点
- q: 退出
3.3dlv 调试代码
- dlv 剖析 strings.Contains 函数
新建a.go
文件,文件内容如下:
package main
import (
"fmt"
"strings"
)
func main() {
str := "Go语言是世界上最好的语言"
if strings.Contains(str,"世界") {
fmt.Println("字符串中包含","世界")
}else {
fmt.Println("字符串中不包含","世界")
}
}
使用 dlv debug a.go
命令可以开始对上述代码进行断点调试:
qinshixian@qinshixiandeMacBook-Pro qinshixian % dlv debug a.go
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x10acd6f for main.main() ./a.go:8
(dlv) c
> main.main() ./a.go:8 (hits goroutine(1):1 total:1) (PC: 0x10acd6f)
3: import (
4: "fmt"
5: "strings"
6: )
7:
=> 8: func main() {
9: str := "Go语言是世界上最好的语言"
10: if strings.Contains(str,"世界") {
11: fmt.Println("字符串中包含","世界")
12: }else {
13: fmt.Println("字符串中不包含","世界")
(dlv) n
> main.main() ./a.go:9 (PC: 0x10acd86)
4: "fmt"
5: "strings"
6: )
7:
8: func main() {
=> 9: str := "Go语言是世界上最好的语言"
10: if strings.Contains(str,"世界") {
11: fmt.Println("字符串中包含","世界")
12: }else {
13: fmt.Println("字符串中不包含","世界")
14: }
(dlv) n
> main.main() ./a.go:10 (PC: 0x10acd9b)
5: "strings"
6: )
7:
8: func main() {
9: str := "Go语言是世界上最好的语言"
=> 10: if strings.Contains(str,"世界") {
11: fmt.Println("字符串中包含","世界")
12: }else {
13: fmt.Println("字符串中不包含","世界")
14: }
15: }
(dlv) p str
"Go语言是世界上最好的语言"
(dlv) s strings.Contains
> strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61 (PC: 0x10ac1e6)
56: s = s[i+len(substr):]
57: }
58: }
59:
60: // Contains reports whether substr is within s.
=> 61: func Contains(s, substr string) bool {
62: return Index(s, substr) >= 0
63: }
64:
65: // ContainsAny reports whether any Unicode code points in chars are within s.
66: func ContainsAny(s, chars string) bool {
(dlv) n
> strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:62 (PC: 0x10ac20d)
57: }
58: }
59:
60: // Contains reports whether substr is within s.
61: func Contains(s, substr string) bool {
=> 62: return Index(s, substr) >= 0
63: }
64:
65: // ContainsAny reports whether any Unicode code points in chars are within s.
66: func ContainsAny(s, chars string) bool {
67: return IndexAny(s, chars) >= 0
(dlv) b Contains
Breakpoint 2 set at 0x10ac1e6 for strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61
(dlv) p s
"Go语言是世界上最好的语言"
(dlv) p substr
"世界"
(dlv) s Index
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1024 (PC: 0x10ac32f)
1019: // One string is empty. Are both?
1020: return s == t
1021: }
1022:
1023: // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
=>1024: func Index(s, substr string) int {
1025: n := len(substr)
1026: switch {
1027: case n == 0:
1028: return 0
1029: case n == 1:
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1025 (PC: 0x10ac36f)
1020: return s == t
1021: }
1022:
1023: // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
1024: func Index(s, substr string) int {
=>1025: n := len(substr)
1026: switch {
1027: case n == 0:
1028: return 0
1029: case n == 1:
1030: return IndexByte(s, substr[0])
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1027 (PC: 0x10ac380)
1022:
1023: // Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
1024: func Index(s, substr string) int {
1025: n := len(substr)
1026: switch {
=>1027: case n == 0:
1028: return 0
1029: case n == 1:
1030: return IndexByte(s, substr[0])
1031: case n == len(s):
1032: if substr == s {
(dlv) p n
6
(dlv) p len(s)
35
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1029 (PC: 0x10ac3a4)
1024: func Index(s, substr string) int {
1025: n := len(substr)
1026: switch {
1027: case n == 0:
1028: return 0
=>1029: case n == 1:
1030: return IndexByte(s, substr[0])
1031: case n == len(s):
1032: if substr == s {
1033: return 0
1034: }
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1031 (PC: 0x10ac403)
1026: switch {
1027: case n == 0:
1028: return 0
1029: case n == 1:
1030: return IndexByte(s, substr[0])
=>1031: case n == len(s):
1032: if substr == s {
1033: return 0
1034: }
1035: return -1
1036: case n > len(s):
(dlv)
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1036 (PC: 0x10ac494)
1031: case n == len(s):
1032: if substr == s {
1033: return 0
1034: }
1035: return -1
=>1036: case n > len(s):
1037: return -1
1038: case n <= bytealg.MaxLen:
1039: // Use brute force when s and substr both are small
1040: if len(s) <= bytealg.MaxBruteForce {
1041: return bytealg.IndexString(s, substr)
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1038 (PC: 0x10ac4cf)
1033: return 0
1034: }
1035: return -1
1036: case n > len(s):
1037: return -1
=>1038: case n <= bytealg.MaxLen:
1039: // Use brute force when s and substr both are small
1040: if len(s) <= bytealg.MaxBruteForce {
1041: return bytealg.IndexString(s, substr)
1042: }
1043: c0 := substr[0]
(dlv) p bytealg.MaxLen
63
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1040 (PC: 0x10ac4e7)
1035: return -1
1036: case n > len(s):
1037: return -1
1038: case n <= bytealg.MaxLen:
1039: // Use brute force when s and substr both are small
=>1040: if len(s) <= bytealg.MaxBruteForce {
1041: return bytealg.IndexString(s, substr)
1042: }
1043: c0 := substr[0]
1044: c1 := substr[1]
1045: i := 0
(dlv) p len(s)
35
(dlv) p bytealg.MaxBruteForce
64
(dlv) n
> strings.Index() /Users/qinshixian/.g/go/src/strings/strings.go:1041 (PC: 0x10ac4ff)
1036: case n > len(s):
1037: return -1
1038: case n <= bytealg.MaxLen:
1039: // Use brute force when s and substr both are small
1040: if len(s) <= bytealg.MaxBruteForce {
=>1041: return bytealg.IndexString(s, substr)
1042: }
1043: c0 := substr[0]
1044: c1 := substr[1]
1045: i := 0
1046: t := len(s) - n + 1
(dlv) s bytealg.IndexString
> internal/bytealg.IndexString() /Users/qinshixian/.g/go/src/internal/bytealg/index_amd64.s:18 (PC: 0x1002840)
Warning: debugging optimized function
13: MOVQ DI, R10
14: LEAQ ret+48(FP), R11
15: JMP indexbody<>(SB)
16:
17: TEXT ·IndexString(SB),NOSPLIT,$0-40
=> 18: MOVQ a_base+0(FP), DI
19: MOVQ a_len+8(FP), DX
20: MOVQ b_base+16(FP), R8
21: MOVQ b_len+24(FP), AX
22: MOVQ DI, R10
23: LEAQ ret+32(FP), R11
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x1032e20 for runtime.throw() /Users/qinshixian/.g/go/src/runtime/panic.go:1188 (0)
Breakpoint unrecovered-panic (enabled) at 0x1033180 for runtime.fatalpanic() /Users/qinshixian/.g/go/src/runtime/panic.go:1271 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x10acd6f for main.main() ./a.go:8 (1)
Breakpoint 2 (enabled) at 0x10ac1e6 for strings.Contains() /Users/qinshixian/.g/go/src/strings/strings.go:61 (0)
(dlv)
4、示例代码
go version:go 1.14.15
package main
func main() {
n := 10
println(read(&n))
}
//go:noinline
func read(p *int) (v int) {
v = *p
return
}
- //go:noinline:禁止Go对函数进行内联
- 内联:内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体本身。这么做的原因是它可以消除函数调用本身的开销,也使得编译器能更高效地执行其他的优化策略
- 使用objdump工具反编译
5、结合ChatGPT反编译调试
- 反汇编的两种命令
go tool compile -S a.go
或
go tool compile -N -l a.go
go tool objdump a.o
Tips:两者的区别是汇编格式不一样,-N -l加上去除优化和内联选项
- 对示例代码反编译
go build main.go //编译
go tool objdump -S -s "main.read" main //反编译
- 图中是用go自带的objdump工具对main.read反编译得到的汇编指令
- 不懂就问,咱们直接问ChatGPT这段汇编指令的含义(首先需要给ChatGPT一些引导和背景介绍),如下图
- 上面解释的很清楚了,为了进一步理解这些指令的含义,追问
6、Go代码原理剖析实战
下面通过列举一些例子,结合ChatGPT来辅助深入理解Go代码实际运行机制,以后遇到类似困惑的时候,都可以通过该方法了解其中的原理。
6.1 指针
- 指针本身是一个无符号整型
go version:go 1.14.15
package main
func main() {
n := int32(10)
println(read32(&n))
}
//go:noinline
func read32(p *int32) (v int32) {
v = *p
return
}
可以看到上述汇编指令中,第一行从参数p中取地址值的操作没变化,只是从AX寄存器中取值的时候,命令有MOVQ(8字节)变为MOVL(4字节),可见不同类型的指针地址本身是一样的类型(无符号整型) 。
- 取地址
go version:go 1.14.15
package main
var n int
func main() {
println(addr())
}
//go:noinline
func addr() (p *int) {
return &n
}
直接问ChatGPT,给出的解释是:
- 从上图可以看到全局变量n是存在main包的静态基地址上(SB),被不同的函数和代码块共享访问,SB 是静态基地址的缩写,它是指向静态基地址的寄存器。因此,"main.n(SB)" 就是通过 SB 指向 main 包的静态基地址上的 n 变量。
- LEAQ 指令将全局变量 n 的有效地址存储到 AX 寄存器中,这样 AX 寄存器就包含了 n 变量的地址,可以用于读取或写入该变量的值。
LEAQ 指令用于将有效地址存储到一个寄存器中
强制类型转换
go version:go 1.14.15
package main
import "unsafe"
func main() {
p := 3
convert(&p)
}
//go:noinline
func convert(p *int) {
q := (*int32)(unsafe.Pointer(p))
*q = 0
}
- 这段汇编代码是将一个指向int类型变量的指针,转换为指向int32类型变量的指针,并将其所指向的内存空间的值设置为0
把指针的类型强转换为int32后,原本的MOVQ指令变成了MOVL,没有产生任何额外指令,所以转换效率是非常高的
6.2go 函数调用栈
程序的基本分段
.data : 有初始化值的全局变量;定义常量。 .bss : 没有初始化值的全局变量。 .text : 代码段。 .rodata: 只读数据段。
CALL 和 RET 作用
当 A 函数调用 B 函数时,编译器就会对应生成一条 CALL 指令,程序执行到 CALL 指令时,就会跳转到被调用函数的入口处开始执行,每个被调用的函数执行结束会有一条 RET 指令,负责函数结束后跳回调用处。go语言函数调用规约
caller
+------------------+
| |
+----------------------> --------------------
| | |
| | caller parent BP |
| BP(pseudo SP) --------------------
| | |
| | Local Var0 |
| --------------------
| | |
| | ....... |
| --------------------
| | |
| | Local VarN |
--------------------
caller stack frame | |
| callee arg2 |
| |------------------|
| | |
| | callee arg1 |
| |------------------|
| | |
| | callee arg0 |
| ----------------------------------------------+ FP(virtual register)
| | | |
| | return addr | parent return address |
+----------------------> +------------------+--------------------------- <-------------------------------+
| caller BP | |
| (caller frame pointer) | |
BP(pseudo SP) ---------------------------- |
| | |
| Local Var0 | |
---------------------------- |
| |
| Local Var1 |
---------------------------- callee stack frame
| |
| ..... |
---------------------------- |
| | |
| Local VarN | |
SP(Real Register) ---------------------------- |
| | |
| | |
| | |
| | |
| | |
+--------------------------+ <-------------------------------+
callee
-----------------
current func arg0
----------------- <----------- FP(pseudo FP)
caller ret addr
+---------------+
| caller BP(*) |
----------------- <----------- SP(pseudo SP,实际上是当前栈帧的 BP 位置)
| Local Var0 |
-----------------
| Local Var1 |
-----------------
| Local Var2 |
----------------- -
| ........ |
-----------------
| Local VarN |
-----------------
| |
| |
| temporarily |
| unused space |
| |
| |
-----------------
| call retn |
-----------------
| call ret(n-1)|
-----------------
| .......... |
-----------------
| call ret1 |
-----------------
| call argn |
-----------------
| ..... |
-----------------
| call arg3 |
-----------------
| call arg2 |
|---------------|
| call arg1 |
----------------- <------------ hardware SP 位置
| return addr |
+---------------+
函数执行时需要有足够的内存空间,供它存放局部变量、参数、返回值,这段空间对应到虚拟地址空间的栈。
- 示例代码
go version:go 1.17.2
package main
import "fmt"
func main() {
a,b := 1,2
c := sum(a,b)
fmt.Println(c)
}
func sum(x,y int) (z int) {
z = x + y
return z
}
- 入栈和出战规则
6.3go defer、panic、recover
- panic 能立即终止程序,并在当前 Goroutine 中递归执行调用方的 defer
- recover 可以终止 panic 造成的程序崩溃,并且它只能在defer中起到作用
_panic结构体
_defer结构体
defer 示例代码调试
go version:go 1.14.15
下面这段关于defer的go代码,你知道defer是如何影响incr函数中的变量a和变量b的吗?
package main import "fmt" func main(){ a,b := 1,2 c := incr(a,b) fmt.Println(c) } func incr(m,n int) int { var b int a := m + n defer func(x int) { a = a + x b++ }(a + 1) a = 99 b = a return b }
函数调用栈示意图:
反编译调试:
go tool compile -N -l main.go
go tool objdump mian.o
结合上面的分析incr函数中的变量a和变量b的分析如下:
(1)incr中变量a作为局部变量,值是 m + n 等于 3;
(2)在入栈给的时候,传给 x 的值是 a+1 等于 4;
(3)给a赋值99,a的值变为99;
(4)给b赋值,b的值为99;
(5)赋值给返回地址,取b的值 99;
(6)执行defer函数,a 的值为 a + x,就是 99 + 4,a的值变为 103;
(7)b++,b的值变为100(注意在此之前b的值为99时,已经赋给返回地址了);
综上所述,main函数中的c最终的打印值是 99。
Tips:go函数defer执行时是先给返回值地址赋值,再执行defer
- panic 正常执行 defer
go version:go 1.14.15
package main
import "fmt"
func main() {
defer A()
defer B()
defer C()
panic("panic A")
fmt.Println("func main")
}
func A() {
fmt.Println("func A")
}
func B() {
fmt.Println("func B")
}
func C(){
fmt.Println("func C")
}
使用如下命令可以查看对应的底层汇编和函数调用栈信息:
go tool compile -N -l t1.go
go tool objdump t1.o
可以看出来遇到 panic
时,底层调用的是 runtime.gopanic
,其对应的函数做如下事情:
- (1)往Goroutine的panic链表表头插入panic
- (2)循环执行Goroutine的_defer链表
- (3)_panic 执行 defer时会先把 started 字段置为 true
(4)把 _defer 结构体 _panic 字段指针指向当前的执行 _panic 的结构体
情况一:panic 正常执行defer,此时被正常执行的_defer节点会被移除 情况二:panic 执行 defer 中有 panic
panic 执行 defer 中有 panic
go version:go 1.14.15
package main
import "fmt"
func main() {
defer A()
defer B()
defer C()
panic("panic A")
fmt.Println("func main")
}
func A() {
fmt.Println("func A")
}
func B() {
panic("panic B")
fmt.Println("func B")
}
func C(){
fmt.Println("func C")
}
- 该例子前面步骤和例子 t1.go 中一样,defer C节点被正常执行会被移除
- 执行到 defer B 的时候,该defer 节点B.startred=true,B.panic = &panicB, 并且 func B() 函数中有 panic,会调用底层 runtime.gopanic,执行和t1.go中相同的动作
- func B() 中的 panicB 会向 goroutine 中的 _panic 链表头部插入 panicB 节点
- panicB 会遍历 defer 链表,并且遍历到第一个 defer B 的时候,发现 defer B.startred 字段已经被设置为 true(是之前由panicA 设置的),此时就会把defer B.panic 指向的 panicA 结构体中的 aborted 字段设置为true,表示 panicA 被终止了
- 移除 defer B节点,继续正常执行 defer A
- 接下来打印 panic 信息,_panic 链表是从链表尾部向头部打印的,所以是先打印 panicA,在打印 panicB的信息
- panic 执行 defer 中有recover
go version:go 1.14.15
package main
import "fmt"
func main() {
defer A()
defer B()
defer C()
panic("panic A")
fmt.Println("func main")
}
func A() {
x := recover()
fmt.Println("func A,recover:",x)
}
func B() {
panic("panic B")
fmt.Println("func B")
}
func C(){
fmt.Println("func C")
}
- 该例子中前面步骤参考 t1.go 和 t2.go,最后panicB 在遍历defer A 节点时,触发 func A() 函数,该函数中,有recover函数
- recover() 函数只做一件事,把当前执行的panic节点的 recovered字段设置为true,并且此时recover()函数捕获到的信息为panicB的信息,即x=”panic B”,该例子中 func A() 函数中的 recover 设置的是 panicB节点中的 recovered字段,此时panicB节点已经被恢复,就会被移除,移除后会跳出panicB的处理流程,接着panicA的处理流程
- panic 执行defer中有recove&panic
go version:go 1.14.15
package main
import "fmt"
func main() {
defer A()
defer B()
defer C()
panic("panic A")
fmt.Println("func main")
}
func A() {
x := recover()
fmt.Println("func A,recover:",x)
panic("panic R")
}
func B() {
panic("panic B")
fmt.Println("func B")
}
func C(){
fmt.Println("func C")
}
- 该例子中前面步骤参考 t1.go、t2.go、t3.go,当执行到 recover 函数时,panic B被recover()函数设置为已回复,但recover()函数后面还有 panicR
- panicR 会在goroutine 头部插入 panic 节点,并且 panicR 也会执行底层 runtime.gopanic函数,遍历defer 链表,发现此时 defer A startred 为 true,且会把 defer A链表指向的 panic B设置为已终止,并且移除 defer A,此时 panic B 的 aborted 和 recovered 都是true
- 接下来就是打印 panic 信息,和之前一样,_panic 链表也是从链表尾部开始的,首先打印 panic A的信息,由于 panic B被恢复了,所以先打印 panic B的信息时候,会提示[recovered],然后打印 panic R。
7、小结
上面只是介绍如何用ChatGPT辅助学习,通过细节追问可以更加深入的了解Go语言原理,ChatGPT拥有大量的知识和信息,可以提供关于Go语言底层原理的详细解释和示例,也可以帮助我们理解底层原理的概念、工作原理和实际应用。我们也可以根据自己的兴趣和理解程度向ChatGPT提问,它支持多轮对话,和它进行交互式的学习,这种交互式学习方式可以帮助我们更好地理解和应用Go语言底层原理。