Intel PIN
Intel PIN
References
[1]. 官方用户指导手册 Pin 3.21 User Guide
[2]. 论文:《Pin: Building Customized Program Analysis Tools with Dynamic Instrumentation》
二进制动态插桩简介
源插桩与二进制插桩
随着软件复杂性的增加,插桩(instrumentation),一种向应用程序中插入额外代码以观察其行为的技术变得越来越重要。 插桩可以在不同阶段执行,例如:源代码中,编译时,链接后,运行时。
插桩方式可以分为两类:
- 源插桩(source instrumentation)。源插桩要求掌握应用程序的源代码,否则无法进行插桩。这一条件在实际应用中较难实现。在生产环境中,源码一般不予公开。
- 二进制插桩(binary instrumentation)。不要求源文件,可以与任何软件应用程序一起使用。
静态插桩与动态插桩
二进制插桩又可分为两类,静态二进制插桩(static instrumentation)和动态二进制插桩(dynamic binary instrumentation)
-
静态插桩。在程序运行前提前,对目标程序进行重写后再运行。静态插桩是会修改源文件,因此必须提前对各项源文件进行备份,且每次插桩时都需要进行备份、修改代码、重编译过程,操作费事且复杂。
-
动态插桩。在程序运行时进行动态编译、插桩,使用的是源代码进行动态插桩后的新代码,源文件不受影响。在jit模式下,执行的都是pin生成的代码,原始代码一般用作参考,并不实际执行。
常见的动态插桩工具
一些常见的动态插桩工具(框架):
- Pin
- DynamoRIO
- Valgrind
- Nirvana
- Frida
Intel Pin比较常用,因为其提供了丰富的编程接口,开发者可以通过调用接口来方便获取程序动态执行期间的指令、内存寄存器等信息,实现细粒度的监控。
Intel Pin简介
Intel pin是Intel公司设计的用于动态二进制插桩的软件平台
Intel pin动态插桩框架
Intel pin插桩主要关注两个问题:
-
分析(在哪插入代码,分析代码analysis routine)
-
桩(插入点执行什么代码,桩代码 instrumentation)
将解决这两个问题的两个组件放置在Pin tool中。Pin tool可以理解为pin中的插件,它能够修改生成代码的流程。
可以将pin看作一个简单的just-in-time
编译器,输入的是可执行文件,输出的是插桩完毕的程序。pin截获第一条可执行指令,产生新的代码序列,并将控制流程转移到新生成的代码序列。新产生的序列基本上与原序列一致,但是pin可以保证在一个分支结束后重新获取控制权,获得控制权之后pin可以为分支目标产生代码并执行。pin可以通过将所有产生的代码放置在内存中以便于重新使用这些代码,加快从一个分支跳转到另一个分支之间的过程,提高效率。
简单示例:
下述代码为一个简要的pintool。该pintool记录代码中的每次内存写入操作,并打印写入内存的地址和写入大小。
FILE * trace;
//输出内存写记录
VOID RecordMemWrite(VOID * ip, VOID * addr, UINT32 size) {
fprintf(trace,"%p: W %p %d\n", ip, addr, size);
}
// 每条指令前都被调用
VOID Instruction(INS ins, VOID *v) {
//使用predictedCall(),确保只有当访存真正发生时,才调用 RecordMemWrite
if (INS_IsMemoryWrite(ins))
{
INS_InsertPredicatedCall(
ins,
IPOINT_BEFORE,
AFUNPTR(RecordMemWrite),
IARG_INST_PTR,
IARG_MEMORYWRITE_EA,
IARG_MEMORYWRITE_SIZE,
IARG_END);
//给函数名称
//传递指令指针
//访问的有效地址
//写入数据的大小
}
}
int main(int argc, char *argv[]) {
PIN_Init(argc, argv);//初始化pin
trace = fopen("atrace.out", "w");//打开文件atrace.out,并向里面写入数据
INS_AddInstrumentFunction(Instruction, 0);//添加指令粒度函数桩
PIN_StartProgram(); // 开始执行程序(never return)
return 0;
}
如上图所示
主函数初始化Pin,注册名为instruction
的函数
并告诉pin开始执行程序。
当向代码缓存中插入新指令的时候,JIT调用Instruction
函数并向他传递已解码指令的句柄。
若指令执行写入内存操作,Pintools将在每条指令之前插入对RecordMemWrite
的调用
Pin插桩的粒度
Pin插桩的粒度由小到大分别为:
-
指令插桩
instruction instrumentation
-
踪迹插桩
trance instrumentation
-
函数插桩
routine instrumentation
-
程序插桩
image instrumentation
-
踪迹插桩trance instrumentation,在pin中trance是指以一个分支跳转开始,以一个无条件跳转branch结束,在一个trance中又被分成了若干基本快块 basic block,在pin中,basic block表示以单一入口,单一出口的指令序列。
1.trace解释。Trace是一个顺序执行的指令序列,其由一个branch的目标代码处起始,直至满足以下三种情况:
- 无条件的控制转移,eg:call、ret
因此trace可以包含jcc指令,这代表trace可以有多个出口,即trace中的代码可能会有多条执行路径(即,“单入口,多出口”) - 预定义的条件控制转移数
- 预定义的trace的指令数
2.BBL解释。basic block是比trace再小一个级别的代码块,它符合“单入口,单出口”的特点。因此实际使用中,可以将BBL作为代码块的最小单位,减少插桩次数。
Pin插桩流程
- 系统加载pin并进行初始化
- pin加载pintool进行相关初始化
- pintool请求pin运行待插桩的程序
- pin拦截程序运行的入口点。
- pin取一个trace然后进行JIT编译
- 在编译过程中运行插桩例程(instrumentation routine), 判断插入点。
- 将分析例程(插桩的代码)与源指令进行整合重新编译生成新的指令序列
- 插桩完成的指令序列放置在code cache中以备后续快速跳转执行。
Pin系统架构
下图展现了pin的软件架构。pin由三部分组成,虚拟机virtual machine、代码缓存 code cache、调用pin-tool的插桩API(instrumentation APIs),在插桩过程中,地址空间中同时存在三个进程:application、pintool、 pin。但是他们之间并不共享任何代码库文件,三者链接私有的库文件副本,用于解决不可重入问题。
虚拟机
虚拟机是pin的核心组成部分,负责调度插桩代码的执行,在pin获得对应用程序的控制之后,vm协调组件来执行应用程序。虚拟机由一个实时编译器(jit compiler),一个仿真器(emulation unit),一个调度程序(dispatcher)组成。
-
JIT compiler 实时编译和检测应用程序代码, 即时编译完成后(插桩后)的新代码存储在code cache中
-
Emulation compiler 模拟器解释执行不能直接执行的指令,它用于需要VM进行特殊处理的系统调用。
-
Dispatcher用来启动jit编译以及检测程序代码
code cache
是用来存储jit编译器插桩完毕的新指令的地方,用于分支之间的快速跳转,提高执行效率
Instrumentation APIs
pin与pintools进行通信的接口
Pin如何优化插桩代码
Pin使用一些jit的一些新特性优化插桩代码
1.Trace Linking
一般情况下,一个已编译的trace在结束时,需要跳转到另外一个trace,此时往往需要从code cache切换到vm,由vm查找好跳转地址再将控制权交由code cache执行,这样切换有额外开销
优化措施:pin使用一个可动态增长的链表将已编译的trace链接成一个链表。当需要在trace之间跳转时,则通过扫描链来寻找跳转地址。当链表中无匹配时,才切换到vm下,进行新的trace编译并添加到trace链表表头。
2.Register Re-allocation
该对寄存器进行重分配,优化插桩代码。在jit运行时,我们经常需要额外的寄存器。例如:在解决indirect分支时,我们就需要三个空闲的寄存器。当instrumentation对一个应用程序插入一段代码,JIT必须保证这段代码没有重写任何应用程序正在使用的寄存器,pin的解决方案是构建一些虚拟寄存器,pin和pintool在使用时可以将它们看作正常的寄存器使用。
3.其他优化措施
利用inline、liveness analysis、schedualing这些优化策略对插桩代码进行优化
通过实施这些优化策略可以将jit编译所生成的插桩后的代码量大大缩减,提高插桩代码的执行效率
Pin几个官方示例程序
在官网上下载pin对应版本,对应平台,解压即可使用。
编写一个简单的示例程序:
#include "stdio.h"
int main(){
char str[10]={'h','e','l','l','o','n','n','\0'};
printf("%s\n",str);
}
1.简单计算指令数目(指令级插桩)
这个示例程序统计了程序总共执行的指令数量。他在每一条指令前插入一次调用docount,程序结束时将count值保存到inscount.out中。
使用其自带的inscount0.so
pintool
./pin -t ./source/tools/ManualExamples/obj-intel64/inscount0.so -o inscount0.log -- ./source/tools/ManualExamples/obj-intel64/test/printnxu
其结果如下图所示:
inscount0log.cpp
源码如下所示:
#include <iostream>
#include <fstream>
#include "pin.H"
using std::cerr;
using std::endl;
using std::ios;
using std::ofstream;
using std::string;
ofstream OutFile;
// 使用static帮助优化docount的编译
static UINT64 icount = 0;
// 在每个指令执行前,该函数被调用
VOID docount() { icount++; }
// Pin遇到每个新指令时调用此函数
VOID Instruction(INS ins, VOID* v)
{
// 每个指令前插入对函数docount的调用
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
KNOB< string > KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool", "o", "inscount.out", "specify output file name");
// 应用程序退出时调用此函数
VOID Fini(INT32 code, VOID* v)
{
// Write to a file since cout and cerr maybe closed by the application
OutFile.setf(ios::showbase);
OutFile << "Count " << icount << endl;
OutFile.close();
}
INT32 Usage()
{
cerr << "This tool counts the number of dynamic instructions executed" << endl;
cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
return -1;
}
int main(int argc, char* argv[])
{
// 初始化 pin
if (PIN_Init(argc, argv)) return Usage();
OutFile.open(KnobOutputFile.Value().c_str());
// 注册 Instruction
INS_AddInstrumentFunction(Instruction, 0);
// 注册 Fini,当程序退出时调用此函数
PIN_AddFiniFunction(Fini, 0);
// 开始此程序
PIN_StartProgram();
return 0;
}
简而言之,整体代码:
- 初始化
- 打开文件
- 注册函数
- 运行程序
2.指令地址追踪(Instruction Instrumentation)
这个示例中展示如何传参给pintool,pin允许传指令指针、寄存器值、内存操作地址、常量等等。
./pin -t ./source/tools/ManualExamples/obj-intel64/itrace.so -- ./source/tools/ManualExamples/obj-intel64/test/printnxu
只需要将上面的例1 进行小小的改动,就可以使之打印出每一条指令的地址。
更改INS_insertcall
的参数,
从
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END)
变成
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END)
在AFUNPTR
和 IARG_END
之间,后者增加了一个IARG_INST_PRT
参数,这个参数将返回当前指令的地址。
printip
是一个函数指针,是将被插入的调用。每次指令前都会调用这个函数,并且将刚刚这个IARG_INST_PRT
作为其参数
VOID printip(VOID *ip) { fprintf(trace, “%p\n”, ip); }
可以看到这个函数就有了一个 VOID * ip 的参数。从而可以通过这种方式知道具体的情况。
itrace.cpp 源码如下
#include <stdio.h>
#include "pin.H"
FILE* trace;
//该函数在每个指令执行前被调用 输出地址
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }
// Pin当遇到新指令的时候调用此函数
VOID Instruction(INS ins, VOID* v)
{
//在每个指令之前插入对函数printip的调用,并向其传递IP
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}
// 当应用退出时调用此函数 finish
VOID Fini(INT32 code, VOID* v)
{
fprintf(trace, "#eof\n");
fclose(trace);
}
INT32 Usage()
{
PIN_ERROR("This Pintool prints the IPs of every instruction executed\n" + KNOB_BASE::StringKnobSummary() + "\n");
return -1;
}
int main(int argc, char* argv[])
{
trace = fopen("itrace.out", "w");
// 初始化 pin
if (PIN_Init(argc, argv)) return Usage();
// Register Instruction 被调用执行插桩
INS_AddInstrumentFunction(Instruction, 0);
// 当应用程序退出时调用register Fini finish函数
PIN_AddFiniFunction(Fini, 0);
// 开始程序,永远不return
PIN_StartProgram();
return 0;
}
3 .内存引用追踪
Memory Reference Trace(Instruction Instrumentation)
./pin -t ./source/tools/ManualExamples/obj-intel64/pinatrace.so -- ./source/tools/ManualExamples/obj-intel64/test/printnxu
编写自己的pintools
首先介绍回调函数:
Pin提供不同程度的回调函数,大体上分为下面几个层次:
IMG:image object
INS:instruction object
SEC:section object
RTN:routine object
REG:register object
SYM:symbol object
TRACE(已经介绍)
BBL(已经介绍)
Pin tool由int main(int argc, char * argv[])
函数开始,由NMAKE
编译选项编译成特定的动态链接库,如果要编译自己的动态链接库,在Nmakefile
文件中把要编译的动态链接库名字加到COMMON_TOOLS=
后面,使用…\nmake.bat TARGET=intel64 xxx.dll
命令进行编译。
如果在程序中要使用Symbol,要调用PIN_InitSymbols()
;
初始化PIN_Init(argc, argv)
PIN tool分为:
-
指令级插桩(instruction instrumentatio),通过函数
INS_AddInstrumentFunctio
实现。 -
轨迹级插装(trace instrumentation),通过函数
TRACE_AddInstrumentFunction
实现。 -
镜像级插装(image instrumentation),使用
IMG_AddInstrumentFunction
函数,由于其依赖于符号信息去确定函数边界,因此必须在调用PIN_Init
之前调用PIN_InitSymbols
。 -
函数级的插装(routine instrumentation),使用
RTN_AddInstrumentFunction
函数。函数级插装比镜像级插装更有效,因为只有镜像中的一小部分函数被执行。
以下两函数需要先调用PIN_InitSymbols()
-
IMG_AddInstrumentFunction
-
RTN_AddInstrumentFunction
来分析出符号。在无符号的程序中,IMG_AddInstrumentFunction
和RTN_AddInstrumentFunction
无法分析出相应的需要插装的块。
在各种粒度的插装函数调用时,可以添加自己的处理函数在代码中,程序被加载后,在被插装的代码运行时,自己添加的函数会被调用。
INS_AddInstrumentFunctio、TRACE_AddInstrumentFunction、IMG_AddInstrumentFunction、RTN_AddInstrumentFunction指定的回调函数只有在相应的代码被分析到时才会被调用,即分析到一次只被调用一次,但程序运行过程中一般不再被调用,但INS_InsertCall之类的程序添加的函数,是在相应的代码位置添加函数,根据程序运行的情况,会被多次调用。
在INS_AddInstrumentFunctio指令级插装的代码中,只有在INS_AddInstrumentFunctio指定的函数被调用时INS指令才有效,在INS_InsertCall函数中,INS无效。
Symbols
Pin 通过使用符号对象SYM来获得函数名字。
参考官方指导手册中的API介绍即可实现自定义pintool的编写。
具体编写待俺学习一下哈哈哈哈。