Page 19 - 《软件学报》2024年第6期
P. 19
陈金宝 等: DBI-Go: 动态插桩定位 Go 二进制的非法内存引用 2595
息, 便于开发者定位错误 (第 3.3 节).
DBI-Go 基于 Pin [21] 设计实现了上述两个组件, 包括约 1 000 行 C++代码. Pin 是由 Intel 开发的支持 IA-32、
X86-64 和 MIC 指令集架构的动态二进制插桩框架, 可用于实现动态程序分析工具.
3.2 使用静态分析从 Go 二进制中恢复 Go 的 store 指针语义
Go 是编译型语言, Go 程序一旦被编译链接后其二进制代码中所有指令都会固定下来. 因此, 可以通过静态分
析的方式识别 Go 程序二进制代码中的 store 指令. 然而, 若要判定一条 store 指令是否在存储指针 (即规则 1 和规
则 2 中的 store addr dst , addr 形式), 就还需要获取 Go 程序的一些高层语义信息, 如类型等. 下面先分析难点, 然后给
出解决思路及其关键点的实现方法.
3.2.1 难点分析及解决思路
根据 Zeng [35] 的工作, 在 C 语言的二进制中, 所有高级类型信息, 如整型、浮点型和指针类型, 在编译后都会
丢失, 二进制代码中仅有的两种类型是寄存器和内存位置. 在 Go 语言中也类似, Go 二进制程序已经难以区分指
针类型和非指针类型.
GC
• Go 二进制中指针的识别难点. Go 语言中, 指针类型用于传递对象地址, 不能进行指针运算. Go 的 GC 会扫
描指针, 堆指针指向的对象会在合适的时机被 GC 回收. 然而, Go 语言中有一种类型 uintptr [36] , 其大小和普通指针
相同, 可以容纳任意的指针类型的值, 可以用于进行指针运算等操作. 但 GC 并不把 uintptr 当作指针, 因此也不会
基于该指针值进行对象标记. 若把某栈对象的指针转为 uintptr 后存入堆对象, 并在之后不通过它访问对象, 那么
这种存入操作并不违反 Go 的逃逸不变式, 因 Go 并不将 uintptr 视作指针. 然而, Go 二进制中仅有的两种类型是寄
存器和内存位置, 难以判断某个寄存器或者内存位置中的值是指针还是 uintptr. 若对其不做区分, 都视为指针, 则
势必会带来很多误报, 影响精度和效率.
• Go 二进制中地址存入操作的识别难点. 除了类型信息的缺失, Go 二进制上的地址存入操作与用户代码中的
地址存入操作也有较大差别. Go 编译器在编译 Go 程序时会执行若干程序变换, 在用户代码中生成诸多与 Go 运
行时管理相关的代码, 以便 Go 运行时系统对 Go 程序的管理. 在这些代码中会产生若干违反 Go 逃逸不变式 store
操作, 但 Go 的运行时保证了这些操作的安全性. 若不将这些操作排除, 也会带来较多的误报.
• 解决思路. Zhong 等人 [17] 通过 Go 运行时系统中管理并发的相关函数恢复了 Go 二进制上的并发语义. 这启
发我们可以通过静态分析的方式, 识别 Go 二进制用户代码中和内存管理、垃圾回收相关的 Go 运行时函数来恢
复相关的 store 指针的语义. 经过对 Go 运行时相关函数的分析可发现, Go 运行时和写屏障相关的运行时函数可以
用来恢复相应的 store 指令. 第 3.2.2 节中介绍该机制, 并在第 3.2.3 节中介绍如何使用该机制来恢复满足要求的
store 指令并使用 Pin API 为其注册运行时的回调函数.
3.2.2 Go 的写屏障
Go 运行时的不可或缺的部分为垃圾回收 (GC) 系统. 尽管 GC 在幕后运作, 却有数个运行时函数与其息息相
关. 这些运行时函数分为两类: 一部分可供用户主动调用, 用于配置 GC 参数或强制启动新的 GC 周期; 另一部分
由编译器在编译期间自动插入, 在运行时辅助 GC 的运行, 确保 GC 相关内存状态的准确性. 在这个体系中, 编译
器自动插入的 runtime.gcWriteBarrier 函数在维护 相关内存状态的正确性方面扮演着关键角色.
Go 的 GC 系统在堆内存使用达到特定阈值时会中断用户程序的运行, 对那些由根集中的指针直接或间接可
达的对象进行扫描和标记, 标记出仍在生命周期中的对象, 随后释放已经不再使用的对象. 在这个过程中, 用户程
序完全暂停, 因此在逻辑上对 GC 的发生没有感知, 这确保了内存读写不会对 GC 状态造成任何干扰.
Go 的并发 GC 在特定阶段允许用户程序与其并行, 以减少等待时间. 然而, 在 GC 对对象进行标记的过程中,
用户程序的内存读写可能会修改对象的引用关系, 这可能导致 GC 的标记与实际情况不一致, 从而错误地清理正
在使用的对象. 例如, 如果在 GC 完成对栈指针的扫描后, Go 应用程序将某个堆对象的地址存入栈对象中, 而这个
新引用关系的创建没有被 GC 感知到, 即该堆对象没有被 GC 标记, 那么在这一轮 GC 结束后, 该栈对象持有了一
个已经被释放的堆对象地址, 从而导致内存错误. 因此, 在 GC 运行期间, GC 需要获知所有指针类型的内存写入,