|
| 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 | + |
| 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