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: 指向栈对象的指针生命期不可超出该栈对象.
   7   8   9   10   11   12   13   14   15   16   17