Page 12 - 《软件学报》2024年第6期
P. 12
2588 软件学报 2024 年第 35 卷第 6 期
分析算法的错误.
本文第 1 节介绍 Go 逃逸分析及现状, 引出研究动机. 第 2 节主要对 Go 二进制进行抽象并提出两条判定规
则. 第 3 节介绍 DBI-Go 的设计挑战以及其最终的设计与实现, 包括其是如何恢复 Go 二进制上的运行时信息和语
义信息及如何减少误报和降低开销. 第 4 节对 DBI-Go 的漏洞覆盖率、误报率以及带来的额外开销进行实验评估.
第 5 节对 DBI-Go 的局限性进行讨论. 第 6 节介绍相关工作. 第 7 节进行总结与展望.
1 相关基础与研究动机
本节简要介绍 Go 语言的运行时系统和逃逸分析, 并结合社区 issue 概述了目前 Go 逃逸分析的现状, 说明了
目前对逃逸分析正确性验证的不足, 引出本文的研究动机.
1.1 Go 的运行时系统与逃逸不变式
1.1.1 Goroutine 及其栈管理
Go 语言使用协程 Goroutine [22] 作为 Go 程序的执行上下文. Goroutine 是轻量级的用户态线程, 与由操作系统
直接调度的操作系统级线程 Thread 不同, Goroutine 的调度是由 Go 的运行时系统进行管理的. 每个 Goroutine 都
有自己独有的栈, 但它的额外开销和默认栈大小都比线程小很多. 与操作系统线程的栈不同, Goroutine 的栈是 Go
运行时系统使用堆内存来模拟的. Go 运行时系统从操作系统申请堆内存后会长期持有, 再通过其内部的内存分配
器按照一定的策略和时机从中划分出部分内存用于模拟 Goroutine 的栈.
图 3 示意了 Go 运行时系统对每个 Goroutine 的栈管理结构, 其中 stack 结构包含两个字段: lo 和 hi, 分别表示
栈的低地址和高地址边界, 它们描述一个栈的内存地址范围位于 [lo, hi) 之间. 每个 Goroutine 在 Go 运行时系统中
用一个 g 类型的对象表示, g 对象的前几个字段描述它的执行栈, 包括一个类型为 stack 的字段, 用于描述该
Goroutine 的栈的地址范围. Go 的运行时系统会在 Goroutine 的栈空间不足时进行栈扩展. 当发生栈扩展时, Go 运
行时会进行栈拷贝, 将旧栈的内容复制到新分配的栈, lo 和 hi 也会进行相应的更改, 因此 Go 程序执行期间, 每个
Goroutine 的栈并非固定在内存中的同一段连续空间保持不变.
栈结构
stack.lo
stack.hi [6]
栈增长方向
高地址 低地址
顶层函数 Goroutine结构
Goroutine 其余函数栈帧 栈帧
栈
rsp
图 3 Goroutine 执行栈管理示意
1.1.2 Go 的垃圾回收与逃逸不变式
Go 语言内存管理主要依赖于其运行时系统, Go 从操作系统申请内存后会长期持有, 将其分为 Goroutine 栈
(如前文所述) 和 Go 堆进行管理. Go 堆使用 TCMalloc 进行快速的并发分配, 并通过 Go 的并发垃圾收集 (GC) [23]
实现堆空间的回收. Go 中的堆对象会使用诸如 runtime.newobject 等运行时函数在 Go 堆上自动分配.
这种内存管理机制使得 Go 程序员无需了解一个变量是分配在栈上还是堆中 [24] . Go 向程序员保证, 程序执行
的任何时刻中, 任意由垃圾回收标记算法标记可达的对象都处于生命周期内, 即, 不可能出现悬挂指针. 作为 Go
编译流水线上的一个重要优化遍, Go 的逃逸分析用于决定一个对象是堆分配还是栈分配. 为了内存安全以及 GC
的正常运行, Go 的设计者提出了以下两条逃逸不变式 [25] .
• 逃逸不变式 1: 指向栈对象的指针不可存储在堆中.
• 逃逸不变式 2: 指向栈对象的指针生命期不可超出该栈对象.