Intel Pin
简介
Pin 是一个动态二进制插桩工具:
- 支持 Linux, macOS 和 Windows 操作系统以及可执行程序。
- Pin可以通过pintools在程序运行期间动态地向可执行文件的任意位置插入任意代码(C/C++),也可以attach到一个正在运行的进程。
- Pin 提供了丰富的API,可以抽象出底层指令集特性,并允许将进程的寄存器数据等的上下文信息作为参数传递给注入的代码。Pin会自动存储和重置被注入代码覆盖的寄存器,以恢复程序的继续运行。对符号和调试信息也可以设置访问权限。
- Pin内置了大量的样例插桩工具的源码,包括基本块分析器、缓存模拟器、指令跟踪生成器等,根据自己的实际需求进行自定义开发也十分方便。
特点
- 只是它的输入不是字节码,而是可执行文件的执行汇编代码(机械码)。
- Pin 对所有实际执行的代码进行插桩,但是如果指令没有被执行过,就一定不会被插桩。
- 工作在操作系统之上,所以只能捕获用户级别的指令。
- 由于Pintool的编译十分自由,Pin不打算提供多线程插桩的支持。
- 同时有3个程序运行:应用程序本身、Pin、Pintool。
- 应用程序是被插桩的对象、Pintool包含了如何插桩的规则(用户通过API编写的插入位置和内容)
- 三者共享同一个地址空间,但不共享库,避免了冲突。 Pintool 可以访问可执行文件的全部数据,还会与可执行文件共享 fd 和其他进程信息。
基本原理
Pin机制类似Just-In-Time (JIT) 编译器,Trace插桩的基本流程(以动态基本块BBLs为分析单位):
- Pin 会拦截可执行文件的第一条指令,然后对从该指令开始的后续的指令序列重新“compile”新的代码,并执行
- Pin 在分支退出代码序列时重新获得控制权限,基于分支生成更多的代码,然后继续运行。
动态基本块BBLs
通过一个例子来说明动态基本块BBLs与 汇编代码的BB的区别
switch(i)
{
case 4: total++;
case 3: total++;
case 2: total++;
case 1: total++;
case 0:
default: break;
}
上述代码会编译成下面的汇编, 对于实际执行时跳转从.L7
进入的情况,BBLs包括四条指令,但是BB只会包括一条。
Pin会将cpuid, popf and REP prefixed 指令在执行break 成很多BBLs,导致执行的基本块比预想的要多。(主要原因是这些指令有隐式循环,所以Pin会将其拆分成多个BBLs)
Download
- Download the
kit
from Intel website - Because the compatibility problem may you should install pin with archlinux package
Installation
This part is always needed by pintool, for example Zsim, Sniper.
When you meet the following situation, you should consider update your pin version even you can ignore this warning by use flags like -ifeellucky
under high compatibility risk.
shaojiemike@snode6 ~/github/ramulator-pim/zsim-ramulator/pin [08:05:47]
> ./pin
E: 5.4 is not a supported linux release
because this will easily lead to the problem
Install pintool(zsim) by reconfig pin version
- My first idea is try a compatible pin version (passd a simple test pintool, whatever) instead of the old pin.
- Find the suitable simpler pintool can reproduce the situation (old pin failed, but newest pin is passed)
- TODO: build(fix pin2.14 CXX_ABI compatibility bug), test suitability
- debug the pin tool in details (See in another blog)
插桩粒度
- Trace instrumentation mode:以动态基本块BBLs为分析单位
- Instruction instrumentation mode:以指令为分析单位
- 其余粒度/角度,这些模式通过“缓存”插装请求来实现,因此会产生空间开销,这些模式也被称为预先插装。
- Image instrumentation mode: 通过前提知道需要执行的所有代码,能绘制出全局的插桩情况图
- 必须在
PIN_Init
之前调用PIN_InitSymbols
。 - IMG粒度加载和Dynamic library有什么关系?。IMG貌似是可执行文件的等价意思
- 必须在
- Routine instrumentation mode: 理解为函数级插桩,可以对目标程序所调用的函数进行遍历,并在每一个routine中对instruction做更细粒度的遍历。
- 两者都是在IMG加载时提前插桩,所有不一定routine会被执行。
范围image, section, routine, trace, basic block, instruction的含义和运行时关系
- image包含section,section包含routine(理由:SEC_Img(),RTN_Sec(),后面的静态遍历IMG中指令条数的代码也能说明)
- routine指的是程序中的函数或子程序,trace指的是程序执行过程中的一条路径,basic block指的是一段连续的指令序列,其中没有跳转指令,instruction指的是程序中的一条指令。
- 在程序执行时,一个routine可以包含多个trace,一个trace可以包含多个basic block,一个basic block可以包含多个instruction。
for (SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec))
{
for (RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn))
{
// Prepare for processing of RTN, an RTN is not broken up into BBLs,
// it is merely a sequence of INSs
RTN_Open(rtn);
for (INS ins = RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins))
{
count++;
}
// to preserve space, release data associated with RTN after we have processed it
RTN_Close(rtn);
}
}
API
最重要的是
- 插桩机制(instrumentation code):插入位置,在什么位置插入什么样的代码
- 分析代码(analysis code):插入内容,在插桩点执行的代码 (和上一点的区别是什么???)
插桩机制(instrumentation code)
TRACE_AddInstrumentFunction
Add a function used to instrument at trace granularity- BBL粒度插桩相关API
INS_AddInstrumentFunction()
Add a function used to instrument at instruction granularity- 指令粒度插桩相关API
IMG_AddInstrumentFunction()
Use this to register a call back to catch the loading of an image- 插桩不仅可以对每个指令插桩,还可以通过分类筛选后,只对符合要求的指令进行插桩
- 比如,使用
INS_InsertPredicatedCall()
遍历所有的指令
// Forward pass over all instructions in bbl
for( INS ins= BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins) )
// Forward pass over all instructions in routine
for( INS ins= RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins) )
遍历trace内BBLs
// Visit every basic block in the trace
for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl))
{
// Insert a call to docount before every bbl, passing the number of instructions
BBL_InsertCall(bbl, IPOINT_BEFORE, (AFUNPTR)docount, IARG_UINT32, BBL_NumIns(bbl), IARG_END);
}
遍历指令里的memOperands
UINT32 memOperands = INS_MemoryOperandCount(ins);
// Iterate over each memory operand of the instruction.
for (UINT32 memOp = 0; memOp < memOperands; memOp++){
if (INS_MemoryOperandIsRead(ins, memOp)||INS_MemoryOperandIsWritten(ins, memOp)
//xxx
}
Pintool
最重要的是
- 插桩机制(instrumentation code):插入位置,在什么位置插入什么样的代码
- 分析代码(analysis code):插入内容,在插桩点执行的代码 (和上一点的区别是什么???)
Pintool代码
示例分析
// IPOINT_BEFORE 时运行的分析函数
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }
// Pin calls this function every time a new instruction is encountered
VOID InstructionFuc(INS ins, VOID* v)
{
// Insert a call to printip before every instruction, and pass it the IP
// IARG_INST_PTR:指令地址 一类的全局变量???
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}
int main(int argc, char* argv[])
{
// Initialize pin
if (PIN_Init(argc, argv)) return Usage();
// 登记InstructionFuc为以指令粒度插桩时每条指令触发的函数
INS_AddInstrumentFunction(InstructionFuc, 0);
// 登记PrintFuc为程序结束时触发的函数
PIN_AddFiniFunction(PrintFuc, 0);
// 部署好触发机制后开始运行程序
PIN_StartProgram();
return 0;
}
Debug pintool
目标:以样例插桩工具的源码为对象,熟悉pin的debug流程。
以官方教程为例子:
uname -a #intel64
cd source/tools/ManualExamples
# source/tools/Config/makefile.config list all make option
make all OPT=-O0 DEBUG=1 TARGET=intel64 |tee make.log|my_hl
# or just select one: make obj-intel64/inscount0.so
# $(OBJDIR)%$(PINTOOL_SUFFIX) - Default rule for building tools.
# Example: make obj-intel64/mytool.so
测试运行
下面介绍Pin 提供的debug工具:
首先创建所需的-g
的stack-debugger.so
和应用fibonacci.exe
其中OPT=-O0
选项来自官方文档Using Fast Call Linkages
小节,说明需要OPT=-O0
选项来屏蔽makefile中的-fomit-frame-pointer
选项,使得GDB能正常显示stack trace(函数堆栈?)
Debug application in Pin JIT mode
$ ../../../pin -appdebug -t obj-intel64/stack-debugger.so -- obj-intel64/fibonacci.exe 1000
Application stopped until continued from debugger.
Start GDB, then issue this command at the prompt:
target remote :33030
使用pin的-appdebug
选项,在程序第一条指令前暂停,并启动debugger窗口。在另一个窗口里gdb通过pid连接:
$ gdb fibonacci #如果没指定应用obj-intel64/fibonacci.exe
(gdb) target remote :33030 #连接gdb端口
(gdb) file obj-intel64/fibonacci.exe #如果没指定应用, 需要指定程序来加载symbols
(gdb) b main #continue 等正常操作
Pintool添加自定义debug指令
能够在上一小节的debug窗口里,通过自定义debug指令打印自定义程序相关信息(比如当前stack使用大小)
Pintool设置满足条件时break并启动debug窗口
Pintool “stack-debugger” 能够监控每条分配stack空间的指令,并当stack使用达到阈值时stop at a breakpoint。
这功能由两部分代码实现,一个是插桩代码,一个是分析代码。
static VOID Instruction(INS ins, VOID *)
{
if (!EnableInstrumentation) // ROI(Region of interest)开始插桩测量
return;
if (INS_RegWContain(ins, REG_STACK_PTR)) //判断指令是不是会改变stack指针(allocate stack)
{
IPOINT where = IPOINT_AFTER;
if (!INS_IsValidForIpointAfter(ins))
where = IPOINT_TAKEN_BRANCH; //寻找stack空间判断函数插入位置(指令执行完的位置)。如果不是after, 就是taken branch
INS_InsertIfCall(ins, where, (AFUNPTR)OnStackChangeIf, IARG_REG_VALUE, REG_STACK_PTR,
IARG_REG_VALUE, RegTinfo, IARG_END); // 插入OnStackChangeIf函数,如果OnStackChangeIf()返回non-zero, 执行下面的DoBreakpoint函数
INS_InsertThenCall(ins, where, (AFUNPTR)DoBreakpoint, IARG_CONST_CONTEXT, IARG_THREAD_ID, IARG_END);
}
}
所需的两个函数的分析代码如下:
static ADDRINT OnStackChangeIf(ADDRINT sp, ADDRINT addrInfo)
{
TINFO *tinfo = reinterpret_cast<TINFO *>(addrInfo);
// The stack pointer may go above the base slightly. (For example, the application's dynamic
// loader does this briefly during start-up.)
//
if (sp > tinfo->_stackBase)
return 0;
// Keep track of the maximum stack usage.
//
size_t size = tinfo->_stackBase - sp;
if (size > tinfo->_max)
tinfo->_max = size; //更新stack使用大小
// See if we need to trigger a breakpoint.
//
if (BreakOnNewMax && size > tinfo->_maxReported)
return 1;
if (BreakOnSize && size >= BreakOnSize)
return 1;
return 0;
}
static VOID DoBreakpoint(const CONTEXT *ctxt, THREADID tid)
{
TINFO *tinfo = reinterpret_cast<TINFO *>(PIN_GetContextReg(ctxt, RegTinfo));
// Keep track of the maximum reported stack usage for "stackbreak newmax".
//
size_t size = tinfo->_stackBase - PIN_GetContextReg(ctxt, REG_STACK_PTR);
if (size > tinfo->_maxReported)
tinfo->_maxReported = size;
ConnectDebugger(); // Ask the user to connect a debugger, if it is not already connected.
// Construct a string that the debugger will print when it stops. If a debugger is
// not connected, no breakpoint is triggered and execution resumes immediately.
//
tinfo->_os.str("");
tinfo->_os << "Thread " << std::dec << tid << " uses " << size << " bytes of stack.";
PIN_ApplicationBreakpoint(ctxt, tid, FALSE, tinfo->_os.str());
}
OnStackChangeIf
函数监控当前的stack使用并判断是否到达阈值。DoBreakpoint
函数连接debugger窗口,然后触发breakpoint,并打印相关信息。
也可以使用-appdebug_enable
参数,取消在第一条指令前开启GDB窗口的功能,而是在触发如上代码的break时,才开启GDB窗口的连接。
而上述代码中的ConnectDebugger
函数实现如下:
static void ConnectDebugger()
{
if (PIN_GetDebugStatus() != DEBUG_STATUS_UNCONNECTED) //判断是不是已有debugger连接
return;
DEBUG_CONNECTION_INFO info;
if (!PIN_GetDebugConnectionInfo(&info) || info._type != DEBUG_CONNECTION_TYPE_TCP_SERVER) //PIN_GetDebugConnectionInfo()获取GDB所需的tcp连接端口
return;
*Output << "Triggered stack-limit breakpoint.\n";
*Output << "Start GDB and enter this command:\n";
*Output << " target remote :" << std::dec << info._tcpServer._tcpPort << "\n";
*Output << std::flush;
if (PIN_WaitForDebuggerToConnect(1000*KnobTimeout.Value())) //等待其余GDB窗口的连接
return;
*Output << "No debugger attached after " << KnobTimeout.Value() << " seconds.\n";
*Output << "Resuming application without stopping.\n";
*Output << std::flush;
}
Tips for Debugging a Pintool
这部分讲述了如何debug Pintool中的问题。(对Pintool的原理也能更了解
为此,pin使用了-pause_tool n
暂停n秒等待gdb连接。
../../../pin -pause_tool 10 -t /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so -- obj-intel64/fibonacci.exe 1000
Pausing for 10 seconds to attach to process with pid 3502000
To load the debug info to gdb use:
*****************************************************************
set sysroot /not/existing/dir
file
add-symbol-file /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so 0x7f3105f24170 -s .data 0x7f31061288a0 -s .bss 0x7f3106129280
*****************************************************************
注意gdb对象既不是pin
也不是stack-debugger.so
,而是intel64/bin/pinbin
。原因是intel64/bin/pinbin
是pin
执行时的核心程序,通过htop
监控可以看出。
# shaojiemike @ snode6 in ~/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples on git:dev x [19:57:26]
$ gdb ../../../intel64/bin/pinbin
(gdb) attach 3502000
这时GDB缺少了stack-debugger.so
的调试信息,需要手动添加。这里的add-symbol-file
命令是在pin
启动时打印出来的,直接复制粘贴即可。
(gdb) add-symbol-file /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so 0x7f3105f24170 -s .data 0x7f31061288a0 -s .bss 0x7f3106129280
(gdb) b main #或者 b stack-debugger.cpp:94
gef➤ info b
Num Type Disp Enb Address What
1 breakpoint keep y <MULTIPLE>
1.1 y 0x00000000000f4460 <main> # 无法访问的地址,需要去除
1.2 y 0x00007f3105f36b65 in main(int, char**) at stack-debugger.cpp:94
(gdb) del 1.1
(gdb) c
个人尝试: 使用VSCODE调试Pintool
- 想法:VSCODE的GDB也可以attach PID,理论上是可以的
- 实际问题:VSCODE attach pid后,不会stopAtEntry,只会在已经设置好的断点暂停。但是无法访问到
stack-debugger.so
的调试信息,无法设置断点。
构建Pintool
- 首先需要熟悉API
- PinTool 编译需要自身的 Pin CRT(C RunTime)库,这个库是 Pin 提供的,可以在 Pin 安装目录下找到。
需要进一步的研究学习
暂无
遇到的问题
暂无