不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Go高级工程师_第2课_语法背后的秘密

编译原理基础

业务场景

下面两段程序,哪个快一些?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type person struct {
age int
}

//case 1
func main() {
var b = person{111}
var a = &b
println(a)
}

//case 2
func main() {
var b = person{111}
var a = &b
println(a)
}

类型转换是啥原理?

1
2
3
4
5
func main() {
var a = "hello"
var b = []byte(a)
println(b)
}

怎么找make和new这些函数的具体实现?

会员服务,给用户分等级

  • 初级会员,发帖数>10;
  • 中级会员,充值>1000人民币;
  • 高级会员,发帖数>100,充值>10000人民币。

如果项目数 = 几百,每个项目都有自己的会员规则,怎么办?

封装统一的数值查询服务

用户提供查询条件,会经常变,代理去不同的模块查数据,外部模块没有统一的数据获取规范。

每次我们的用户提了需求,我们就一定要写一遍代码吗?

公司想从Thrift切换到gRPC

已经有了大量的Thrift IDL,想提供gRPC接口,手工把Thrift IDL抄写成pb文件效率太低,怎么办?

SQL审计

我是SQL专家,我知道怎么获取到表的索引,我可以把用户代码里的SQL扫描出来…
我想在上线的时候能自动做一些拦截,提醒用户去给表加索引,怎么办?

公司卖出去的服务软件,客户要自己定制,但不想给客户源码

要支持客户写的扩展代码能在我们的模块上运行。

编译过程

词法分析

1
2
3
func main() {
println(1 + 2)
}

语法分析

语义分析

1
2
3
4
func main() {
var x int = "abc"
println(x)
}

在抽象语法树AST上做类型检查

中间代码(SSA)生成与优化

1
2
3
4
func main() {
c := 8
return a*4 + b*c
}

SSA的两大要点:

  • Static:每个变量只能赋值一次;
  • Single:每个表达式只能做一个简单运算,对于复杂的表达式a*b+c*d要拆分成图片里的形式。

机器码生成

虚拟地址重定位

编译与反编译⼯具

编译

1
2
3
4
5
6
7
8
//hello.go
package main

func main() {
var a = "hello"
var b = []byte(a)
println(b)
}

命令行输入go tool compile -S ./hello.go | grep "hello.go:5"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
"".main STEXT size=109 args=0x0 locals=0x58 funcid=0x0
0x0000 00000 (./hello.go:3) TEXT "".main(SB), ABIInternal, $88-0
0x0000 00000 (./hello.go:3) CMPQ SP, 16(R14)
0x0004 00004 (./hello.go:3) PCDATA $0, $-2
0x0004 00004 (./hello.go:3) JLS 102
0x0006 00006 (./hello.go:3) PCDATA $0, $-1
0x0006 00006 (./hello.go:3) SUBQ $88, SP
0x000a 00010 (./hello.go:3) MOVQ BP, 80(SP)
0x000f 00015 (./hello.go:3) LEAQ 80(SP), BP
0x0014 00020 (./hello.go:3) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0014 00020 (./hello.go:3) FUNCDATA $1, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x0014 00020 (./hello.go:5) LEAQ ""..autotmp_2+40(SP), AX
0x0019 00025 (./hello.go:5) LEAQ go.string."hello"(SB), BX
0x0020 00032 (./hello.go:5) MOVL $5, CX
0x0025 00037 (./hello.go:5) PCDATA $1, $0
0x0025 00037 (./hello.go:5) CALL runtime.stringtoslicebyte(SB) #var b = []byte(a)
0x002a 00042 (./hello.go:6) MOVQ AX, "".b.ptr+72(SP)
0x002f 00047 (./hello.go:6) MOVQ BX, "".b.len+24(SP)
0x0034 00052 (./hello.go:6) MOVQ CX, "".b.cap+32(SP)
0x0039 00057 (./hello.go:6) PCDATA $1, $1
0x0039 00057 (./hello.go:6) CALL runtime.printlock(SB)
0x003e 00062 (./hello.go:6) MOVQ "".b.ptr+72(SP), AX
0x0043 00067 (./hello.go:6) MOVQ "".b.len+24(SP), BX
0x0048 00072 (./hello.go:6) MOVQ "".b.cap+32(SP), CX
0x004d 00077 (./hello.go:6) PCDATA $1, $0
0x004d 00077 (./hello.go:6) CALL runtime.printslice(SB)
0x0052 00082 (./hello.go:6) CALL runtime.printnl(SB)
0x0057 00087 (./hello.go:6) CALL runtime.printunlock(SB)
0x005c 00092 (./hello.go:7) MOVQ 80(SP), BP
0x0061 00097 (./hello.go:7) ADDQ $88, SP
0x0065 00101 (./hello.go:7) RET
0x0066 00102 (./hello.go:7) NOP
0x0066 00102 (./hello.go:3) PCDATA $1, $-1
0x0066 00102 (./hello.go:3) PCDATA $0, $-2
0x0066 00102 (./hello.go:3) CALL runtime.morestack_noctxt(SB)
0x006b 00107 (./hello.go:3) PCDATA $0, $-1
0x006b 00107 (./hello.go:3) JMP 0
0x0000 49 3b 66 10 76 60 48 83 ec 58 48 89 6c 24 50 48 I;f.v`H..XH.l$PH
0x0010 8d 6c 24 50 48 8d 44 24 28 48 8d 1d 00 00 00 00 .l$PH.D$(H......
0x0020 b9 05 00 00 00 e8 00 00 00 00 48 89 44 24 48 48 ..........H.D$HH
0x0030 89 5c 24 18 48 89 4c 24 20 e8 00 00 00 00 48 8b .\$.H.L$ .....H.
0x0040 44 24 48 48 8b 5c 24 18 48 8b 4c 24 20 e8 00 00 D$HH.\$.H.L$ ...
0x0050 00 00 e8 00 00 00 00 e8 00 00 00 00 48 8b 6c 24 ............H.l$
0x0060 50 48 83 c4 58 c3 e8 00 00 00 00 eb 93 PH..X........
rel 28+4 t=15 go.string."hello"+0
rel 38+4 t=7 runtime.stringtoslicebyte+0
rel 58+4 t=7 runtime.printlock+0
rel 78+4 t=7 runtime.printslice+0
rel 83+4 t=7 runtime.printnl+0
rel 88+4 t=7 runtime.printunlock+0
rel 103+4 t=7 runtime.morestack_noctxt+0
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
0x0000 6d 61 69 6e main
""..inittask SNOPTRDATA size=24
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 00 00 00 00 00 00 00 00 ........
go.string."hello" SRODATA dupok size=5
0x0000 68 65 6c 6c 6f hello
gclocals·69c1753bd5f81501d95132d08af04464 SRODATA dupok size=8
0x0000 02 00 00 00 00 00 00 00 ........
gclocals·9fb7f0986f647f17cb53dda1484e0f7a SRODATA dupok size=10
0x0000 02 00 00 00 01 00 00 00 00 01 ..........

使⽤调试⼯具

调试汇编时,使用SI到JMP目标位置

使用c(continue)从上一个断点到下一个断点

用disass反汇编

官方文档

语法实现分析

go func

1
2
3
4
5
6
7
8
9
import "time"

func main() {
go func() {
println("hello world")
}()

time.Sleep(time.Second * 5)
}

channel send & recv

1
2
3
4
5
6
7
func main() {
var a = make(chan int, 1)
a <- 666

x := <-a
println(x)
}

非阻塞recv

1
2
3
4
5
6
7
func main() {
var ch1 = make(chan int)
select {
case <- ch1:
default
}
}

找到这张图中所有报错的内部代码位置

1
2
3
4
5
func main() {
var ch chan int
close(ch)
ch <- 1
}

运行:

1
2
3
4
5
6
7
$ go run hello.go 
panic: close of nil channel

goroutine 1 [running]:
main.main()
/home/neo/Code/go/src/test/hello.go:5 +0x1b
exit status 2

Parser 场景示例

内置AST工具

简单的规则引擎

从Go的注释到swagger

使用社区Parser

从Thrift切换到gRPC

SQL审计

  • Vitess
  • PingCAP

统一数据接入服务

客户定制需求

函数调⽤规约

函数栈

为什么Go可以一个函数多个返回值

局部变量,只要不逃逸,都在栈上分配空间。

当我们调用其它函数时:

callerBP

函数调用规约:

  • The order in which atomic parameters,or individual parts of a complex parameters,are allocated
  • How parameters are passed(push 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.

References

Go 的词法分析和语法/语义分析过程
编译器各阶段的简单介绍
Linkers and loaders,只看内部对 linker 的职责描述就⾏,不⽤看原理
SSA 的简单介绍(*只做了解)
⽼外的写的如何定制 Go 编译器,⾥⾯对 Go 的编译过程介绍更详细,SSA 也说明得很好,只做了解
如何阅读 go 的 SSA,难,只做了解
CMU 的编译器课,讲 SSA,难,只做了解
对逆向感兴趣的话扩展内容
Vitess 的 SQL Parser
PingCAP 的 TiDB 的 SQL Parser
GoCN 上的 dlv 的新译⽂
[C语⾔调⽤规约](https://github.com/cch123/llp-trans/blob/master/part3/translation-details/function-calling-sequence/callingconvention.md)
Go 语⾔新版调⽤规约

其它说明

  • C 语⾔的调⽤规约是寄存器与栈混合使⽤的,Go 语⾔⽬前只使⽤了栈(1.17 中会有⽤上寄存器的新⽅式)
  • 有⼀些⽐较奇怪的问题,可以⽤SSA的⽹⻚来看到底是哪个阶段导致的,⽐如:https://xargin.com/addr-of-empty-struct-may-noteq/,对于⼤多数 Gopher 只要知道这件事情是优化阶段⼲的就可以了,不需要研究明⽩优化每个阶段都在⼲什么
  • Go 语⾔的参数和返回值都是 caller 提供空间,在callee中需要根据FP寄存器找到位置,需要⼀定的汇编知识。
------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!