此文章由机器翻译。
应用程序检测
使用 Pin 分析应用程序
程序分析是在发展过程中的基本步骤。它涉及到分析程序能够轻松确定它将如何在运行时的行为。有两种类型的程序分析:静态和动态。
您将没有运行目标程序,通常在源代码的代码编译期间执行静态分析。Visual Studio 为静态分析提供了大量的优秀工具。大多数现代编译器自动执行静态分析,以确保该程序荣誉这种语言的语义规则,安全优化的代码。虽然静态分析并不总是准确的其主要的好处指出代码的潜在问题之前你运行它,减少的数量会在调试会话并节省了宝贵时间。
运行目标程序时,您将执行动态规划分析。当程序结束时,动态分析仪产生行为信息的配置文件。在 Microsoft.NET 框架中,只是在实时 (JIT) 编译器在运行时,以进一步优化代码执行动态分析,并确保它不会做任何违反的类型系统。
静态分析动态分析的主要优点是它可以确保 100%的代码覆盖率。为了确保这种高代码覆盖率与动态分析,您通常需要多次运行该程序,每次采用不同的输入所以分析采取不同的路径。动态分析的主要优点是它可以产生详细和准确的信息。当您开发和运行.NET 应用程序中或安全的 c + + 应用程序时,这两种分析将自动执行罩,以确保代码荣誉规则的框架下。
在这篇文章重点将是动态规划分析,也被称为分析。有很多种程序,如使用框架事件、 OS 钩子和动态检测过程进行分析。虽然 Visual Studio 提供了一个分析框架,其动态检测能力是目前有限的。为所有,但最简单的动态检测情况,你会需要一个更先进的框架。这是针进场。
针是什么?
针是由英特尔公司开发的动态二进制探测框架 这允许您构建程序分析工具称为 Pintools for Windows 和 Linux 平台。您可以使用这些工具来监视和记录程序的行为,它运行时。然后您可以有效地评估程序如其正确性、 性能和安全的许多重要方面。
您可以与 Microsoft Visual Studio 能够轻松构建和调试 Pintools 集成销框架。在本文中,我将展示如何使用 Visual Studio 的 Pin 来开发和调试简单但有用的 Pintool。Pintool 将检测关键的内存问题,如内存泄漏和双释放分配的内存在 C/c + + 程序中。
为了更好地理解 Pin 的性质,看看完整的定义进行了逐项:
- 框架是代码的赖以你写一个程序的集合。它通常包括一个部分控制执行程序 (例如启动和终止) 的运行时组件。
- 仪器仪表是通过添加或修改代码分析程序的过程 — — 或两者。
- 二进制文件指示所添加的代码或修改二进制形式的机器代码。
- 动态指示仪表流程执行在运行时,当程序正在执行时。
完整的短语"动态二进制探测"是一口,所以人们通常使用 DBI 首字母缩略词。Pin 是 DBI 的框架。
您可以使用 Pin (IA32 和 Intel64) 的 Windows、 Linux (IA32 和 Intel64)、 Mac OS X (IA32 和 Intel64) 和 Android (IA32)。针也支持英特尔至强皮皮微处理器的超级计算机。它不仅支持 Windows,也无缝集成与 Visual Studio。您可以在 Visual Studio 中编写 Pintools 并使用 Visual Studio 调试器进行调试。你甚至可以开发调试 Pin 来使用 Visual Studio 中的无缝扩展。
入门针
虽然针是专有软件,可以下载并使用它免费的非商业用途。针还没有支持 Visual Studio 2013 年,所以我将使用 Visual Studio 2012。 如果您已经安装了 Visual Studio 2012 和 2013 年,你可以创建和打开 Visual Studio 2012 项目从 2013 年开始以及使用的 c + + 库和工具 Visual Studio 2012 从 2013 年开始。
下载从 Pin intel.ly/1ysiBs4。除了文件和二进制文件,针包括大量的样品 Pintools,你会发现在源/工具的源代码。从 MyPinTool 文件夹中,在 Visual Studio 中打开 MyPinTool 解决方案。
检查细节,以确定正确的 Pintool 配置的项目的属性。所有 Pintools 都是 DLL 文件。因此,该项目配置类型应该设置为动态库 (.dll)。你会还必须指定所有标题、 文件、 库和销头文件所需的预处理器符号数目。设置入口点,至 Ptrace_DllMainCRTStartup %4012 正确初始化 C 运行时。指定要导入的主要功能的 /export:main 交换机。
你可以使用正确配置的 MyPinTool 项目或创建一个新项目并配置它自己。您还可以创建包含所需的配置详细信息的属性表,并将其导入到您的 Pintool 项目。
Pin 粒度
引脚允许您将代码插入到特定的地方,在你要检测的程序 — — 通常只是之前或之后执行的特定指令或函数。例如,您可能想要记录所有的动态内存分配,可以检测内存泄漏。
有三个主要层次的粒度对 Pin:例程、 指令和图像。别针也有一个不明显级别 — — 跟踪粒度。跟踪是某一项完全直线的指令序列。它通常以无条件分支。他们有条件跟踪可能包括多个出境点。无条件分支的例子包括电话、 回报和无条件跳转。请注意跟踪恰好只有一个入口点。如果针检测到一个分支到跟踪内的某个位置,它将结束,在该位置的跟踪,并启动一个新的跟踪。
Pin 提供这些仪器粒度,以帮助您选择性能和详细程度之间的适当平衡。因为可能有数十亿的指令,则将检测指令一级可能会导致严重的性能退化。另一方面,在函数级别检测可能是过于笼统,因此,它可能会增加分析代码的复杂性。痕迹帮助你而不会影响性能或详细的仪器。
写 Pintool
现在是时间来写有用的 Pintool。该 Pintool 示例的目的是检测内存解除分配问题 C/c + + 程序共有的。我要去写简单的 Pintool 可以诊断现有程序而无需修改源代码,该代码或重新编译它,因为针执行它在运行时的工作。这里是 Pintool 将检测到的问题:
- 内存泄漏:内存分配,但不是会释放。
- 双重释放:不止一次被释放的内存。
- 释放未分配的内存:释放内存,还没被拨出 (如电话免费并向它传递 NULL)。
为了简化代码,我将假定:
- 该程序的主要功能被名为 main。我不会考虑其他的变种。
- 分配和释放内存的唯一功能是新/malloc 和删除/免费,分别。我不会考虑 calloc 和重新分配,例如。
- 该计划包括一个可执行文件。
一旦你理解代码,你可以对其进行修改并使该工具实用得多。
定义解决方案
若要检测这些内存问题,Pintool 必须监视对分配和取消分配函数的调用。因为 new 运算符在内部,调用 malloc 和 delete 运算符在内部调用自由,我可以只是监测对 malloc 的调用和自由。
每当程序调用 malloc,将记录返回的地址 (NULL 或分配的内存区域的地址)。每当它调用自由,我会与我记录被释放的内存的地址相匹配。如果它已分配但未释放,我会将其标记为释放。然而,如果它被分配和释放,这将是尝试释放它再一次,这表明一个问题。最后,如果没有已分配的内存被释放的记录,那会是试图释放未分配的内存。当该程序将终止时,再查那些已分配但不是释放可以检测内存泄漏的内存区域的记录。
选择粒度
Pin 可以检测一个节目在四个粒度:图像、 例程、 跟踪和指令。哪些是最适合这个 Pintool?虽然任何粒度会做这份工作,我需要选择那个招最小的性能系统开销。在这种情况下,图像粒度会是最好的。一旦加载程序的映像,Pintool 可以找到的 malloc 和免费的代码,在图像内并插入代码分析。这种方式,仪器仪表开销将每-图像而不是,搁浅,每个指令。
若要使用 Pin API,我必须包含 pin。H 头文件中的代码中。Pintool 将会将结果写入一个文件,所以我也要包含 fstream 头文件。我将使用映射 STL 类型来跟踪内存被分配和解除分配。此类型是在地图头文件中定义的。我还会使用绑定流显示提示性消息:
#include "pin.H"
#include <iostream>
#include <fstream>
#include <map>
我将定义三个符号进行函数调用 malloc,自由和主要的名称:
#define MALLOC "malloc"
#define FREE "free"
#define MAIN "main"
这些都是需要的全局变量:
bool Record = false;
map<ADDRINT, bool> MallocMap;
ofstream OutFile;
string ProgramImage;
KNOB<string> OutFileName(KNOB_MODE_WRITEONCE,
"Pintool", "o", "memtrace.txt",
"Memory trace file name");
记录变量指示是否我是里面的主要功能。MallocMap 变量保存每个已分配的内存区域的状态。ADDRINT 类型定义由别针。H 代表一个内存地址。如果与内存相关的值 TRUE 解决这个问题,它已释放。
ProgramImage 变量保存程序映像的名称。最后一个变量是一个旋钮。这是对 Pintool 的命令行开关。针很容易地定义的开关为 Pintool。为每个交换机,定义一个旋钮变量。模板类型参数字符串表示的交换机将值的类型。在这里,旋钮允许您指定通过"o"交换机 Pintool 的输出文件的名称。默认值为 memtrace.txt。
接下来,我要定义分析例程执行代码序列中特定的点。我需要一种分析功能,所界定的图 1,调用 malloc 返回来记录所分配的内存的地址后,只是。此函数接受 malloc 返回的地址,并将返回 nothing。
图 1 RecordMalloc 分析例程调用每次 Malloc 返回
VOID RecordMalloc(ADDRINT addr) {
if (!Record) return;
if (addr == NULL) {
cerr << "Heap full!";
return;
}
map<ADDRINT, bool>::iterator it = MallocMap.find(addr);
if (it != MallocMap.end()) {
if (it->second) {
// Allocating a previously allocated and freed memory.
it->second = false;
}
else {
// Malloc should not allocate memory that has
// already been allocated but not freed.
cerr << "Imposible!" << endl;
}
}
else {
// First time allocating at this address.
MallocMap.insert(pair<ADDRINT, bool>(addr, false));
}
}
每次调用 malloc 时,将调用此函数。然而,我只是感兴趣的内存如果它是程序的一部分。所以我会在记录为 TRUE 时,只记录的地址。如果地址为空,我就会忽略它。
然后该函数确定地址是否已在 MallocMap。如果它是,那么它必须有被以前的分配与释放和,因此,它现在正在重用。如果地址不在 MallocMap 中,我会与假作为插入值,该值指示它还没有被释放。
我将定义另一种分析常规、 示图 2,那会说只是要记录被释放的内存区域的地址免费在调用之前。使用 MallocMap,我可以很容易检测是否已释放的内存被释放或者它还没有被分配。
图 2 RecordFree 分析例程
VOID RecordFree(ADDRINT addr) {
if (!Record) return;
map<ADDRINT, bool>::iterator it = MallocMap.find(addr);
if (it != MallocMap.end()) {
if (it->second) {
// Double freeing.
OutFile << "Object at address " << hex << addr << "
has been freed more than once." << endl;
}
else {
it->second = true; // Mark as freed.
}
}
else {
// Freeing unallocated memory.
OutFile << "Freeing unallocated memory at "
<< hex << addr << "." << endl;
}
}
接下来,我将需要两个更多的分析例程,标记执行并返回的主要功能:
VOID RecordMainBegin() {
Record = true;
}
VOID RecordMainEnd() {
Record = false;
}
分析例程确定的代码以检测程序。我也要告诉 Pin 时执行这些例程。这就是仪器仪表例程的目的。我定义仪表例程,如中所示图 3。每次运行过程中加载图像时,将调用这个例程。当加载程序映像时,我会告诉 Pin 以在适当的位置插入分析例程。
图 3 图像检测例程
VOID Image(IMG img, VOID *v) {
if (IMG_Name(img) == ProgramImage) {
RTN mallocRtn = RTN_FindByName(img, MALLOC);
if (mallocRtn.is_valid()) {
RTN_Open(mallocRtn);
RTN_InsertCall(mallocRtn, IPOINT_AFTER, (AFUNPTR)RecordMalloc,
IARG_FUNCRET_EXITPOINT_VALUE,
IARG_END);
RTN_Close(mallocRtn);
}
RTN freeRtn = RTN_FindByName(img, FREE);
if (freeRtn.is_valid()) {
RTN_Open(freeRtn);
RTN_InsertCall(freeRtn, IPOINT_BEFORE, (AFUNPTR)RecordFree,
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_END);
RTN_Close(freeRtn);
}
RTN mainRtn = RTN_FindByName(img, MAIN);
if (mainRtn.is_valid()) {
RTN_Open(mainRtn);
RTN_InsertCall(mainRtn, IPOINT_BEFORE, (AFUNPTR)RecordMainBegin,
IARG_END);
RTN_InsertCall(mainRtn, IPOINT_AFTER, (AFUNPTR)RecordMainEnd,
IARG_END);
RTN_Close(mainRtn);
}
}
}
IMG 对象表示可执行映像。在图像一级运作的所有引脚功能都开始与照片 *。例如,IMG_Name 返回指定映像的名称。同样,在常规一级运作的所有引脚功能都开始与 RTN_ *。例如,RTN_FindByName 接受的图像和 C 样式字符串,并返回表示的例程,正在 RTN 对象。如果在图像中定义了请求的例程,则返回的 RTN 对象会有效。一旦我找到主要例程的调用 malloc,自由,我可以在适当的地点,使用 RTN_InsertCall 函数插入分析例程。
此函数接受三个强制性参数紧接着数目可变的参数:
- 第一个是我想要的例程的文书。
- 第二个是枚举类型指定在何处插入分析例程的终期。
- 第三是分析例程,以插入。
然后我可以指定要传递给分析例程的参数列表。此列表必须由 IARG_END 终止。若要将 malloc 函数的返回值传递给分析例程,我会指定 IARG_FUNCRET_EXITPOINT_VALUE。若要将免费的函数的参数传递给分析例程,我会指定 IARG_FUNCARG_ENTRYPOINT_VALUE 紧接着免费函数的参数的索引。以 IARG_ * 开头的所有这些值由 IARG_TYPE 枚举定义。对 RTN_InsertCall 的调用必须对 RTN_Open 和 RTN_Close 的调用换行,所以 Pintool 可以插入分析例程。
现在,我已经定义了我分析和仪器的例程,必须定义一个完成例程。这将要求检测程序终止。它接受两个参数,一个是代码参数包含从该程序的 main 函数返回的值。另将在稍后讨论。我已经使用一系列基于 for 循环,使代码更具可读性:
VOID Fini(INT32 code, VOID *v) {
for (pair<ADDRINT, bool> p : MallocMap) {
if (!p.second) {
// Unfreed memory.
OutFile << "Memory at " << hex << p.first << "
allocated but not freed." << endl;
}
}
OutFile.close();
}
所做完成例程中就是遍历 MallocMap 和检测那些还没有被释放的分配。从菲返回标志着在检测过程的结束。
代码的最后一部分是 Pintool 的主要功能。在主函数中,PIN_Init 被称为有 Pin 解析命令行来初始化该旋钮。因为我在寻找功能使用他们的名字,别针有加载程序映像的符号表。我可以通过调用 PIN_InitSymbols 来这。IMG_AddInstrumentFunction 函数注册仪器函数被称为每次加载图像时的图像。
此外,使用 PIN_AddFiniFunction,注册定版功能。请注意,这些函数的第二个参数传递给 v 参数。我可以使用此参数,将任何额外的信息传递给仪器仪表功能。最后,PIN_StartProgram 被调用来启动我分析的程序。这个函数实际上从来没有返回的主要功能。一旦它叫什么,Pin 接管一切:
int main(int argc, char *argv[]) {
PIN_Init(argc, argv);
ProgramImage = argv[6]; // Assume that the image name is always at index 6.
PIN_InitSymbols();
OutFile.open(OutFileName.Value().c_str());
IMG_AddInstrumentFunction(Image, NULL);
PIN_AddFiniFunction(Fini, NULL);
PIN_StartProgram();
return 0;
}
装配所有这些部分的代码构成充分的 func国际 Pintool。
运行 Pintool
你应该能够生成此项目没有任何错误。你还需要一个程序来测试 Pintool。您可以使用下面的测试程序:
#include <new>
void foo(char* y) {
int *x = (int*)malloc(4);
}
int main(int argc, char* argv[]) {
free(NULL);
foo(new char[10]);
return 0;
}
显然,这个程序患有两个内存泄漏和不必要调用一次免费,指示程序逻辑有问题。创建另一个项目,其中包括测试程序。生成项目以生成一个 EXE 文件。
要运行 Pintool 的最后一步是将 Pin 作为外部工具添加到 Visual Studio。从工具菜单中,选择外部工具。将打开一个对话框,如中所示图 4。单击 Add 按钮来添加一个新的外部工具。标题应销和命令应该是 pin.exe 文件的目录。参数包含要传递给 pin.exe 的参数。 -T 开关指定的 Pintool 目录。指定要在两个连字符后检测的程序。单击确定,你应该能够从工具菜单运行 Pin。
图 4 将 Pin 添加到 Visual Studio 使用外部工具对话框
当运行程序时,输出窗口将打印什么你扔在绑定和 cout 的溪流中。绑定流通常在执行过程中从 Pintool 打印的提示性消息。一旦引脚终止,可以通过打开 Pintool 已创建的文件来查看结果。默认情况下,这被称为 memtrace.txt。 当您打开文件时,您应该看到这样的事情:
Freeing unallocated memory at 0.
Memory at 9e5108 allocated but not freed.
Memory at 9e5120 allocated but not freed.
如果你有更复杂的程序,坚持 Pintool 假设,应作为您可能会发现,你并不知道其他内存问题的方法来使用 Pintool,将这些文书。
调试 Pintool
当开发的 Pintool,你将遇到通过大量的 bug。您可以通过添加无缝地调试与 Visual Studio 调试器-pause_tool 开关。此开关的值指定 Pin 将等待它实际上运行 Pintool 之前的秒数。这允许您将 Visual Studio 调试器附加到正在运行 Pintool (这是运行检测的程序的过程相同) 的进程。然后您可以正常调试您的 Pintool。
这里已经开发了 Pintool 假定该映像的名称是 argv 数组索引 6 处。所以如果你想要添加暂停工具开关,图像名称将索引 8 处。 您可以自动完成这写更多的代码。
总结
为了进一步发展你的技能,可以提高 Pintool,因此它可以检测其他种类的悬空指针等野指针的内存问题。此外,Pintool 输出不是代码的十分有用,因为它不会指出哪一部分引起的问题。我会很高兴打印导致问题的变量的名称和在其中声明该变量的函数的名称。这将有助于您轻松地查找和修复 bug 的源代码中。尽管打印函数名称很容易的印刷的变量名是支持的更具挑战性缺乏 Pin。
有很多的引脚,Pintool 和仪表化的程序之间发生的相互作用。它是重要的是理解这些相互作用,开发先进的 Pintools 时。现在,你应该工作通过实例提供的 Pin 以更好地了解它的力量。
Hadi Brais 是一个博士学位 印度研究所的技术德里 (驾御),研究的下一代存储技术的优化编译器设计的学者。他花了他大部分的时间编写的代码在 C / C + + / C# 和挖深到 CLR 和 CRT。他的博客 hadibrais.wordpress.com。联系到他在 hadi.b@live.com。
衷心感谢以下技术专家对本文的审阅:普里蒂 Ranjan 熊猫