Page 21 - 《软件学报》2024年第6期
P. 21

陈金宝 等: DBI-Go: 动态插桩定位 Go 二进制的非法内存引用                                             2597



                 13.       识别     call 指令  c 的参数
                 14.       在     call 指令  c 前注册运行时回调函数
                 15.      END IF
                 16.     END FOR
                 17.    END IF
                 18.   END IF
                 19.  END IF
                 20. ENDFOR
                    在算法   1  中, 基本块  bb 2 中的所有  store 指令被认为都可能存储指针, 并将这些          store 转为规则  1  和规则  2  接
                 受的形式: store addr dst , addr. 之后, 通过使用 Pin 的  API 为这些  store 指令注册运行时回调函数. 在程序恰好运行到
                 这些  store 指令之前时, 注册的回调函数会被执行用来检测其是否违反了                  Go  逃逸不变式.
                    对于基本块     bb 3 , 它对应在  GC  期间调用  runtime.gcWriteBarrier 函数来接管  store. 因此, 可以在  bb 3 中识别
                                      对象不对用户暴露, 但幸运的是, 根据
                 runtime.gcWriteBarrier 所需的参数  (即图  7  中所示的  ptr 和  val). 在实现中发现, Go  编译器为了优化调用   runtime.
                 gcWriteBarrier 的流程, 减少准备参数的开销, 为     runtime.gcWriteBarrier 函数生成了不同版本, 这些不同的版本只
                 有传参的寄存器有区别. 比如         runtime.gcWriteBarrierR9  函数, 相比于原版的  runtime.gcWriteBarrier, 参数  val 使用
                 寄存器   R9  来进行传递, 其余流程均与       runtime.gcW-riteBarrier 相同. 为此, 可以识别  runtime.gcWriteBarrierRXX
                 的后缀来判断其传递        val 参数的寄存器. 当识别出      runtime.gcWriteBarrier 的参数后, 就可将其转为    store addr dst ,
                 addr 的形式  (ptr 对应 addr dst  , val 对应 addr), 并在  call 指令前注册运行时的回调函数用于在运行时检测该       call 指
                 令所代表的    store 是否违反了   Go  逃逸不变式.

                 3.3   在运行时回调函数中恢复 Go 运行时栈信息
                    Go  运行时函数库以静态链接的方式与           Go  应用代码链接起来形成可执行的          Go  程序. Go  运行时函数负责在运
                 行时管理   Go  程序运行所需的堆、Goroutine 的调度以及         Goroutine 的栈等. 用户编写代码时无需了解运行时的实
                 现细节, 比如对象如何分配, Goroutine 如何调度, Goroutine 栈如何管理等. 但是, 如若要在二进制层面分析内存的
                 引用关系, 分析栈对象地址是否被存储到栈外、是否违反逃逸不变式, 这就要求必须能够获得受                              Go  运行时管理的
                 一些信息, 比如当前     Goroutine 的栈信息等. 然而, Go  的运行时管理系统并不像操作系统一样提供了若干                  API 用于
                 在外部获取系统运行时信息. Go         的运行时系统相对封闭, 没有完备的           API 供外部获得当前     Go  程序的运行时信息.
                    幸运的是, 我们注意到       Go  语言的  ABI 规范  [32] 中定义了一些运行时信息的存储位置, 比如当前的             Goroutine,
                 来供运行时函数使用. 这意味着可以通过在二进制中添加回调函数的方式获得这些运行时信息.
                    在第  3.2.3 节中, 已为满足要求的     store 指令注册了运行时的回调函数. 该回调函数需要结合运行时信息来使
                 用规则   1  和规则  2  检测这些  store 是否违反了  Go  的逃逸不变式. 第 3.3.1 节将介绍该回调函数在运行时如何利用
                 Go    的  ABI 从  Go  二进制中获得当前执行指令的    Goroutine 及其栈的相关信息.
                 3.3.1    在运行时回调函数中获得 Goroutine 栈信息
                    由第  1.1  节可知, Goroutine 栈是在操作系统进程的堆内存中模拟, 那么要获知              Goroutine 栈的范围, 判断某个
                 指针是否是栈指针就不能简单地用操作系统的系统栈去判定. 为了得到                       Goroutine 的栈信息, 需要获得运行时中用
                 于管理   Goroutine 的  g  对象. 之后通过解析  g  对象的前几个字段的内存布局即可获得相应             Goroutine 的栈信息.
                    虽然  Go  运行时中的    g                             Go  的  ABI-Internal [32] 的约定可知, 在  AMD64 架
                 构中, R14  寄存器会保存当前执行的代码所在的            Goroutine, 也就是  g  对象的地址. 再结合  ABI-Internal 中有关基本
                 类型大小和对齐的约定以及前文所述的 g             和  stack  的结构, 可以通过公式   (7) 获得当前栈的    lo、hi:

                                                    
                                                     lo = ∗REG R14
                                                    
                                                                                                      (7)
                                                    
                                                    
                                                    
                                                      hi = ∗(REG R14 +8)
   16   17   18   19   20   21   22   23   24   25   26