intel 插桩工具 pin 介绍

目录

Pin 介绍🎨

pin 概念⚽

简单使用🎽

重要功能介绍💎

指令级别

memory 操作

多线程相关

process 相关

插桩粒度

context 概念

 debug 相关

小结🧩

pin 原理探究🧲

pin 与 DBI🔍

插桩粒度探究📈

整体架构🏡

执行过程🚄

pin 优化手段🚿

小结📁

pin 总结📘

References📤


Pin 介绍🎨

pin 概念⚽

pin 是一个用来对程序进行插桩 (instrumentation) 的工具,支持 Android,Linux,Windows 等平台,支持 IA-32,Intel(R) 64 等架构,之前版本还支持 ARM,由 intel 公司提供。

pin 的官方文档、教程、工具可以查看 pin 的官网:

Pin - A Dynamic Binary Instrumentation Tool

pin 允许在可执行文件 (executable) 文件的任意位置插入任意的代码 (C/C++ 编写) ,它的代码可以被动态的添加到正在执行的可执行文件中,另外还可以将 pin 附加 (attach) 到正在运行的进程中。

pin 提供了丰富的 API,可以抽象出底层指令集特性,并允许将诸如寄存器内容之类的上下文 (context) 信息作为参数传递给注入的代码,pin 会自动保存和恢复被注入代码覆盖的寄存器,以便让应用程序继续工作,pin 还提供了 limited access to symbol and debug information [1]。

插桩包含两个重要部分:

  1. a mechanism that decides where and what code is inserted; (Instrumentation)
  2. the code to execute at insertion points. (Analysis)

pin 的官方教程中包含了大量的示例,可以快速的入门使用,并通过它来制作新的工具。

pin 应用广泛,开发模拟器(分支预测,缓存建模)、程序安全分析、fault tolerance studies、emulating new instructions、线程分析、call-graph generation、code coverage 等。

简单使用🎽

上面来自官网的介绍可能还不能让你充分体会到 pin 的强大,掌握一个工具的最好方法就是先学会使用它,那让我们先来简单的使用一下 pin,看看 pin 都可以做些什么吧。

如果想在 windows 上使用 pin,那么还要先安装 windows 上的支持 make 功能的工具,我们在 linux 环境下进行 pin 功能的演示:

pin 下载地址:Pin - A Binary Instrumentation Tool - Downloads

下载完成 pin 之后,执行:

$ cd source/tools/ManualExamples
$ make all TARGET=intel64

 编译生成的文件会放在新生成的目录 obj-intel64 下:

该目录下的文件如下,以后缀 .so 为结尾的文件我们之后会用到:

执行下面的命令,-t 指明要使用的 pintool,也就是刚刚生成的 inscount0.so,pintool 中指明了我们希望插桩工具所做的事情,-- 后面的 /bin/ls 就是我们要插桩的程序,这里就是我们常见的 ls ,我们可以看到 ls 是正常执行了的,也就是显示了当前目录下所有的文件名,我们执行 cat inscount.out 命令,可以看到插桩工具输出的结果就是 Count 422838,这个 inscount.so 实际上做的事情就是统计 ls 这个程序中包含的二进制指令的条数,并且是在 ls 执行过程中统计完成的。

我们现在可以看看 ManualExamples/ 目录下的 inscount0.cpp 文件中的具体内容,看看这个文件是怎样定义的:

/*
 * Copyright (C) 2004-2021 Intel Corporation.
 * SPDX-License-Identifier: MIT
 */
 
#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 UINT64 icount = 0;
 
// 每次 Instruction 函数都会调用这个函数进行 icount 变量自增
VOID docount() { icount++; }
 
// PIN 每次执行一条 instruction 前都会执行这个函数
VOID Instruction(INS ins, VOID* v)
{
    // INS_InsertCall 函数对指令插入指定的函数
    // ins 是当前可执行文件准备执行的指令
    // IPOINT_BEFFORE 指的是 instruction 执行前时间点
    // docount 是注册的函数,在上面定义,统计指令条数
    // IARG_END 指明传入 docount 的参数的结尾,这里 docount 传入参数为空
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
 
// 在命令行中指明 -o xxx 可以改变输出信息存储文件路径
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();
}
 
/* ===================================================================== */
/* 打印 help 信息                                                        */
/* ===================================================================== */
 
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);
 
    // 注册终止函数
    PIN_AddFiniFunction(Fini, 0);
 
    // 开始执行应用,永远不会返回
    PIN_StartProgram();
 
    return 0;
}

 上面的代码添加了详细的注释,这个例子比较简单,也很好理解,我们现在可以简单理解为,我们的目标插桩可执行文件 (ls) 在每次执行一条二进制指令时,都会首先调用我们注册的插桩函数 Instruction() 函数,这个函数会在每次执行该二进制指令前调用 docount() 函数,令全局变量 icount 自增 1,然后再真正执行原目标插桩可执行文件 (ls) 这条二进制指令,这个过程反复的进行,最终目标插桩可执行文件 (ls) 执行结束,统计也执行结束,统计信息被写入到我们指定的输出文件 inscount.out 中,整个过程就结束了,是不是很简单?没错,pin 就是那么简单而且很实用,这个 inscount0.cpp 被称为 pintool,它调用了 pin 提供的库函数,将它编译成 .so 文件后实用命令就可以对我们的目标插桩可执行文件进行操作了,我们按照自己的需求,根据官方提供的教程,自己编写属于我们自己的 pintool 工具,比如按照官方文档中的建议修改 MyPintool.cpp 然后按照需求编写代码即可。

当然这个例子是很简单的,pin 的能量远远不止这一点。🎃

重要功能介绍💎

限于篇幅,这里不可能对 pin 的所有功能,所有 api,以及具体编写事项完全解释清楚,而且官方文档比较详尽,内容也不算多么繁杂,相信读者可以很快的通过实地的练习熟练掌握 pin 的使用,这里仅仅就作者认为比较重要的几点做出阐述。

指令级别

pin 可以分析指令类型(INS_IsMov 函数),获取操作码,操作数(INS_Opcode 函数),获取操作寄存器(INS_RegW 函数),删除指令(INS_Delete 函数),反汇编(INS_Disassemble 函数),插入跳转(INS_InsertDirectJump 函数),系统调用相关函数(INS_SyscallStd 函数),

memory 操作

通过 pin 提供的 memory 操作 api,我们可以统计比如指令的包含访存操作数的数量 (INS_MemoryOperandCount),访存是读还是写,获得指令的 effective address 即 (IARG_MEMORY_EA) 等。利用收集到的地址 trace 记录,我们可以开发一些 cache 或者 cpu 模拟器,例如比较常见的 champsim 模拟器,它就是基于 pin 收集到的 trace 记录开发的,它收集的内容很简单,指令 ip,是否是分支,分支是否跳转,源/目的寄存器号,源/目的访存地址,基于这几条简单的内容足以开发出一个模拟器,可见 pin 功能的强大。

多线程相关

pin 提供了多线程支持,比如锁机制(PIN_LOCK),线程操作(PIN_Yield),线程私有存储 TLS (Thread Local Storage),如 PIN_GetThreadData() 函数。

process 相关

pin 不仅可以对可执行程序进行插桩(比如 exe 文件),还可以对正在执行的进程进行插桩,可以 attach 到 process 上,或者从 process 上 detach(PIN_Detach),这个功能是很重要的,比如如果我们想对一个包含了很多个进程的大型程序的某些部分进行分析,比如一个数据库应用,我们没有必要对它的客户端进程进行分析,只要对它的服务器进程进行分析即可。

插桩粒度

我们上面演示的例子中粒度是指令级别,实际上我们可以不对单条指令进行插桩,可以对多条指令进行插桩,BBL 级别指的是 basic block,关于它我们后面还会继续介绍;IMG,image 级别的插桩;SEC,section 级别插桩,比如统计可执行文件有多少个 section;RTN,routine 级别插桩,routine 包括了函数等,我们可以先使用 PIN_InitSymbols 函数获得 symbol table 即符号表信息,这样就可以指定获得某个函数名的 routine 的相关信息 (RTN_FindByName),比如地址,参数,返回值等,可以进行 routine 替换(RTN_Replace),统计 routine 被调用的次数。

context 概念

我们最开始介绍过了,pin 在插桩前后要能够保存和恢复可执行文件的上下文 (context) 信息,这样才能够保证 pin 不会影响到原目标插桩程序的正常执行,我们可以在调用 INS_InsertCall 函数时,将这个 context 作为一个参数传递给具体插桩函数 (IARG_CONTEXT),通过这个 context 我们可以读取/修改架构级别(逻辑)寄存器的具体值,包括整型寄存器、浮点 status/control 寄存器,fp 栈寄存器等等,这些函数的具体用法不过多的解释,注意在插桩函数中对这些寄存器的修改返回后都被忽略,如果想真正实现能在修改后的 context 运行,要执行下面的 PIN_ExcuteAt 函数,另外这些寄存器都是架构级别的,无法访问到物理寄存器级别。

 debug 相关

听我这么多对 pin 的描述是不是觉得 pin 好像能作为一个调试工具,实际上它可以和调试工具一起更多的功能。pin 可以配合 debugger 工具,比如 gdb ,一起完成 debugger 可能无法处理的信息,比如观察所有涉及到分配栈空间的操作,pin 可以在 debugger 的 breakpoint 处停止并执行插桩统计函数(PIN_ApplicationBreakPoint),可以为 debugger 添加新的调试命令。vs 上也提供了集成 pin 完成调试的功能。

小结🧩

这部分内容简单介绍了 pin 的概念、使用以及重要功能,关于 pin 的具体实践,读者可以在仔细阅读官方手册后继续学习。

pin 原理探究🧲

pin 与 DBI🔍

知道了 pin 的用法之后,我们再来看一看 pin 的原理,学习 pin 的原理对 pin 的使用也有很多的帮助。

pin 的特点是首先 binary 级别,它插桩的程序是二进制可执行文件,我们不用获得目标程序的源代码也可以执行,这一点可以帮助我们去分析那些没有源代码的应用或程序,它支持 real-life application,比如 Database,web 浏览器;第二是 dynamic,它是边插入边执行的,而不是静态的插桩,在它插桩的同时,无论是  pintool 还是目标插桩程序都是处于运行中的。这两个特点可以称之为 DBI(dynamic binary instrumentation)。

插桩粒度探究📈

上面我们简单提到了插桩粒度的问题,这里我们对插桩粒度进一步的进行解释。

 

上面的这幅图 [2] 中共有 6 条汇编指令,即 6 insts,pin 中又提出了一个概念叫做 BBL,即 basic block,它是以 control-flow transfer instruction 作为结尾的多条指令序列,例如上面的 6 条指令中,jle 和 jmp 都是控制流转移指令,所以它们每三条组成了一个BBL,共有2个 BBL,即 2 BBs;更粗的粒度是 trace,它是指以 unconditional control-flow transfer instruction 作为结尾的多条指令序列,无条件控制流转移指令如 jump,call,return 等,可以看到 jle 不是无条件跳转,而 jmp 是无条件跳转,所以这六条指令组成了一个 trace。在 pin 的 api 中,比如上面举的例子,统计指令的条数,我们可以在每条指令执行前插入函数统计指令,也可以在一个 BBL 前插入函数统计整个 BBL 的指令条数,都有自己对应的函数,trace 也是如此,比如下面的这个例子是对上面指令级插桩统计指令条数的 BBL 级改进。

 

整体架构🏡

pin 的基本架构可以用下面的这幅图来表示:

 大致可以分为三个部分:

  • 供 pintool 调用的 instrumentation api 库;
  • VM:即 virtual machine,它的功能主要是编译并执行程序,包含了 emulation unit 用来处理系统调用;JIT 编译器,负责编译和插桩,每次取一个 trace 编译后送到 code cache 中;
  • code cache:用来存储编译后的代码,真正执行的是 code cache 中的代码,原目标插桩文件中的代码不会执行。

 这个 JIT 即 just in time 即时编译器有点像 java 里的编译器,包括这个 VM,有点像 JVM,所谓的即时编译就是等到真正执行时才真正翻译成可执行指令,比如 java 编译得到的 .class 文件,我们开始在 jvm 上执行时,才真正将 class 文件中的字节码翻译成底层架构可以识别的二进制指令并执行,但 pin 有点区别的是它的输入不像 java 的 class 文件那样是字节码,而是本来就可以被执行的二进制指令,只是它做了插桩处理。

除了 JIT 这种运行修改后的源可执行文件的拷贝,即不对原目标插桩可执行文件进行修改的模式外,pin 还提供了一种 probe 探测模式,可以直接在原可执行文件上修改并运行,不过它对编写者有很高的责任要求,这里就不深入介绍了。

执行过程🚄

这里简单的演示一下 pin 的执行过程。

首先如下图所示,原目标可执行文件中有 7 个 BBL,每个 BBL 以条件跳转指令作为结尾,比如 BBL1 的结尾可能跳到 BBL2 或者 BBL 3,开始执行时,CPU 的控制权首先在 pin 手中,pin 从 BBL1 开始取一个 trace,这个 trace 包括了 BBL1,2,7,BBL7 是这条 trace 的结尾,它的最后一条指令就像我们刚刚介绍的那样是一条无条件控制流转移指令,这个 trace 按照我们定义好的 pintool 中的插桩函数所做的那样,进行插桩,可能是每条指令后进行插桩,这个过程就相当于编译,编译后的代码被放到了 code cache 中,这样 1 个 trace 就完成了编译,pin 将 CPU 的控制权交给这个编译后的代码。

 

现在 CPU 在编译后的代码手中,开始执行 BBL1' 中的代码,执行到 BBL1' 的最后时发现它不是跳转到 BBL2' 而是应该跳转到 BBL3,此时 code cache 中还没有 BBL3,所以它只能将 CPU 的控制权交还给 pin,pin 到源目标程序中从 BBL3 开始再取一个 trace 编译后放到 code cache 中,如果下图所示,BBL3,5,6 组成了一个新的 trace,然后将 CPU 控制权交还给编译后的代码,继续执行,以后的过程就是重复进行这个编译-执行-控制权转移的过程,编译后的代码一直会留在 code cache 中,下次再用到就不用再编译了。

 

pin 优化手段🚿

为了更好的执行,pin 内部做了很多的优化,这里举了几个例子。

trace linking:pin 对跳转预测的优化 [3] ;

register re-allocation:pin 对寄存器分配的优化;

内联优化:如下图所示,将插入的函数放进编译生成的 trace 中,而不是多次跳转执行。

 

 

小结📁

这部分简单介绍了 pin 的内部架构和运行机制,有了这些知识,相信读者使用 pin 将会更加得心应手。

pin 总结📘

pin 是 intel 提供的一款程序插桩工具,应用广泛,intel 内部就开发了多款基于 pin 的工具,如Opcodemix,它可以用来分析程序包含的不同指令组合数目,比如可以比较两款编译器编译生成的文件的指令组合的差异;PinPoints,它是在 SimPoint 的基础上可以用来分析大型商用程序的热点区域 (representative regions) 的信息等等。pin 大约在 2005 年左右推出,有不少相关的论文,有兴趣的话可以找来看一看。

本文的创作基于 pin 的官方文档、PPT 教程和论文,简单介绍了 pin 的功能、使用方法以及基础架构,可能有不够准确的地方,希望大家可以批评指正。🎉

References📤

[1] pin.https://software.intel.com/sites/landingpage/pintool/docs/98484/Pin/html/. 2021.

[2] San Jose, CA and New Brunswick, NJ. PPT 6.7MB. CGO 2012/ISPASS 2012.

[3] Chi-Keung Luk, Robert Cohn, etc. Pin: Building Customized Pregram Analysis Tools with Dynamic Instrumentation. PLDI, 2005.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值