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

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


                 息, 便于开发者定位错误       (第 3.3 节).
                    DBI-Go 基于  Pin [21]  设计实现了上述两个组件, 包括约      1 000  行  C++代码. Pin  是由  Intel 开发的支持  IA-32、
                 X86-64  和  MIC  指令集架构的动态二进制插桩框架, 可用于实现动态程序分析工具.

                 3.2   使用静态分析从   Go  二进制中恢复    Go  的  store 指针语义
                    Go 是编译型语言, Go 程序一旦被编译链接后其二进制代码中所有指令都会固定下来. 因此, 可以通过静态分
                 析的方式识别 Go 程序二进制代码中的 store 指令. 然而, 若要判定一条 store 指令是否在存储指针                    (即规则  1  和规
                 则  2  中的  store addr dst , addr 形式), 就还需要获取 Go 程序的一些高层语义信息, 如类型等. 下面先分析难点, 然后给
                 出解决思路及其关键点的实现方法.

                 3.2.1    难点分析及解决思路
                    根据  Zeng [35]  的工作, 在  C 语言的二进制中, 所有高级类型信息, 如整型、浮点型和指针类型, 在编译后都会
                 丢失, 二进制代码中仅有的两种类型是寄存器和内存位置. 在                   Go 语言中也类似, Go 二进制程序已经难以区分指
                 针类型和非指针类型.
                                                       GC
                    • Go  二进制中指针的识别难点. Go       语言中, 指针类型用于传递对象地址, 不能进行指针运算. Go                的  GC  会扫
                 描指针, 堆指针指向的对象会在合适的时机被              GC  回收. 然而, Go  语言中有一种类型 uintptr   [36] , 其大小和普通指针
                 相同, 可以容纳任意的指针类型的值, 可以用于进行指针运算等操作. 但                     GC  并不把  uintptr 当作指针, 因此也不会
                 基于该指针值进行对象标记. 若把某栈对象的指针转为 uintptr 后存入堆对象, 并在之后不通过它访问对象, 那么
                 这种存入操作并不违反        Go  的逃逸不变式, 因    Go  并不将  uintptr 视作指针. 然而, Go  二进制中仅有的两种类型是寄
                 存器和内存位置, 难以判断某个寄存器或者内存位置中的值是指针还是 uintptr. 若对其不做区分, 都视为指针, 则
                 势必会带来很多误报, 影响精度和效率.
                    • Go  二进制中地址存入操作的识别难点. 除了类型信息的缺失, Go                二进制上的地址存入操作与用户代码中的
                 地址存入操作也有较大差别. Go         编译器在编译      Go  程序时会执行若干程序变换, 在用户代码中生成诸多与                 Go  运
                 行时管理相关的代码, 以便        Go  运行时系统对    Go  程序的管理. 在这些代码中会产生若干违反             Go  逃逸不变式   store
                 操作, 但  Go  的运行时保证了这些操作的安全性. 若不将这些操作排除, 也会带来较多的误报.
                    • 解决思路. Zhong 等人   [17]  通过  Go  运行时系统中管理并发的相关函数恢复了          Go  二进制上的并发语义. 这启
                 发我们可以通过静态分析的方式, 识别            Go  二进制用户代码中和内存管理、垃圾回收相关的                 Go  运行时函数来恢
                 复相关的   store 指针的语义. 经过对    Go  运行时相关函数的分析可发现, Go         运行时和写屏障相关的运行时函数可以
                 用来恢复相应的      store 指令. 第  3.2.2  节中介绍该机制, 并在第   3.2.3  节中介绍如何使用该机制来恢复满足要求的
                 store 指令并使用  Pin API 为其注册运行时的回调函数.

                 3.2.2    Go  的写屏障
                    Go  运行时的不可或缺的部分为垃圾回收             (GC) 系统. 尽管  GC  在幕后运作, 却有数个运行时函数与其息息相
                 关. 这些运行时函数分为两类: 一部分可供用户主动调用, 用于配置                   GC  参数或强制启动新的       GC 周期; 另一部分
                 由编译器在编译期间自动插入, 在运行时辅助               GC  的运行, 确保  GC  相关内存状态的准确性. 在这个体系中, 编译
                 器自动插入的     runtime.gcWriteBarrier 函数在维护  相关内存状态的正确性方面扮演着关键角色.
                    Go  的  GC  系统在堆内存使用达到特定阈值时会中断用户程序的运行, 对那些由根集中的指针直接或间接可
                 达的对象进行扫描和标记, 标记出仍在生命周期中的对象, 随后释放已经不再使用的对象. 在这个过程中, 用户程
                 序完全暂停, 因此在逻辑上对         GC  的发生没有感知, 这确保了内存读写不会对            GC  状态造成任何干扰.
                    Go  的并发  GC  在特定阶段允许用户程序与其并行, 以减少等待时间. 然而, 在                 GC  对对象进行标记的过程中,
                 用户程序的内存读写可能会修改对象的引用关系, 这可能导致                     GC  的标记与实际情况不一致, 从而错误地清理正
                 在使用的对象. 例如, 如果在       GC  完成对栈指针的扫描后, Go       应用程序将某个堆对象的地址存入栈对象中, 而这个
                 新引用关系的创建没有被 GC 感知到, 即该堆对象没有被 GC 标记, 那么在这一轮                     GC  结束后, 该栈对象持有了一
                 个已经被释放的堆对象地址, 从而导致内存错误. 因此, 在                GC  运行期间, GC  需要获知所有指针类型的内存写入,
   14   15   16   17   18   19   20   21   22   23   24