Go 语言,如何做逆向类型推导?

dcb911c0a4ebec5cf79384a2f7afe703.gif

文|朱德江(GitHub ID:doujiang24)

MOSN 项目核心开发者

蚂蚁集团技术专家

a37d071aa3f01586b41e615538515430.png

专注于云原生网关研发的相关工作。

本文 2241 字 阅读 8 分钟

PART. 1

引言

在上回的文章《Go 内存泄漏,pprof 够用了么?》中说到,从一个 core 文件生成内存引用关系火焰图时,虽然可以从 core 文件中读到所有的内存对象,但是并不知道它们的类型信息。

这是因为 Go 作为静态类型语言,在运行时,内存对象的类型是已知的。也就是说,并不需要想动态类型语言那样,为每个内存对象在内存中存储其类型信息(有点例外的是 interface)

比如这个 Go 语言例子:

type Foo struct {
    a uint64
    b int64
}


func foo(f *Foo) int64 {
    return f.b
}

Foo 函数在使用 f 这个指针时,并不需要判断其类型,直接读一个带偏移量地址就能得到 f.b,也就是一条指令:mov rax, qword ptr [rax + 8],就是这么简单直接。

再看 Lua 语言这个例子:

function foo(f)
    return f.b
end
foo({ b = 1 })

Foo 函数在执行的时候,首先得判断 f 的类型,如果是 table,则按照 key 取 b 的值;如果不是,则抛运行时 error。

能够运行时判断 f 的类型,是因为 Lua 中变量是用 TValue 来表示的,这个 TValue 结构中,就有一个信息用来存储变量类型。

PART. 2

逆向类型推导

逆向类型推导的逻辑是:根据已知内存的类型信息,推导被引用的内存对象的类型信息。

比如这个例子:

type Foo struct {
    a uint64
    b int64
}
type Bar struct {
    f *Foo
}
var b Bar

如果我们知道了 b 的类型是 Bar,那么 b  中第一个 field 指向的内存对象,就是 Foo 类型了(前提是合法的内存对象地址)

既然存在推导,那我们怎么知道一些初始值呢?

一共有两类来源:

1.全局变量;

2.协程中每一帧函数的局部变量。

PART. 3

全局变量

Go 在编译的时候,默认会生成一些调试信息,按照 DWARF 标准格式,放在 ELF 文件中 .debug_* 这样的段里。

这些调试信息中,我们关注两类关键信息:

  1. 类型信息:包括了源码中定义的类型,比如某个 struct 的名字、大小、以及各个 field 类型信息;

  2. 全局变量:包括变量名、地址、类型,调试信息中的、全局变量的地址、以及其类型信息,也就是构成推导的初始值。

函数局部变量,要复杂一些,不过基本原理是类似的,这里就不细说了~

PART. 4

推导过程

推导过程,跟 GC-Mark 的过程类似,甚至初始值也跟 GC-Root 一样。

所以,全部推导完毕之后,GC-Mark 认为是 alive 的内存对象,其类型信息都会被推导出来。

interface

Go 语言中 interface 比较类似动态类型,如下是空接口的内存结构,每个对象都存储了其类型信息:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

按照类型推导,我们能知道一个对象是 interface{},但是其中 Data 指向对象,是什么类型,我们则需要读取 _type 中的信息了。

_type 中有两个信息,对我们比较有用:

1.名字

不过比较坑的是,只存了 pkg.Name 并没有存完整的 Include Path 这个也合理的,毕竟 Go 运行时并不需要那么精确,也就是异常时,输出错误信息中用一下。不过在类型推导的时候,就容易踩坑了。

2.指针信息

具体存储形式有点绕,不过也就是表示这个对象中,有哪些偏移量是指针。

有了这两个信息之后,就可以从全量的类型中,筛选出符合上面两个信息的类型。

通常情况下,会选出一个正确的答案,不过有时候选出多个,仅仅根据这两个信息还不能区分出来,一旦一步错了,后面可能就全推导不出来了。

我们给 Go 官方 Debug 贡献了一个补丁,可以进一步的筛选,有兴趣的可以看 CL 419176[1]

unsafe.pointer

其实,在上面的 interface 示例中,最根源的原因,也就是 data unsafe.pointer,这个指针并没有类型信息,只是 interface 的实现中,有另外的字段来存储类型信息。

不过,在 Go Runtime 中还有其它的 unsafe.pointer,就没有那么幸运了。

比如 map 和 sync.map 的实现都有 unsafe.pointer,这种就没有办法像 interface 那样统一来处理了,只能 case-by-case,根据 map/sync.map 的结构特征来逆向写死了...

我们给 Go 官方 Debug 贡献了 sync.map 的逆向实现,有兴趣的可以看 CL 419177[2]

PART. 5

隐藏类型

除了源码中显示定义的类型,还有一些隐藏的类型,比如:Method ValueClosure 的实现中,也都是用 struct 来表示的,这些属于不太容易被关注到的“隐藏”类型。

Method Value 在逆向推导中,还是比较容易踩坑的,我们给 Go 官方 Debug 贡献了这块的实现,有兴趣的可以看 CL 419179[3]

相比 Method Value 这种固定结构的,Closure 这种会更难搞一些,不过幸运的是,我们目前的使用过程中,还没有踩坑的经历。

PART. 6

逆向推导风险

这种逆向推导要做到 100% 完备还是挺难的,根本原因还是 unsafe.pointer

reflect.Value  中也有 unsafe.pointer,据我所知,这个是还没有逆向推导实现的,类似的应该也还有其它未知的。

甚至,如果是标准库中的类型,我们还是可以一个个按需加上,如果是上层应用代码用到的 unsafe.pointer,那就很难搞了。

还有一种可能,推导不出来的原因,就是内存泄漏的来源,我们就碰到这样一个例子,以后有机会再分享~

幸运的是:如果是只是少量的对象没有推导出来,对于全局内存泄漏分析这种场景,通常影响其实也不大。

另外,对于一个对象,只需要有一个路径可以推导出来也就够了。

也就是说,如果一条推导线索因为 unsafe.pointer 断了,如果另外有一个线索可以推导到这个对象,那也是不影响的。因为从 GC root 到一个 GC obj 的引用关系链,可能会不止一条。

PART. 7

小结

Go 虽然是静态类型语言,不过由于提供了 unsafe.pointer,给逆向类型推导带来了很大的麻烦。好在 Go 对于 unsafe.pointer 的使用还是比较克制,把标准库中常用到的 unsafe.pointer 搞定了,基本也够用了。

理论上来说,逆向推导这一套也适用于 C 语言,只不过 C 语言这种指针漫天飞的,动不动就来个强制类型转换,就很难搞了。

|相关链接|

[1]CL 419176:
https://go-review.googlesource.com/c/debug/+/419176

[2]CL 419177:

https://go-review.googlesource.com/c/debug/+/419177

[3]CL 419179:

https://go-review.googlesource.com/c/debug/+/419179

了解更多...

MOSN Star 一下✨:
https://github.com/mosn/mosn

推荐观看...

  本周推荐阅读  

e29ca97bc6e0ea897dbb2285ec8d0d2b.jpeg

MOSN 反向通道详解

4d0619e716676ad5a5c551b5732a2486.png

Go 原生插件使用问题全解析

c8c8548aa54112a9cf8fcd2c510005d0.png

Go 内存泄漏,pprof 够用了么?

2ca45486bc06eba95de09b3d676e233f.png

MOSN 文档使用指南

4ae70240cca649052208cd38e13cd0e5.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值