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  不是这种错误发生的第一现场, 因此通常难以定位这种运
   5   6   7   8   9   10   11   12   13   14   15