binary instrumentation: 二进制执行文件插桩简介
文章目录
在二进制层面对执行文件进行插桩,可以摆脱对源码和编译器的依赖,覆盖大部分的代码逻辑。
1、static instrumentation
代码覆盖分析,可以用来做测试,比如分析哪些函数没有被测试到,也可以用来做程序启动时使用的函数收集、无用函数检测等。
在二进制层面使用静态插桩(static instrumentation),有几个好处:
- 不需要编译器的支持,可以达到极高的代码覆盖率。
- 可以避免编译器优化对于桩指令的影响,比如 inline 后可能会影响了桩指令的位置。
- 不需要考虑不同编程语言、不同编译器的问题。
- 灵活而强大,基本上可以覆盖大部分的场景,复杂如异常处理里的执行等也没问题。
2、bcov
bcov(a tool for binary-level coverage analysis),一种在二进制层面上做代码覆盖率(code coverage) 分析的工具,目前只支持 x86-64 ELF 格式。
bcov 主要思路是在要覆盖的地方,插入探针(probe、插桩),把控制流转移到 trampoline(跳板),执行完相关操作后,再跳回到桩的下方,正常执行原来的函数体。
这种方式甚至可以做到代码块级别的程度。但覆盖所有的代码块是性能消耗巨大的,采用 superblocks(超级块) 整理代码块支配的关系,可以只在必要的代码块插桩,减少性能和体积的消耗。
2.1、操作
bcov 会扩展 ELF 文件,多分配两个可加载的段(loadable segment),一个是代码段,用来写入 trampoline 逻辑;另一个是数据段,用来保存覆盖率数据。
bcov 读取函数的定义,解析 CFI(call frame information) 信息,然后构建调用图和 CFG 等。
bcov 在原始代码位置,插入跳转到 trampoline 的指令,在该 trampoline 中,会执行覆盖率数据修改的代码(把相关内存标记为 1),修改的内存对应的正是 bcov 添加的数据段内容;然后会执行原始代码块的指令。
数据段只包含有一个小的头信息来描述段内容,以及一大片初始为 0 的一个字节大小类型的数组。在运行了程序之后,我们可以提取内容,内存中标记为 1 的地方即说明被执行过。把数据 dump 出来分析即可。
3、inline hook
前面 bcov 的 trampoline hook 是把原始指令挪到 trampoline 当中执行,这种操作与 inline hook 的原理是类似的。(下面引用自 《理解Inline Hook,HookApi通信》)
inline hook 替换函数开头的几条指令,转移执行流到我们所需要的地方,即 patch 来执行所需的逻辑,然后回调执行被覆盖的头几条指令,再重新跳转回去。
inline hook 处理时,需要注意的几个问题:
- 需要考虑被覆盖的指令是否存在跳转指令,跳转指令挪动了位置需要修正,且需考虑跳转指令的范围限制问题。
- 需要考虑函数原本的执行体,是否会跳转到被覆盖的指令上,导致执行流错误。
- 对于范围过大的跳转,一些平台可能会使用寄存器读取偏移来进行 PC 相对跳转,这时候也需要考虑是否会污染掉已在使用的寄存器的变量。
4、dynamic instrumentation
动态插桩的方式,可以在运行时直接利用 perf 等工具收集信息,虽然是基于采样的,但好处是对性能影响不大。
还有一种在软件层面实现的,不依赖于编程语言、编译器、操作系统或硬件的支持。二进制指令会被工具翻译执行,相当于实现了一个虚拟机。指令都经过了虚拟机,自然要做插桩也是比较容易的。
关于这部分,不做展开了,之前写过一篇文章 【论文阅读】C++ 二进制插桩检测程序缺陷,感兴趣可以阅读一下。
参考
文章作者 calssion
上次更新 2023-06-19