Schreiben Sie einen Memory Leak Detector für Windows.
Vorwort
(https://vld.codeplex.com/)Dieses Tool wird durch den Austausch der DLL-Schnittstellen, die unter Windows für das Speicher-Management verantwortlich sind, implementiert, um die Speicherzuweisungen und -freigaben zu verfolgen. Daher entschieden wir uns, uns an Visual Leak Detector (im Folgenden VLD genannt) zu orientieren, um ein einfaches Werkzeug zur Erkennung von Speicherlecks zu erstellen und das Verständnis der DLL-Verlinkung zu fördern.
Vorwissen
Das Buch "Linking" erklärt ausführlich die Prinzipien der Verknüpfung von ausführbaren Dateien unter Linux und Windows, wobei das Dateiformat für ausführbare Dateien unter Windows als PE (Portable Executable) bezeichnet wird. Die Erklärung für DLL-Dateien lautet wie folgt:
DLL steht für Dynamic-Link Library, was so viel wie dynamische Linkbibliothek bedeutet, und entspricht dem gemeinsamen Objekt unter Linux. In Windows-Systemen wird dieses DLL-Mechanismus in großem Umfang verwendet, sogar die Struktur des Windows-Kernels ist stark auf das DLL-Mechanismus angewiesen. DLL-Dateien und EXE-Dateien unter Windows sind im Grunde dasselbe Konzept, da sie beide binäre Dateien im PE-Format sind. Der einzige Unterschied besteht darin, dass im PE-Dateikopf ein Symbol vorhanden ist, das angibt, ob es sich um eine EXE- oder eine DLL-Datei handelt. Zudem ist die Dateierweiterung einer DLL-Datei nicht unbedingt .dll; sie kann auch andere Formate wie .ocx (OCX-Steuerelement) oder .CPL (Systemsteuerungsprogramm) haben.
Es gibt zum Beispiel Erweiterungsdateien für Python wie .pyd. In DLL haben wir hier das Konzept der Symbol-Export- und -Importtabelle im Zusammenhang mit der Speicherlecküberwachung.
Symbol Export Table
Wenn ein PE einige Funktionen oder Variablen für andere PE-Dateien freigeben muss, bezeichnen wir dieses Verhalten als Symbol Exporting.
Einfach gesagt werden in Windows PE alle exportierten Symbole in einer Struktur gespeichert, die als Exporttabelle (Export Table) bezeichnet wird. Diese bietet eine Zuordnung zwischen Symbolnamen und Symboladressen. Symbole, die exportiert werden sollen, müssen mit dem Modifizierer __declspec(dllexport)
versehen werden.
Symbolimporttabelle
Die Symbol-Import-Tabelle ist das Schlüsselkonzept hier, das im Gegensatz zur Symbol-Export-Tabelle steht. Lassen Sie uns zuerst die Konzepterklärung betrachten:
Wenn wir in einem Programm Funktionen oder Variablen aus einer DLL verwenden, nennen wir dieses Verhalten Symbolimport.
In Windows PE, the structure that stores the symbols of variables and functions that modules need to import, along with information about their location, is called the Import Table. When Windows loads a PE file, one of the tasks is to determine all the function addresses that need to be imported, and adjust the elements in the import table to the correct addresses. This allows the program, at runtime, to locate the actual address of functions by querying the import table and making the necessary calls. The most crucial structure in the import table is the Import Address Table (IAT), which stores the actual addresses of the imported functions.
Wenn du bis hierher gelesen hast, hast du vielleicht schon eine Ahnung davon, wie wir den Memory-Leak-Detektor implementieren werden :) Richtig, wir hacken die Importtabelle, genauer gesagt, wir ändern die Adressen der Funktionen für Speicherzuweisung und -freigabe in der Importtabelle des Moduls, das wir überwachen möchten, so dass sie auf von uns definierte Funktionen verweisen. Auf diese Weise können wir jedes Mal, wenn das Modul Speicher anfordert oder freigibt, überprüfen, was vor sich geht, und die gewünschten Überprüfungen durchführen.
Für weitere detaillierte Informationen über DLL-Verknüpfungen können Sie "Linkage" oder andere Quellen konsultieren.
Memory Leak Detector
Nachdem ich das Prinzip verstanden habe, folgt nun die Umsetzung der Erkennung von Speicherlecks basierend auf diesem Prinzip. Die folgende Erklärung wird auf meiner eigenen Implementierung basieren, die ich auf meinem GitHub veröffentlicht habe: LeakDetector。
Ersatzfunktion
Lassen Sie uns zunächst die entscheidenden Funktionen betrachten, die sich in RealDetector.cppBitte geben Sie den Text an, den Sie ins Deutsche übersetzen möchten.
Lass uns diese Funktion analysieren. Wie im Kommentar erwähnt, besteht die Funktion darin, die Adresse einer bestimmten Funktion im IAT in die Adresse einer anderen Funktion zu ändern. Schauen wir uns zunächst die Zeilen 34-35 an:
idte = (IMAGE_IMPORT_DESCRIPTOR*)ImageDirectoryEntryToDataEx((PVOID)importModule,
TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &size, §ion);
Die Funktion ImageDirectoryEntryToDataEx
gibt die Adresse einer bestimmten Struktur im Dateikopf eines Moduls zurück. IMAGE_DIRECTORY_ENTRY_IMPORT
bestimmt die Importtabellestruktur, daher zeigt idte
, das zurückgegeben wird, auf die Importtabelle des Moduls.
In Zeile 36-40 wird überprüft, ob "idte" gültig ist. In Zeile 41 zeigt "idte->FirstThunk" auf die tatsächliche IAT. Daher werden in den Zeilen 41-48 die Funktionen des Moduls gesucht, die ersetzt werden müssen, wenn das Modul anhand des Modulnamens nicht gefunden werden kann, bedeutet das, dass die Funktionen dieses Moduls nicht aufgerufen wurden, es wird ein Fehler gemeldet und zurückgegeben.
Nachdem das Modul gefunden wurde, muss natürlich die zu ersetzende Funktion gefunden werden. In den Zeilen 55-62 wird das Modul geöffnet, zu dem die Funktion gehört, und in Zeile 64 die Adresse der Funktion gefunden. Da das IAT keine Namen speichert, muss zunächst die Funktion anhand der ursprünglichen Adresse lokalisiert und dann die Adresse dieser Funktion geändert werden. In den Zeilen 68-80 wird genau das gemacht. Nach erfolgreichem Auffinden der Funktion wird die Adresse einfach in die Adresse des Ersatzes
geändert.
Bis hierhin haben wir erfolgreich die Funktion im IAT ersetzt.
Modul- und Funktionsnamen
Obwohl wir die Ersetzung der IAT-Funktion patchImport
bereits umgesetzt haben, benötigt diese Funktion die Angabe des Modul- und Funktionsnamens. Aber wie können wir herausfinden, welche Module und Funktionen für die Speicherzuweisung und -freigabe des Programms verwendet werden? Um dieses Problem zu klären, müssen wir das Tool Dependency WalkerErstellen Sie in Visual Studio ein neues Projekt und verwenden Sie im main
-Funktion new
, um Speicher anzufordern. Kompilieren Sie die Debug-Version und öffnen Sie dann die kompilierte exe-Datei mit depends.exe
, um eine ähnliche Benutzeroberfläche zu sehen (zum Beispiel mein Projekt LeakDetectorTestUm das zu illustrieren:
You can see that LeakDetectorTest.exe uses malloc
and _free_dbg
from uscrtbased.dll (not shown in the image), these are the functions we need to replace. Please note that the actual module function names may vary depending on your Windows and Visual Studio versions. In my case, I am using Windows 10 and Visual Studio 2015. What you need to do is to use depends.exe to check which functions are actually being called.
Analyse des Aufrufstacks
(https://github.com/disenone/LeakDetector/blob/master/LeakDetector/RealDetector.cpp)。
Speicherleck-Überprüfung
Bis hierhin haben wir alle Drachenbälle gesammelt, jetzt rufen wir offiziell Shenlong herbei.
Ich möchte eine Lösung entwickeln, die eine partielle Erkennung von Speicherlecks ermöglicht (dies unterscheidet sich von VLD, das eine globale Erkennung durchführt und Mehrfach-Threads unterstützt). Daher habe ich auf der Klasse RealDetector
, die die Funktionen tatsächlich ersetzt, eine weitere Schicht in Form von LeakDetector
hinzugefügt und die Schnittstelle von LeakDetector
den Benutzern zugänglich gemacht. Bei der Verwendung muss man lediglich LeakDetector
instanziieren, um die Funktion zu ersetzen und mit der Erkennung von Speicherlecks zu beginnen. Bei der Zerstörung von LeakDetector
wird die ursprüngliche Funktion wiederhergestellt, die Erkennung von Speicherlecks eingestellt und die Ergebnisse der Speicherleck-Erkennung ausgegeben.
Testen Sie es mit dem folgenden Code:
#include "LeakDetector.h"
#include <iostream>
using namespace std;
void new_some_mem()
{
char* c = new char[12];
int* i = new int[4];
}
int main()
{
auto ld = LDTools::LeakDetector("LeakDetectorTest.exe");
new_some_mem();
return 0;
}
Der Code hat direkt Speicher mit new
allokiert, wurde aber nicht freigegeben und ist dann ohne Freigabe beendet, was zu folgendem Programmausdruck führte:
============== LeakDetector::start ===============
LeakDetector init success.
============== LeakDetector::stop ================
Memory Leak Detected: total 2
Num 1:
e:\program\github\leakdetector\leakdetector\realdetector.cpp (109): LeakDetector.dll!LDTools::RealDetector::_malloc() + 0x1c bytes
f:\dd\vctools\crt\vcstartup\src\heap\new_scalar.cpp (19): LeakDetectorTest.exe!operator new() + 0x9 bytes
f:\dd\vctools\crt\vcstartup\src\heap\new_array.cpp (15): LeakDetectorTest.exe!operator new[]() + 0x9 bytes
e:\program\github\leakdetector\leakdetectortest\leakdetectortest.cpp (12): LeakDetectorTest.exe!new_some_mem() + 0x7 bytes
e:\program\github\leakdetector\leakdetectortest\leakdetectortest.cpp (19): LeakDetectorTest.exe!main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (74): LeakDetectorTest.exe!invoke_main() + 0x1b bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (264): LeakDetectorTest.exe!__scrt_common_main_seh() + 0x5 bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (309): LeakDetectorTest.exe!__scrt_common_main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): LeakDetectorTest.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x24 bytes
ntdll.dll!RtlUnicodeStringToInteger() + 0x253 bytes
ntdll.dll!RtlUnicodeStringToInteger() + 0x21e bytes
Num 2:
e:\program\github\leakdetector\leakdetector\realdetector.cpp (109): LeakDetector.dll!LDTools::RealDetector::_malloc() + 0x1c bytes
f:\dd\vctools\crt\vcstartup\src\heap\new_scalar.cpp (19): LeakDetectorTest.exe!operator new() + 0x9 bytes
f:\dd\vctools\crt\vcstartup\src\heap\new_array.cpp (15): LeakDetectorTest.exe!operator new[]() + 0x9 bytes
e:\program\github\leakdetector\leakdetectortest\leakdetectortest.cpp (11): LeakDetectorTest.exe!new_some_mem() + 0x7 bytes
e:\program\github\leakdetector\leakdetectortest\leakdetectortest.cpp (19): LeakDetectorTest.exe!main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (74): LeakDetectorTest.exe!invoke_main() + 0x1b bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (264): LeakDetectorTest.exe!__scrt_common_main_seh() + 0x5 bytes
f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl (309): LeakDetectorTest.exe!__scrt_common_main()
f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp (17): LeakDetectorTest.exe!mainCRTStartup()
KERNEL32.DLL!BaseThreadInitThunk() + 0x24 bytes
ntdll.dll!RtlUnicodeStringToInteger() + 0x253 bytes
ntdll.dll!RtlUnicodeStringToInteger() + 0x21e bytes
The program successfully identified the two memory allocations that were not released, printed the complete call stack information, and thus the required functionality has been completed.
Schlussfolgerung
Wenn du noch nicht mit Programmverlinkung, Laden und Bibliotheken vertraut bist, könntest du verwirrt sein, wie man die Funktionen einer gemeinsamen Linkbibliothek findet, ganz zu schweigen davon, diese Funktionen durch unsere eigenen Funktionen zu ersetzen. Hier nehmen wir als Beispiel die Erkennung von Speicherlecks, um zu erörtern, wie man die Funktionen einer Windows DLL ersetzt. Detailliertere Implementierungen können im Quellcode von VLD nachgelesen werden.
Außerdem möchte ich sagen, dass "Die Selbstverbesserung des Programmierers: Verlinkung, Laden und Bibliotheken" wirklich ein gutes Buch ist, ich äußere lediglich meine Gedanken, ohne Werbung zu machen.
Original: https://wiki.disenone.site/de
This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.
Visitors. Total Visits. Page Visits.
Dieser Beitrag wurde mit ChatGPT übersetzt, bitte geben Sie Ihr FeedbackBitte weisen Sie auf etwaige Auslassungen hin.