安装多版本的 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 开始,每条指令是相对函数第一条指令的偏移。而链接后所有指令都有了全局唯一的地址:
编译与反编译⼯具
编译
该命令会生成 .o
目标文件,并把目标的汇编内容输出:
例如要对比以下两段代码的执行效率,一种方法就是使用go tool compile
来进行测试,本例会发现两者在框出部分输出的汇编是一致的:
反编译
go tool objdump
查找 make 的实现 (https://go.dev/ref/spec,官⽅ spec),make.go 内容如下:
执行命令go build make.go && go tool objdump ./make | grep -E "make.go:6|make.go:10|make.go:14"
,得到的结果如下:
接下来可进一步查看runtime.makeslice
、runtime.makechan
以及runtime.makemap
来进行分析。
使⽤调试⼯具
https://github.com/go-delve/delve/tree/master/Documentation/cli
- 调试汇编时 使用
si
到 JMP 目标位置 - 使用
c
(continue) 从一个断点到 下一个断点 - 用
disass
反汇编
语法实现分析
go func
通过go build hello.go && go tool objdump ./hello | grep "hello.go:6"
命令可得到
可查看其中的 runtime 函数
channel send && recv
关键点就是 chansend1 和 chanrecv1 函数
非阻塞 recv
使用go tool compile -S nonblock_recv.go | grep nonblock_recv.go:6
得到
可进一步查看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
- “龙书”、“虎书”,新手易劝退