在当今的信息时代,编程语言已经成为了科技领域的基础工具之一。其中,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
AI 代码解读
- 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("字符串中不包含","世界") } }
AI 代码解读
使用 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)
AI 代码解读
4、示例代码
go version:go 1.14.15
AI 代码解读
package main func main() { n := 10 println(read(&n)) } //go:noinline func read(p *int) (v int) { v = *p return }
AI 代码解读
- //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加上去除优化和内联选项
AI 代码解读
- 对示例代码反编译
go build main.go //编译 go tool objdump -S -s "main.read" main //反编译
AI 代码解读
- 图中是用go自带的objdump工具对main.read反编译得到的汇编指令
- 不懂就问,咱们直接问ChatGPT这段汇编指令的含义(首先需要给ChatGPT一些引导和背景介绍),如下图
- 上面解释的很清楚了,为了进一步理解这些指令的含义,追问
6、Go代码原理剖析实战
下面通过列举一些例子,结合ChatGPT来辅助深入理解Go代码实际运行机制,以后遇到类似困惑的时候,都可以通过该方法了解其中的原理。
6.1 指针
- 指针本身是一个无符号整型
go version:go 1.14.15
AI 代码解读
package main func main() { n := int32(10) println(read32(&n)) } //go:noinline func read32(p *int32) (v int32) { v = *p return }
AI 代码解读
可以看到上述汇编指令中,第一行从参数p中取地址值的操作没变化,只是从AX寄存器中取值的时候,命令有MOVQ(8字节)变为MOVL(4字节),可见不同类型的指针地址本身是一样的类型(无符号整型) 。
- 取地址
go version:go 1.14.15
AI 代码解读
package main var n int func main() { println(addr()) } //go:noinline func addr() (p *int) { return &n }
AI 代码解读
直接问ChatGPT,给出的解释是:
- 从上图可以看到全局变量n是存在main包的静态基地址上(SB),被不同的函数和代码块共享访问,SB 是静态基地址的缩写,它是指向静态基地址的寄存器。因此,"main.n(SB)" 就是通过 SB 指向 main 包的静态基地址上的 n 变量。
- LEAQ 指令将全局变量 n 的有效地址存储到 AX 寄存器中,这样 AX 寄存器就包含了 n 变量的地址,可以用于读取或写入该变量的值。
LEAQ 指令用于将有效地址存储到一个寄存器中
强制类型转换
go version:go 1.14.15
AI 代码解读
package main import "unsafe" func main() { p := 3 convert(&p) } //go:noinline func convert(p *int) { q := (*int32)(unsafe.Pointer(p)) *q = 0 }
AI 代码解读
- 这段汇编代码是将一个指向int类型变量的指针,转换为指向int32类型变量的指针,并将其所指向的内存空间的值设置为0
把指针的类型强转换为int32后,原本的MOVQ指令变成了MOVL,没有产生任何额外指令,所以转换效率是非常高的
6.2go 函数调用栈
程序的基本分段
.data : 有初始化值的全局变量;定义常量。 .bss : 没有初始化值的全局变量。 .text : 代码段。 .rodata: 只读数据段。
AI 代码解读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 | +---------------+
AI 代码解读
函数执行时需要有足够的内存空间,供它存放局部变量、参数、返回值,这段空间对应到虚拟地址空间的栈。
- 示例代码
go version:go 1.17.2
AI 代码解读
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 }
AI 代码解读
- 入栈和出战规则
6.3go defer、panic、recover
- panic 能立即终止程序,并在当前 Goroutine 中递归执行调用方的 defer
- recover 可以终止 panic 造成的程序崩溃,并且它只能在defer中起到作用
_panic结构体
_defer结构体
defer 示例代码调试
go version:go 1.14.15
AI 代码解读下面这段关于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 }
AI 代码解读函数调用栈示意图:
反编译调试:
go tool compile -N -l main.go go tool objdump mian.o
AI 代码解读
结合上面的分析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
AI 代码解读
- panic 正常执行 defer
go version:go 1.14.15
AI 代码解读
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") }
AI 代码解读
使用如下命令可以查看对应的底层汇编和函数调用栈信息:
go tool compile -N -l t1.go go tool objdump t1.o
AI 代码解读
可以看出来遇到 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
AI 代码解读panic 执行 defer 中有 panic
go version:go 1.14.15
AI 代码解读
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") }
AI 代码解读
- 该例子前面步骤和例子 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
AI 代码解读
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") }
AI 代码解读
- 该例子中前面步骤参考 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
AI 代码解读
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") }
AI 代码解读
- 该例子中前面步骤参考 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语言底层原理。