linux静态插桩工具PEBIL

引言

PEBIL是San Diego Supercomputer Center某实验室研发的工具,用来对ELF文件进行静态插桩。主要参考资料为IEEE论文:PEBIL: Efficient Static Binary Instrumentation for Linux和Github项目。本文为学习笔记。

论文学习

摘要及简介

PEBIL为静态插桩工具,与其他静态插桩工具的不同之处主要在于对插桩后运行效率的提升。PEBIL为基本块计数引入的开销平均为Dyninst开销的65%,Pin开销的41%,DynamoRIO开销的15%,以及Valgrind开销的8%。

二进制代码插桩主要用于在可执行程序中插入额外代码从而观察或者改变程序运行。插桩主要分为动态插桩和静态插桩,动态插桩的优势在于可以在程序运行的过程中修改或者删除插入代码。而其劣势也因此而产生,由于动态插桩需要在运行的时候分析语句,反编译,生成代码并做其他决定,所以开销大。静态插桩则可以很好地避免,但会因此损失一定的灵活性。静态插桩在大型应用的插桩场合常能派上用场。
PEBIL作为静态插桩工具,在指令长度固定的平台上的插桩位置使用分支语句来实现控制的转移。如下图所示:

插桩分支语句
原程序
保存原转状态
完成特定任务
恢复原状态

但是当指令长度不固定时,由于不一定有空间完整地插入分支指令,所以上述方式不一定能生效。PEBIL定位并且转换每一个函数的代码来解决这个问题。(我看到这里没有很懂具体是怎么操作的,后面会详细讲)

PEBIL包括程序执行计数,基本块计数和一个记录程序运行内存地址流的缓冲模拟工具。这三个插桩工具均由小于700行C++代码实现。

设计与实现

1. 识别代码部分和数据部分

在ELF中.text段中仍可能存在一部分数据(比如switch语句的跳转表1),所以插桩工具需要准确区分数据部分和代码部分。

PEBIL使用函数级的代码区分方式,根据符号表找到各个函数入口地址。然后从函数入口开始,用控制驱动的反编译和线性驱动的反编译来寻找代码部分。控制驱动指的是PEBIL会跟随函数的代码执行流来寻找所有的代码段,如果在过程中遇到问题则会默认反汇编出现问题,从而转向线性驱动。线性驱动指的是每一个指令按照在函数中的出现一条条进行反汇编。如果再次遇到问题,这个函数就会被标记为不可插桩。

符号表
开始
函数入口
可控制驱动?
控制驱动
线性驱动
得到下一条指令

两种方式都可能遇到的问题有:
1. 未定义的机器代码
2. 控制流返回到已经反编译过的部分
3. 控制流通过分支语句超出函数边界

在大部分情况下都可以正常地获取一条指令的控制流流向,比如流向下一跳语句或者是存在于指令内的一个地址。但是对于存在寄存器里等需要通过运行信息才能间接得到的地址,PEBIL使用一种peephole examination的策略来通过之前的指令得到间接地址。
peephole examination (简称PE) 的一种常见应用是计算跳转表(jump table)的地址。其跳转地址由jump table的基址与偏移量之和得到。可以先通过PE得到基址,再与对应位置的偏移相加得到每个跳转项的地址。

2. 插桩代码和数据

PEBIL的插桩代码和数据是在原ELF文件的基础上增加了一个插桩代码段和数据段,没有更改原本的段,如下图所示。这样便于之后的修改。
在这里插入图片描述

插桩代码效率

效率是PEBIL关注的重点

1. 代码转移和变换
对于指令长度固定的平台来说,控制从原代码转换到插桩代码一般是将插桩点的一句原代码替换为无条件跳转语句。但是对于代码长度不固定的平台来说,由于无条件跳转指令的长度大概率长于一般指令,故无法直接替换。解决方式有二:

  • 先用一个小分支语句跳转到一个更大的空间,然后在更大的空间处跳转到插桩代码处。此方法的问题在于有的指令的长度甚至小于小的分支指令长度,且还需要额外的空间进行多次跳转。
  • 使用int 3,使控制移交到操作系统提供的异常处理的特定地址。由于int 3是最短的指令,故不存在放不下的问题。这一方法由于调用了操作系统所以效率低,但是由于出现的概率不大,所以不必过分纠结。

然而PEBIL使用的策略是尽可能使插桩点的长度足够放入无条件跳转指令。具体操作步骤如下图:
在这里插入图片描述
注:0x8000地址位于插桩代码段。这样不会改变原代码段长度。

此方法会对效率产生影响,但是经过测试,插桩后的原程序执行开销只比插桩前增加4.8%。故认为影响不大。

2. 插桩片段
一般的插桩工具使用插桩函数来完成用户指定的任务,但是出于效率的考虑,PEBIL使用一系列汇编语言组成的插桩片段来执行任务。用内存中某计数器加一操作为例来看看这两者复杂程度的对比。

对比项插桩函数(function)插桩片段(snippet)
保存的变量标志寄存器+其他在函数中使用的寄存器(+栈顶的128字节空间)标志寄存器
控制转移方式call/return方式无条件跳转语句
影响指令cache程度较重(由于指令更多)

所以在需完成的任务较少的情况下,选择插桩片段会有更好的效率。

3. 插桩代码嵌入原函数
如果将插桩的代码直接嵌入原函数(个人认为应该是嵌入放在插桩代码段后经过一次原代码处的跳转才能到达的函数,不过论文中没有明确说),可以将效率大幅提升,因为减少了控制转移的开销。经过测试,开销可减少45%。

实验结果及其他

1. 实验数据
经过实验,使用PEBIL插桩后的代码的原代码执行(不执行插桩代码)平均开销为1.23%,最大开销为4.73%。而其他的插桩工具开销最小者平均开销38%,最大开销113%。
Pebil的基本块计数器的开销在28%-111%之间,平均开销为62%。Dyninst的静态仪器工具箱的平均开销为96%(41%-179%),Pin的平均开销为151%(8%-350%),Dynamorio的平均开销为408%(58%-693%),Valgrind的平均开销为734%(91%-1483%)。
PEBIL的插桩策略与Dyninst类似,而效率更高的原因可能是Dyninst插桩后的代码段中如果存在跳转,转移的地址仍然是原来的代码地址而不是插桩代码段中对应的地址,所以控制流会在原代码段和插桩代码段中多次转移。并且Dyninst 在基本块计数中使用pushf/popf来保存寄存器但是PEBIL使用lahf/sahf,由于保存的标志寄存器位数更少所以会减少流水线冲突。

2. 可进行的改进
目前PEBIL没有进行任何动态的寄存器分析,也就是不能知道那些寄存器有改动,如果可以增加此项分析,就可以在保存标志寄存器的时候知道是完全不用保存呢,还是可以用lahf/sahf保存一部分呢,还是需要用pushf/popf保存全部。

具体使用

由于我突然意识到我要找的静态插桩工具是要用在ARM架构上但是这个并不支持所以我就不继续学习如何使用了。有兴趣可以参考他们的repo。


  1. switch语句与跳转表 ↩︎

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值