安装多版本的 Go:https://golang.org/doc/manage-install
- 编译原理基础
- 编译与反编译⼯具
- 使⽤调试⼯具
- 语法实现分析
- Parser 应⽤场景示例
- 函数调⽤规约
编译原理基础
编译过程
词法分析(Lexical Analysis)
语法分析(Syntax Analysis)
语义分析(Semantic Analysis)
在抽象语法树 AST 上做类型检查
中间代码(SSA)⽣成与优化
https://golang.design/gossa,或者GOSSAFUNC=funcname go build x.go
SSA(Single Static Assignment)的两大要点是:
- Static: 每个变量只能赋值一次(因此应该叫常量更合适);
- Single: 每个表达式只能做一个简单运算,对于复杂的表达式
a*b+c*d
要拆分成:t0=a*b; t1=c*d; t2=t0+t1;
三个简单表达式;
机器码⽣成
编译的最后一个阶段是机器码:
链接过程
最重要的就是进行虚拟地址重定位(Relocation)。编译后所有函数地址都是从 0 开始,每条指令是相对函数第一条指令的偏移。而链接后所有指令都有了全局唯一的地址:
编译与反编译⼯具
编译
1 |
go tool compile -S ./hello.go | grep “hello.go:5” |
该命令会生成 .o
目标文件,并把目标的汇编内容输出:
例如要对比以下两段代码的执行效率,一种方法就是使用go tool compile
来进行测试,本例会发现两者在框出部分输出的汇编是一致的:
反编译
go tool objdump
查找 make 的实现 (https://go.dev/ref/spec,官⽅ spec),make.go 内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package main func main() { // make slice // 空间开的比较大,是为了让 slice 分配在堆上,栈上的 slice 结果不大一样 var sl = make([]int, 100000) println(sl) // make channel var ch = make(chan int, 5) println(ch) // make map var m = make(map[int]int, 22) println(m) } |
执行命令go build make.go && go tool objdump ./make | grep -E "make.go:6|make.go:10|make.go:14"
,得到的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
make.go:6 0x1054bf8 488d05c1400000 LEAQ runtime.types+16128(SB), AX make.go:6 0x1054bff bba0860100 MOVL $0x186a0, BX make.go:6 0x1054c04 4889d9 MOVQ BX, CX make.go:6 0x1054c07 e8b4acfeff CALL runtime.makeslice(SB) make.go:6 0x1054c0c 4889442428 MOVQ AX, 0x28(SP) make.go:10 0x1054c32 488d05073f0000 LEAQ runtime.types+15744(SB), AX make.go:10 0x1054c39 bb05000000 MOVL $0x5, BX make.go:10 0x1054c3e 6690 NOPW make.go:10 0x1054c40 e87be8faff CALL runtime.makechan(SB) make.go:10 0x1054c45 4889442420 MOVQ AX, 0x20(SP) make.go:14 0x1054c65 488d4c2430 LEAQ 0x30(SP), CX make.go:14 0x1054c6a 440f1139 MOVUPS X15, 0(CX) make.go:14 0x1054c6e 488d542440 LEAQ 0x40(SP), DX make.go:14 0x1054c73 440f113a MOVUPS X15, 0(DX) make.go:14 0x1054c77 488d542450 LEAQ 0x50(SP), DX make.go:14 0x1054c7c 440f113a MOVUPS X15, 0(DX) make.go:14 0x1054c80 488d05795b0000 LEAQ runtime.types+23104(SB), AX make.go:14 0x1054c87 bb16000000 MOVL $0x16, BX make.go:14 0x1054c8c e80f69fbff CALL runtime.makemap(SB) make.go:14 0x1054c91 4889442418 MOVQ AX, 0x18(SP) |
接下来可进一步查看runtime.makeslice
、runtime.makechan
以及runtime.makemap
来进行分析。
使⽤调试⼯具
https://github.com/go-delve/delve/tree/master/Documentation/cli
- 调试汇编时 使用
si
到 JMP 目标位置 - 使用
c
(continue) 从一个断点到 下一个断点 - 用
disass
反汇编
语法实现分析
go func
1 2 3 4 5 6 7 8 9 10 |
package main import "time" func main(){ go func() { println("hello world") }() time.Sleep(time.Second * 5) } |
通过go build hello.go && go tool objdump ./hello | grep "hello.go:6"
命令可得到
1 2 3 4 |
hello.go:6 0x46333d c7042400000000 MOVL $0x0, 0(SP) hello.go:6 0x463344 488d0525d90100 LEAQ 0x1d925(IP), AX hello.go:6 0x46334b 4889442408 MOVQ AX, 0x8(SP) hello.go:6 0x463350 e82b63fdff CALL runtime.newproc(SB) |
可查看其中的 runtime 函数
channel send && recv
关键点就是 chansend1 和 chanrecv1 函数
非阻塞 recv
1 2 3 4 5 6 7 8 9 |
package main func main() { var ch1 = make(chan int) select{ case <- ch1: default: } } |
使用go tool compile -S nonblock_recv.go | grep nonblock_recv.go:6
得到
1 2 3 |
0x003b 00059 (hello.go:6) MOVQ $0, (SP) 0x0043 00067 (hello.go:6) MOVQ AX, 8(SP) 0x0048 00072 (hello.go:6) CALL runtime.selectnbrecv(SB) |
可进一步查看runtime.selectnbrecv
Parser 应⽤场景示例
内置 AST 工具-简单的规则引擎
假设有一个简单的需求,初中高级会员的规则如下(字段posts
表示发帖数,invest
表示充值消费):
- 初级会员,发帖数 > 10
- 中级会员,充值 > 1000 RMB
- 高级会员,发帖数 > 100,充值 > 10000 RMB
社区 Parser-SQL 审计
- Vitess
- PingCAP
SQL Parser 的例子:https://github.com/cch123/elasticsql
如何在交付二进制的同时使我们的 Go 模块具备一定的扩展性?
- RPC,但存在性能问题
- go-plugin,不同版本编译不兼容
- REPL,需要编译原理知识或使用社区 Parser
- WASM,Go 版本不完善
函数调⽤规约
函数栈
为什么 Go 可以一个函数多个返回值?
局部变量只要不逃逸,都在栈上分配空间,从低地址向高地址不断压入栈中
在我们调用其它函数时,参数和返回值都 caller 提供调用空间的,因此可以有多个返回值:
函数调用规约
• The order in which atomic (scalar) parameters, or individual parts of a complex parameter, are allocated
• How parameters are passed (pushed on the stack, placed in registers, or a mix of both)
• Which registers the called function must preserve for the caller (also known as: callee-saved registers or non-volatile registers)
• How the task of preparing the stack for, and restoring after, a function call is divided between the caller and the callee
参考资料
- Go 的词法分析和语法/语义分析过程
- 编译器各阶段的简单介绍
- Linkers and loaders,只看内部对 linker 的职责描述就行,不用看原理
- SSA 的简单介绍(*只做了解)
- 老外的写的如何定制 Go 编译器,里面对 Go 的编译过程介绍更详细,SSA 也说明得很好(*只做了解)
- 如何阅读 go 的 SSA(*难,只做了解)
- CMU 的编译器课,讲 SSA(*难,只做了解)
- 对逆向感兴趣的话(扩展内容,与本课程无关)
- Vitess 的 SQL Parser
- PingCAP 的 TiDB 的 SQL Parser
- GoCN 上的 dlv 的新译文
- C语言调用规约
- Go 语言新版调用规约
补充
编译原理相关书籍:
- 入门
- Crafting Interpreters
- Writing An Interpreter In Go
- “龙书”、“虎书”,新手易劝退