本文章是由機器翻譯。

應用程式測試設備

利用 Pin 分析應用程式

Hadi Brais

程式分析是在發展過程中的基本步驟。它涉及到分析程式能夠輕鬆確定它將如何在運行時的行為。有兩種類型的程式分析:靜態和動態。

您將沒有運行目的程式,通常在原始程式碼的代碼編譯期間執行靜態分析。Visual Studio為靜力分析提供了大量的優秀工具。大多數現代編譯器自動執行靜態分析,以確保該程式榮譽這種語言的語義規則,安全優化的代碼。雖然靜態分析並不總是準確的其主要的好處指出代碼的潛在問題之前你運行它,減少的數量會在調試會話並節省了寶貴時間。

運行目的程式時,您將執行動態規劃分析。當程式結束時,動態分析儀產生行為資訊的設定檔。在 Microsoft.NET 框架中,只是在即時 (JIT) 編譯器在運行時,以進一步優化代碼執行動態分析,並確保它不會做任何違反的類型系統。

靜態分析動態分析的主要優點是它可以確保 100%的代碼覆蓋率。為了確保這種高代碼覆蓋率與動態分析,您通常需要多次運行該程式,每次採用不同的輸入所以分析採取不同的路徑。動態分析的主要優點是它可以產生詳細和準確的資訊。當您開發和運行.NET 應用程式中或安全的 c + + 應用程式時,這兩種分析將自動執行罩,以確保代碼榮譽規則的框架下。

在這篇文章重點將是動態規劃分析,也被稱為分析。有很多種程式,如使用框架事件、 OS 鉤子和動態檢測過程進行分析。雖然Visual Studio提供一個分析框架,其動態檢測能力是目前有限的。為所有,但最簡單的動態檢測情況,你會需要一個更先進的框架。這是針進場。

針是什麼?

針是由英特爾公司開發的動態二進位探測框架 這允許您構建程式分析工具稱為 Pintools for Windows 和 Linux 平臺。您可以使用這些工具來監視和記錄程式的行為,它運行時。然後您可以有效地評估程式如其正確性、 性能和安全的許多重要方面。

您可以與 MicrosoftVisual 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。您可以編寫 PintoolsVisual Studio中,並使用Visual Studio調試器進行調試。你甚至可以開發調試擴展用於無縫地使用來自Visual Studio的 Pin。

入門針

雖然針是專有軟體,可以下載並使用它免費的非商業用途。針還沒有支援Visual Studio2013 年,所以我將使用Visual Studio2012。 如果您已經安裝了兩個Visual Studio2012 和 2013 年,你可以創建和打開從 2013 年開始的Visual Studio2012年專案以及使用 c + + 庫和工具的Visual Studio2012年從 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。

將密碼添加到Visual Studio使用外部工具對話方塊
圖 4 將密碼添加到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 熊貓