Skip to content

Commit a934dc8

Browse files
Create index.md (#1173)
1 parent 1362c20 commit a934dc8

File tree

1 file changed

+178
-0
lines changed
  • content/zh/blog/Go-language-how-to-do-inverse-type-derivation

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
title: "Go 语言,如何做逆向类型推导?"
3+
authorlink: "https://github.com/sofastack"
4+
description: "Go 语言,如何做逆向类型推导?"
5+
categories: "SOFAStack"
6+
tags: ["MOSN"]
7+
date: 2023-02-14T15:00:00+08:00
8+
cover: "https://mdn.alipayobjects.com/huamei_soxoym/afts/img/A*VV4FR4uvoE4AAAAAAAAAAAAADrGAAQ/original"
9+
---
10+
11+
![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99b9ad6a5aa54a198cef65736b7c1fa5~tplv-k3u1fbpfcp-zoom-1.image)
12+
13+
文|朱德江(GitHub ID:doujiang24)
14+
15+
MOSN 项目核心开发者蚂蚁集团技术专家
16+
17+
*专注于云原生网关研发的相关工作。*
18+
19+
**本文 224 字 阅读 8 分钟**
20+
21+
**PART. 1**
22+
23+
### 引言
24+
25+
在上回的文章[《Go 内存泄漏,pprof 够用了么?》](https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&mid=2247516046&idx=1&sn=c8ed0fbbc18b4377778c2ed06c7332ba&chksm=faa35054cdd4d9425b6780ae5ed1a6b83ab16afd9d870affba350c8002a2c4e2efdb85abc603&token=977879696&lang=zh_CN&scene=21#wechat_redirect)中说到,从一个 core 文件生成内存引用关系火焰图时,虽然可以从 core 文件中读到所有的内存对象,但是并不知道它们的类型信息。
26+
27+
这是因为 Go 作为静态类型语言,在运行时,内存对象的类型是已知的。也就是说,并不需要想动态类型语言那样,为每个内存对象在内存中存储其类型信息 *(有点例外的是 interface)*
28+
29+
比如这个 Go 语言例子:
30+
31+
```go
32+
type Foo struct { a uint64 b int64}
33+
func foo(f *Foo) int64 { return f.b}
34+
```
35+
36+
`Foo` 函数在使用 `f` 这个指针时,并不需要判断其类型,直接读一个带偏移量地址就能得到 `f.b`,也就是一条指令:`mov rax, qword ptr [rax + 8]`,就是这么简单直接。
37+
38+
再看 Lua 语言这个例子:
39+
40+
```go
41+
function foo(f) return f.bendfoo({ b = 1 })
42+
```
43+
44+
`Foo` 函数在执行的时候,首先得判断 `f` 的类型,如果是 `table`,则按照 key 取 `b` 的值;如果不是,则抛运行时 error。
45+
46+
能够运行时判断 `f` 的类型,是因为 Lua 中变量是用 `TValue` 来表示的,这个 `TValue` 结构中,就有一个信息用来存储变量类型。
47+
48+
**PART. 2**
49+
50+
### 逆向类型推导
51+
52+
逆向类型推导的逻辑是:根据已知内存的类型信息,推导被引用的内存对象的类型信息。
53+
54+
比如这个例子:
55+
56+
```go
57+
type Foo struct { a uint64 b int64}type Bar struct { f *Foo}var b Bar
58+
```
59+
60+
如果我们知道了 `b` 的类型是 `Bar`,那么 `b`  中第一个 field 指向的内存对象,就是 `Foo` 类型了 *(前提是合法的内存对象地址)* **
61+
62+
**既然存在推导,那我们怎么知道一些初始值呢?**
63+
64+
一共有两类来源:
65+
66+
1.全局变量;
67+
68+
2.协程中每一帧函数的局部变量。
69+
70+
**PART. 3**
71+
72+
### 全局变量
73+
74+
Go 在编译的时候,默认会生成一些调试信息,按照 DWARF 标准格式,放在 ELF 文件中 `.debug_*` 这样的段里。
75+
76+
这些调试信息中,我们关注两类关键信息:
77+
78+
1. **类型信息:** 包括了源码中定义的类型,比如某个 struct 的名字、大小、以及各个 field 类型信息;
79+
80+
1. **全局变量:** 包括变量名、地址、类型,调试信息中的、全局变量的地址、以及其类型信息,也就是构成推导的初始值。
81+
82+
函数局部变量,要复杂一些,不过基本原理是类似的,这里就不细说了~
83+
84+
**PART. 4**
85+
86+
### 推导过程
87+
88+
推导过程,跟 GC-Mark 的过程类似,甚至初始值也跟 GC-Root 一样。
89+
90+
所以,全部推导完毕之后,GC-Mark 认为是 alive 的内存对象,其类型信息都会被推导出来。
91+
92+
**interface**
93+
94+
Go 语言中 interface 比较类似动态类型,如下是空接口的内存结构,每个对象都存储了其类型信息:
95+
96+
```
97+
type eface struct { _type *_type data unsafe.Pointer}
98+
```
99+
100+
按照类型推导,我们能知道一个对象是 `interface{}`,但是其中 Data 指向对象,是什么类型,我们则需要读取 `_type` 中的信息了。
101+
102+
`_type` 中有两个信息,对我们比较有用:
103+
104+
**1.名字**
105+
106+
不过比较坑的是,只存了 `pkg.Name` 并没有存完整的 Include Path 这个也合理的,毕竟 Go 运行时并不需要那么精确,也就是异常时,输出错误信息中用一下。不过在类型推导的时候,就容易踩坑了。
107+
108+
**2.指针信息**
109+
110+
具体存储形式有点绕,不过也就是表示这个对象中,有哪些偏移量是指针。
111+
112+
有了这两个信息之后,就可以从全量的类型中,筛选出符合上面两个信息的类型。
113+
114+
通常情况下,会选出一个正确的答案,不过有时候选出多个,仅仅根据这两个信息还不能区分出来,一旦一步错了,后面可能就全推导不出来了。
115+
116+
我们给 Go 官方 Debug 贡献了一个补丁,可以进一步的筛选,有兴趣的可以看 CL 419176[1]
117+
118+
**unsafe.pointer**
119+
120+
其实,在上面的 interface 示例中,最根源的原因,也就是 `data unsafe.pointer`,这个指针并没有类型信息,只是 interface 的实现中,有另外的字段来存储类型信息。
121+
122+
不过,在 Go Runtime 中还有其它的 `unsafe.pointer`,就没有那么幸运了。
123+
124+
比如 `map` 和 `sync.map` 的实现都有 `unsafe.pointer`,这种就没有办法像 `interface` 那样统一来处理了,只能 case-by-case,根据 `map/sync.map` 的结构特征来逆向写死了...
125+
126+
我们给 Go 官方 Debug 贡献了 `sync.map` 的逆向实现,有兴趣的可以看 CL 419177[2]
127+
128+
**PART. 5**
129+
130+
### 隐藏类型
131+
132+
除了源码中显示定义的类型,还有一些隐藏的类型,比如:`Method Value``、``Closure` 的实现中,也都是用 `struct` 来表示的,这些属于不太容易被关注到的“隐藏”类型。
133+
134+
`Method Value` 在逆向推导中,还是比较容易踩坑的,我们给 Go 官方 Debug 贡献了这块的实现,有兴趣的可以看 CL 419179[3]
135+
136+
相比 `Method Value` 这种固定结构的,`Closure` 这种会更难搞一些,不过幸运的是,我们目前的使用过程中,还没有踩坑的经历。
137+
138+
**PART. 6**
139+
140+
### 逆向推导风险
141+
142+
这种逆向推导要做到 100% 完备还是挺难的,根本原因还是 `unsafe.pointer`
143+
144+
在 `reflect.Value`  中也有 `unsafe.pointer`,据我所知,这个是还没有逆向推导实现的,类似的应该也还有其它未知的。
145+
146+
甚至,如果是标准库中的类型,我们还是可以一个个按需加上,如果是上层应用代码用到的 `unsafe.pointer`,那就很难搞了。
147+
148+
还有一种可能,推导不出来的原因,就是内存泄漏的来源,我们就碰到这样一个例子,以后有机会再分享~
149+
150+
幸运的是:如果是只是少量的对象没有推导出来,对于全局内存泄漏分析这种场景,通常影响其实也不大。
151+
152+
另外,对于一个对象,只需要有一个路径可以推导出来也就够了。
153+
154+
也就是说,如果一条推导线索因为 `unsafe.pointer` 断了,如果另外有一个线索可以推导到这个对象,那也是不影响的。因为从 `GC root` 到一个 `GC obj` 的引用关系链,可能会不止一条。
155+
156+
**PART. 7**
157+
158+
### 小结
159+
160+
Go 虽然是静态类型语言,不过由于提供了 `unsafe.pointer`,给逆向类型推导带来了很大的麻烦。好在 Go 对于 `unsafe.pointer` 的使用还是比较克制,把标准库中常用到的 `unsafe.pointer` 搞定了,基本也够用了。
161+
162+
理论上来说,逆向推导这一套也适用于 C 语言,只不过 C 语言这种指针漫天飞的,动不动就来个强制类型转换,就很难搞了。
163+
164+
**|相关链接|**
165+
166+
[1]CL 419176:
167+
[https://go-review.googlesource.com/c/debug/+/419176](https://go-review.googlesource.com/c/debug/+/419176)
168+
169+
[2]CL 419177:
170+
[https://go-review.googlesource.com/c/debug/+/419177](https://go-review.googlesource.com/c/debug/+/419177)
171+
172+
[3]CL 419179:
173+
[https://go-review.googlesource.com/c/debug/+/419179](https://go-review.googlesource.com/c/debug/+/419179)
174+
175+
**了解更多...**
176+
177+
**MOSN Star 一下✨:**
178+
[https://github.com/mosn/mosn](https://github.com/mosn/mosn)

0 commit comments

Comments
 (0)