Page 10 - 《软件学报》2024年第6期
P. 10
2586 软件学报 2024 年第 35 卷第 6 期
aspect. To effectively detect whether the code generated by the compiler has illegal memory references that may cause runtime crashes and
fill the research gap, this study conducts abstract modeling on the Go program and proposes two rules for verifying the validity of store
instructions. Based on these two rules, it overcomes the challenges of lacking high-level semantics in Go binaries and inconvenient access
to runtime information and designs a lightweight analysis tool DBI-Go. DBI-Go adopts static analysis plus dynamic binary instrumentation
and is implemented based on Pin, a dynamic binary analysis framework. Meanwhile, DBI-Go can identify illegal store instructions in Go
binaries. Evaluation results show that DBI-Go can detect all known escape-related issues in the Go community, and also discover an issue
that is previously unknown to the Go community. Finally, this issue has been confirmed. The applications in actual projects show that DBI-
Go can assist developers in finding bugs in escape analysis algorithms. Evaluation results also show that the measures adopted by DBI-Go
can reduce the false positive rate, and the extra runtime overhead brought by DBI-Go in 93.3% of the cases is less than twice the original.
Additionally, DBI-Go can be adapted to different versions of Go without modifying Go’s compilation and runtime, therefore yielding wide
applicability.
Key words: binary analysis; dynamic binary instrumentation; static analysis; Go; compiler testing; escape analysis
[1]
Golang (简称 Go) 是由 Google 提出并于 2009 年开源的新兴编程语言. Go 语言因其语法简单、静态强类型、
自动内存管理、原生支持高并发、编译型等特性, 得到许多开发者的认可. 2009 年和 2016 年两度成为 Tiobe 编程
然而, 目前 Go 社区中的逃逸问题频发, 据不完全统计, 从
语言排行榜的年度明星 [2] . 根据 2022 年 Go 开发者雇佣报告 [3] , 全球 10.5% 的开发人员将 Go 作为主力编程语言,
亚洲占比最高, 达 57 万人, 而中国更是有超过 16% 的开发者使用 Go 语言.
随着软件无处不在, 软件的安全及效能变得越来越重要, 而其中内存管理方式及其实现机制对软件开发产能、
安全和效能的影响尤为重要. 传统的 C/C++ 语言需要开发者显式决定变量是分配在栈、静态数据区、还是堆中,
显式管理对象的分配与回收, 这使得程序极易出错, 给开发和维护带来极大的负担. 为提升内存安全性和软件开发
产能, 越来越多的现代编程语言, 如 Java、Python、Go 等, 提供垃圾回收 (garbage collection, GC) 等自动内存管理
机制, 使得开发者无需管理对象的回收. Python 采用以引用计数为主、以分代解决循环引用为辅的 GC , 其采用
[4]
全局解释器锁 (GIL) 使多线程在竞争时串行执行. Java 虚拟机如 OpenJDK 则提供多种丰富的 GC 算法来专注于
吞吐量 (throughput)、延迟 (latency)、或内存占用 (memory footprint) 等不同性能指标, 它们组合运用多线程 stop-
the-world (STW, 多线程回收时让用户代码停止执行)、分代、紧致 (compaction, 回收期间通过移动对象来紧致内
存以解决碎片问题) 或不同的并发 (concurrent, 回收与用户代码并发执行) 等技术 [5] . 而 Go 的 GC 建立在基本没有
[6]
碎片问题的分配器 TCMalloc 之上, 使用的是无分代的、无紧致、并发的三色标记清除算法, GC 算法轻量. 这些
特性使得 Go 在特定场景下拥有 Python 般流畅的开发体验的同时, 又能达到接近 C++的运行效率 [7] .
Go 使用逃逸分析 (escape analysis) 在编译阶段来决定对象是否堆分配, 使得开发者无需手动指定对象的分配
位置. 现有逃逸分析有关的研究主要集中在 Java 语言上 [8−13] , 而非 Go. 对于栈分配, 只需通过修改栈帧指针即可几
乎零开销地完成分配和回收; 而堆分配则需要 GC 承担相当厚重的分配和回收代价. Java 语言呈现给开发者的观
点是对象在堆中分配, 而具有局部作用域的基本类型的值和对象类型的引用才会在栈上分配. 为降低 GC 的负担,
现代 Java 虚拟机在即时编译器中利用逃逸分析结果来开展内存使用优化, 如将未逃逸出方法的对象进行标量替
换 (即展开为一系列基本类型的值), 进而可以自动在栈上分配 [10] . Go 语言则期望让开发者专注于程序功能逻辑本
身, 不必指定变量或对象是分配在栈还是堆中, 而由编译器的逃逸分析来判定对象是否逃逸到堆上, 进而决定分配
的位置. Go 语言的逃逸分析是编译流水线中的必要流程, 其还承担了维护 Go 内存安全以及 GC 正常运行的责任.
Go 语言编译器的逃逸分析需要正确判断一个对象是否逃逸并分配在堆上, 否则极有可能产生若干内存漏洞, 如悬
垂指针等, 导致 Go 程序运行时异常行为甚至崩溃 (panic), 在实际应用环境中造成较大损失.
一个正确的 Go 逃逸分析需要找到所有需要堆分配的解, 使得程序可以正常运行, 不会因为内存漏洞而崩溃.
2017 年至今已有 17 个和逃逸分析相关的 issues (如图 1
所示), 并且无减少收敛趋势 (如图 2 所示). 比如 2022 年的 issue#54247 [14] , 逃逸分析和后续编译优化的配合出错,
导致本该逃逸的对象栈分配, 造成堆对象引用了悬挂的栈指针; 2021 年的 issue#44614 [15] , 由于逃逸分析的分析出
错, 导致全局对象引用了悬挂的栈指针. Go 的 GC 在运行时发现不正确的指针后会直接崩溃 [16] , 因此和逃逸相关
的问题一旦出现, 通常都是致命的问题. 另外, 由于 GC 不是这种错误发生的第一现场, 因此通常难以定位这种运