memoryjs
ist ein NPM-Paket zum Lesen und Schreiben von Prozessspeicher!
Funktionen • Erste Schritte • Verwendung • Dokumentation • Debuggen
TODO:
Dies ist ein Node-Add-on (zuletzt getestet, um auf v14.15.0
zu funktionieren) und erfordert daher zur Verwendung Node-Gyp.
Möglicherweise müssen Sie auch diese Schritte ausführen, um node-gyp
zu installieren und einzurichten.
npm install memoryjs
Bei der Verwendung von MemoryJS sollte der Zielprozess mit der Plattformarchitektur der ausgeführten Node-Version übereinstimmen. Wenn Sie beispielsweise auf einen 64-Bit-Prozess abzielen möchten, sollten Sie versuchen, eine 64-Bit-Version von Node zu verwenden.
Sie müssen außerdem die Bibliothek neu kompilieren und auf die gewünschte Plattform ausrichten. Gehen Sie zum Node-Modulverzeichnis „memoryjs“, öffnen Sie ein Terminal und führen Sie eines der folgenden Kompilierungsskripte aus:
# will automatically compile based on the detected Node architecture
npm run build
# compile to target 32 bit processes
npm run build32
# compile to target 64 bit processes
npm run build64
Wenn Sie planen, dieses Modul mit Node Webkit oder Electron zu verwenden, werfen Sie hier einen Blick auf die Build-Notizen von Liam Mitchell.
const memoryjs = require('memoryjs');
const processName = "csgo.exe";
// sync: open a process
const processObject = memoryjs.openProcess(processName);
// async: open a process
memoryjs.openProcess(processName, (error, processObject) => {});
// sync: get all processes
const processes = memoryjs.getProcesses();
// async: get all processes
memoryjs.getProcesses((error, processes) => {});
// close a process (release handle)
memoryjs.closeHandle(handle);
Im Abschnitt „Dokumentation“ dieser README-Datei erfahren Sie, wie ein Prozessobjekt aussieht.
// sync: find a module
const moduleObject = memoryjs.findModule(moduleName, processId);
// async: find a module
memoryjs.findModule(moduleName, processId, (error, moduleObject) => {});
// sync: get all modules
const modules = memoryjs.getModules(processId);
// async: get all modules
memoryjs.getModules(processId, (error, modules) => {});
Im Abschnitt „Dokumentation“ dieser README-Datei erfahren Sie, wie ein Modulobjekt aussieht.
// sync: read data type from memory
const value = memoryjs.readMemory(handle, address, dataType);
// async: read data type from memory
memoryjs.readMemory(handle, address, dataType, (error, value) => {});
// sync: read buffer from memory
const buffer = memoryjs.readBuffer(handle, address, size);
// async: read buffer from memory
memoryjs.readBuffer(handle, address, size, (error, buffer) => {});
// sync: write data type to memory
memoryjs.writeMemory(handle, address, value, dataType);
// sync: write buffer to memory
memoryjs.writeBuffer(handle, address, buffer);
// sync: fetch memory regions
const regions = memoryjs.getRegions(handle);
// async: fetch memory regions
memoryjs.getRegions(handle, (regions) => {});
Im Abschnitt „Dokumentation“ dieser README-Datei erfahren Sie, welche Werte dataType
haben kann.
// sync: open a named file mapping object
const fileHandle = memoryjs.openFileMapping(fileName);
// sync: map entire file into a specified process
const baseAddress = memoryjs.mapViewOfFile(processHandle, fileName);
// sync: map portion of a file into a specified process
const baseAddress = memoryjs.mapViewOfFile(processHandle, fileName, offset, viewSize, pageProtection);
// sync: close handle to a file mapping object
const success = memoryjs.closeHandle(fileHandle);
Einzelheiten zu den Parametern und Rückgabewerten für diese Funktionen finden Sie im Abschnitt „Dokumentation“ dieser README-Datei.
// sync: change/set the protection on a region of memory
const oldProtection = memoryjs.virtualProtectEx(handle, address, size, protection);
Sehen Sie sich den Abschnitt „Dokumentation“ dieser README-Datei an, um zu erfahren, was protection
sein kann.
// sync: pattern scan all modules and memory regions
const address = memoryjs.findPattern(handle, pattern, flags, patternOffset);
// async: pattern scan all modules and memory regions
memoryjs.findPattern(handle, pattern, flags, patternOffset, (error, address) => {});
// sync: pattern scan a given module
const address = memoryjs.findPattern(handle, moduleName, pattern, flags, patternOffset);
// async: pattern scan a given module
memoryjs.findPattern(handle, moduleName, pattern, flags, patternOffset, (error, address) => {});
// sync: pattern scan a memory region or module at the given base address
const address = memoryjs.findPattern(handle, baseAddress, pattern, flags, patternOffset);
// async: pattern scan a memory region or module at the given base address
memoryjs.findPattern(handle, baseAddress, pattern, flags, patternOffset, (error, address) => {});
// sync: execute a function in a remote process
const result = memoryjs.callFunction(handle, args, returnType, address);
// async: execute a function in a remote process
memoryjs.callFunction(handle, args, returnType, address, (error, result) => {});
Klicken Sie hier, um zu sehen, wie ein Ergebnisobjekt aussieht.
Klicken Sie hier, um Einzelheiten zum Formatieren der Argumente und des Rückgabetyps zu erfahren.
// sync: inject a DLL
const success = memoryjs.injectDll(handle, dllPath);
// async: inject a DLL
memoryjs.injectDll(handle, dllPath, (error, success) => {});
// sync: unload a DLL by module base address
const success = memoryjs.unloadDll(handle, moduleBaseAddress);
// async: unload a DLL by module base address
memoryjs.unloadDll(handle, moduleBaseAddress, (error, success) => {});
// sync: unload a DLL by module name
const success = memoryjs.unloadDll(handle, moduleName);
// async: unload a DLL by module name
memoryjs.unloadDll(handle, moduleName, (error, success) => {});
// sync: attach debugger
const success = memoryjs.attachDebugger(processId, exitOnDetach);
// sync: detach debugger
const success = memoryjs.detachDebugger(processId);
// sync: wait for debug event
const success = memoryjs.awaitDebugEvent(hardwareRegister, millisTimeout);
// sync: handle debug event
const success = memoryjs.handleDebugEvent(processId, threadId);
// sync: set hardware breakpoint
const success = memoryjs.setHardwareBreakpoint(processId, address, hardwareRegister, trigger, length);
// sync: remove hardware breakpoint
const success = memoryjs.removeHardwareBreakpoint(processId, hardwareRegister);
Hinweis: Diese Dokumentation wird derzeit aktualisiert. Weitere Informationen finden Sie im Wiki.
{ dwSize: 304,
th32ProcessID: 10316,
cntThreads: 47,
th32ParentProcessID: 7804,
pcPriClassBase: 8,
szExeFile: "csgo.exe",
modBaseAddr: 1673789440,
handle: 808 }
Die Eigenschaften handle
und modBaseAddr
sind nur beim Öffnen eines Prozesses verfügbar und nicht beim Auflisten von Prozessen.
{ modBaseAddr: 468123648,
modBaseSize: 80302080,
szExePath: 'c:\program files (x86)\steam\steamapps\common\counter-strike global offensive\csgo\bin\client.dll',
szModule: 'client.dll',
th32ProcessID: 10316,
GlblcntUsage: 2 }
{ returnValue: 1.23,
exitCode: 2 }
Dieses Objekt wird zurückgegeben, wenn eine Funktion in einem Remote-Prozess ausgeführt wird:
returnValue
ist der von der aufgerufenen Funktion zurückgegebene WertexitCode
ist der Beendigungsstatus des Threads Bei Verwendung der Schreib- oder Lesefunktionen sollte der Datentypparameter (dataType) auf eine Konstante aus der Bibliothek verweisen:
Konstante | Bytes | Aliase | Reichweite |
---|---|---|---|
memoryjs.BOOL | 1 | memoryjs.BOOLEAN | 0 zu 1 |
memoryjs.INT8 | 1 | memoryjs.BYTE , memoryjs.CHAR | -128 bis 127 |
memoryjs.UINT8 | 1 | memoryjs.UBYTE , memoryjs.UCHAR | 0 bis 255 |
memoryjs.INT16 | 2 | memoryjs.SHORT | -32.768 bis 32.767 |
memoryjs.UINT16 | 2 | memoryjs.USHORT , memoryjs.WORD | 0 bis 65.535 |
memoryjs.INT32 | 4 | memoryjs.INT , memoryjs.LONG | -2.147.483.648 bis 2.147.483.647 |
memoryjs.UINT32 | 4 | memoryjs.UINT , memoryjs.ULONG , memoryjs.DWORD | 0 bis 4.294.967.295 |
memoryjs.INT64 | 8 | n / A | -9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807 |
memoryjs.UINT64 | 8 | n / A | 0 bis 18.446.744.073.709.551.615 |
memoryjs.FLOAT | 4 | n / A | 3.4E +/- 38 (7 Ziffern) |
memoryjs.DOUBLE | 8 | n / A | 1.7E +/- 308 (15 Ziffern) |
memoryjs.PTR | 4/8 | memoryjs.POINTER | n / A |
memoryjs.UPTR | 4/8 | memoryjs.UPOINTER | n / A |
memoryjs.STR | n / A | memoryjs.STRING | n / A |
memoryjs.VEC3 | 12 | memoryjs.VECTOR3 | n / A |
memoryjs.VEC4 | 16 | memoryjs.VECTOR4 | n / A |
Hinweise:
_BE
an den Datentyp an. Zum Beispiel: memoryjs.DOUBLE_BE
.INT64
, UINT64
, INT64_BE
, UINT64_BE
) müssen Sie einen BigInt angeben. Beim Lesen einer 64-Bit-Ganzzahl erhalten Sie einen BigInt.Diese Datentypen werden verwendet, um die Art der Daten zu kennzeichnen, die gelesen oder geschrieben werden.
Beispiel für eine 64-Bit-Ganzzahl:
const value = memoryjs.readMemory(handle, address, memoryjs.INT64);
console.log(typeof value); // bigint
memoryjs.writeMemory(handle, address, value + 1n, memoryjs.INT64);
Vector3 ist eine Datenstruktur aus drei Floats:
const vector3 = { x: 0.0, y: 0.0, z: 0.0 };
memoryjs.writeMemory(handle, address, vector3, memoryjs.VEC3);
Vector4 ist eine Datenstruktur aus vier Floats:
const vector4 = { w: 0.0, x: 0.0, y: 0.0, z: 0.0 };
memoryjs.writeMemory(handle, address, vector4, memoryjs.VEC4);
Wenn Sie eine Struktur haben, die Sie in den Speicher schreiben möchten, können Sie Puffer verwenden. Ein Beispiel dafür finden Sie im Pufferbeispiel.
Um eine Struktur in den/aus dem Speicher zu schreiben/lesen, können Sie structron verwenden, um Ihre Strukturen zu definieren und sie zum Schreiben oder Analysieren von Puffern zu verwenden.
Wenn Sie einen std::string
mit structron
lesen möchten, stellt die Bibliothek einen benutzerdefinierten Typ bereit, der zum Lesen/Schreiben von Strings verwendet werden kann:
// To create the type, we need to pass the process handle, base address of the
// structure, and the target process architecture (either "32" or "64").
const stringType = memoryjs.STRUCTRON_TYPE_STRING(processObject.handle, structAddress, '64');
// Create a custom structure using the custom type, full example in /examples/buffers.js
const Struct = require('structron');
const Player = new Struct()
.addMember(string, 'name');
Alternativ können Sie die Konzentrat- und Auflösungsbibliotheken verwenden, um dasselbe zu erreichen. Ein altes Beispiel dafür finden Sie hier.
Der Schutztyp ist ein Bit-Flag-DWORD-Wert.
Dieser Parameter sollte auf eine Konstante aus der Bibliothek verweisen:
memoryjs.PAGE_NOACCESS, memoryjs.PAGE_READONLY, memoryjs.PAGE_READWRITE, memoryjs.PAGE_WRITECOPY, memoryjs.PAGE_EXECUTE, memoryjs.PAGE_EXECUTE_READ, memoryjs.PAGE_EXECUTE_READWRITE, memoryjs.PAGE_EXECUTE_WRITECOPY, memoryjs.PAGE_GUARD, memoryjs.PAGE_NOCACHE, memoryjs.PAGE_WRITECOMBINE, memoryjs.PAGE_ENCLAVE_THREAD_CONTROL, memoryjs.PAGE_TARGETS_NO_UPDATE, memoryjs.PAGE_TARGETS_INVALID, memoryjs.PAGE_ENCLAVE_UNVALIDATED
Weitere Informationen finden Sie in den Speicherschutzkonstanten von MSDN.
Der Speicherzuordnungstyp ist ein DWORD-Bit-Flag-Wert.
Dieser Parameter sollte auf eine Konstante aus der Bibliothek verweisen:
memoryjs.MEM_COMMIT, memoryjs.MEM_RESERVE, memoryjs.MEM_RESET, memoryjs.MEM_RESET_UNDO
Weitere Informationen finden Sie in der VirtualAllocEx-Dokumentation von MSDN.
Sie können diese Bibliothek verwenden, um entweder einen „String“ oder „char*“ zu lesen und einen String zu schreiben.
In beiden Fällen möchten Sie die Adresse des char-Arrays erhalten:
std::string str1 = "hello";
std::cout << "Address: 0x" << hex << (DWORD) str1.c_str() << dec << std::endl;
char* str2 = "hello";
std::cout << "Address: 0x" << hex << (DWORD) str2 << dec << std::endl;
Von hier aus können Sie diese Adresse einfach zum Schreiben und Lesen des Speichers verwenden.
Beim Lesen einer Zeichenfolge im Speicher gibt es jedoch eine Einschränkung: Da die Bibliothek nicht weiß, wie lang die Zeichenfolge ist, wird sie mit dem Lesen fortfahren, bis sie den ersten Nullterminator findet. Um eine Endlosschleife zu verhindern, wird der Lesevorgang beendet, wenn nach 1 Million Zeichen kein Nullterminator gefunden wird.
Eine Möglichkeit, diese Einschränkung in Zukunft zu umgehen, besteht darin, einen Parameter zuzulassen, mit dem Benutzer die maximale Zeichenanzahl festlegen können.
Beim Musterscannen müssen Flags für die Signaturtypen gesetzt werden. Der Signaturtypparameter muss einer der folgenden sein:
0x0
oder memoryjs.NORMAL
was eine normale Signatur bezeichnet.
0x1
oder memoryjs.READ
, das den Speicher an der Adresse liest.
0x2
oder memoryjs.SUBSTRACT
wodurch die Bildbasis von der Adresse subtrahiert wird.
Um mehrere Flags zu setzen, verwenden Sie den bitweisen ODER-Operator: memoryjs.READ | memoryjs.SUBTRACT
.
Die Bibliothek stellt Funktionen zum Zuordnen, Abrufen eines Handles und Lesen einer speicherzugeordneten Datei bereit.
openFileMapping(fileName)
Weitere Informationen finden Sie in der OpenFileMappingA-Dokumentation von MSDN.
mapViewOfFile(processHandle, fileName)
memoryjs.openFileMapping
constants.PAGE_READONLY
eingestellt.mapViewOfFile(processHandle, fileName, offset, viewSize, pageProtection)
memoryjs.openFileMapping
number
oder bigint
): der Offset vom Anfang der Datei (muss ein Vielfaches von 64 KB sein)number
oder bigint
): die Anzahl der zuzuordnenden Bytes (bei 0
wird die gesamte Datei gelesen, unabhängig vom Offset)Weitere Informationen finden Sie in der MapViewOfFile2-Dokumentation von MSDN.
Informationen zu Seitenschutztypen finden Sie unter Schutztyp.
Wir haben einen Prozess, der eine Dateizuordnung erstellt:
HANDLE fileHandle = CreateFileA("C:\foo.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE fileMappingHandle = CreateFileMappingA(fileHandle, NULL, PAGE_READONLY, 0, 0, "MappedFooFile");
Wir können die Datei einem angegebenen Zielprozess zuordnen und die Datei mit memoryjs
lesen:
const processObject = memoryjs.openProcess("example.exe");
const fileHandle = memoryjs.openFileMapping("MappedFooFile");
// read entire file
const baseAddress = memoryjs.mapViewOfFile(processObject.handle, fileHandle.handle);
const data = memoryjs.readMemory(processObject.handle, baseAddress, memoryjs.STR);
// read 10 bytes after 64KB
const baseAddress = memoryjs.mapViewOfFile(processObject.handle, fileHandle.handle, 65536, 10, constants.PAGE_READONLY);
const buffer = memoryjs.readBuffer(processObject.handle, baseAddress, 10);
const data = buffer.toString();
const success = memoryjs.closeHandle(fileHandle);
Wenn Sie eine speicherzugeordnete Datei lesen möchten, ohne einen Zielprozess zu haben, dem Sie die Datei zuordnen können, können Sie sie mit der globalen Variablen process.pid
dem aktuellen Node-Prozess zuordnen:
const processObject = memoryjs.openProcess(process.pid);
Bei der Remote-Funktionsausführung wird ein Array von Argumenten erstellt und dynamisch Shellcode generiert, der in den Zielprozess eingefügt und ausgeführt wird. Aus diesem Grund kann es zu Abstürzen kommen.
Um eine Funktion in einem Prozess aufzurufen, kann die Funktion callFunction
verwendet werden. Die Bibliothek unterstützt die Übergabe von Argumenten an die Funktion und muss im folgenden Format vorliegen:
[{ type: T_INT, value: 4 }]
Die Bibliothek erwartet, dass es sich bei den Argumenten um ein Array von Objekten handelt, wobei jedes Objekt einen type
hat, der den Datentyp des Arguments angibt, und einen value
, der den tatsächlichen Wert des Arguments darstellt. Die verschiedenen unterstützten Datentypen finden Sie unten.
memoryjs.T_VOID = 0x0,
memoryjs.T_STRING = 0x1,
memoryjs.T_CHAR = 0x2,
memoryjs.T_BOOL = 0x3,
memoryjs.T_INT = 0x4,
memoryjs.T_DOUBLE = 0x5,
memoryjs.T_FLOAT = 0x6,
Wenn Sie callFunction
verwenden, müssen Sie auch den Rückgabetyp der Funktion angeben, der wiederum einer der oben genannten Werte sein muss.
Nehmen wir zum Beispiel die folgende C++-Funktion:
int add(int a, int b) {
return a + b;
}
Sie würden diese Funktion folgendermaßen aufrufen:
const args = [{
type: memoryjs.T_INT,
value: 2,
}, {
type: memoryjs.T_INT,
value: 5,
}];
const returnType = T_INT;
> memoryjs.callFunction(handle, args, returnType, address);
{ returnValue: 7, exitCode: 7 }
Weitere Informationen dazu, was callFunction
zurückgibt, finden Sie in der Dokumentation zum Ergebnisobjekt.
Hinweise: Die Übergabe eines double
als Argument wird derzeit nicht unterstützt, die Rückgabe eines Double jedoch schon.
Vielen Dank an die verschiedenen Mitwirkenden, die diese Funktion ermöglicht haben.
Hardware-Breakpoints funktionieren, indem sie einen Debugger an den Prozess anhängen, einen Breakpoint auf eine bestimmte Adresse setzen und einen Triggertyp deklarieren (z. B. Breakpoint beim Schreiben an die Adresse) und dann kontinuierlich auf das Auftreten eines Debug-Ereignisses warten (und es dann konsequent behandeln).
Diese Bibliothek stellt die Hauptfunktionen bereit, enthält aber auch eine Wrapper-Klasse, um den Prozess zu vereinfachen. Ein vollständiges Codebeispiel finden Sie in unserem Debugging-Beispiel.
Beim Festlegen eines Haltepunkts müssen Sie einen Triggertyp übergeben:
memoryjs.TRIGGER_ACCESS
– Haltepunkt tritt auf, wenn auf die Adresse zugegriffen wirdmemoryjs.TRIGGER_WRITE
– Haltepunkt tritt auf, wenn in die Adresse geschrieben wird Beachten Sie, dass beim Überwachen einer Adresse, die eine Zeichenfolge enthält, der size
der Funktion setHardwareBreakpoint
die Länge der Zeichenfolge sein sollte. Bei Verwendung der Debugger
-Wrapper-Klasse ermittelt der Wrapper automatisch die Größe der Zeichenfolge, indem er versucht, sie zu lesen.
Zusammenfassend:
Bei Verwendung der Debugger
-Klasse:
size
muss nicht an setHardwareBreakpoint
übergeben werdensetHardwareBreakpoint
gibt das Register zurück, das für den Haltepunkt verwendet wurdeBei manueller Nutzung der Debugger-Funktionen:
size
ist die Größe der Variablen im Speicher (z. B. int32 = 4 Bytes). Bei einer Zeichenfolge ist dieser Parameter die Länge der Zeichenfolgememoryjs.DR0
bis memoryhs.DR3
). Es stehen nur 4 Hardwareregister zur Verfügung (bei einigen CPUs sind möglicherweise sogar weniger als 4 verfügbar). Dies bedeutet, dass jeweils nur 4 Haltepunkte gesetzt werden könnensetHardwareBreakpoint
gibt einen booleschen Wert zurück, der angibt, ob der Vorgang erfolgreich warWeitere Informationen zum Debuggen und Hardware-Haltepunkten finden Sie unter den folgenden Links:
Der Debugger-Wrapper enthält die folgenden Funktionen, die Sie verwenden sollten:
class Debugger {
attach(processId, killOnDetach = false);
detach(processId);
setHardwareBreakpoint(processId, address, trigger, dataType);
removeHardwareBreakpoint(processId, register);
}
const hardwareDebugger = memoryjs.Debugger;
hardwareDebugger.attach(processId);
const address = 0xDEADBEEF;
const trigger = memoryjs.TRIGGER_ACCESS;
const dataType = memoryjs.INT;
const register = hardwareDebugger.setHardwareBreakpoint(processId, address, trigger, dataType);
// `debugEvent` event emission catches debug events from all registers
hardwareDebugger.on('debugEvent', ({ register, event }) => {
console.log(`Hardware Register ${register} breakpoint`);
console.log(event);
});
// You can listen to debug events from specific hardware registers
// by listening to whatever register was returned from `setHardwareBreakpoint`
hardwareDebugger.on(register, (event) => {
console.log(event);
});
const hardwareDebugger = memoryjs.Debugger;
hardwareDebugger.attach(processId);
// available registers: DR0 through DR3
const register = memoryjs.DR0;
// int = 4 bytes
const size = 4;
const address = 0xDEADBEEF;
const trigger = memoryjs.TRIGGER_ACCESS;
const dataType = memoryjs.INT;
const success = memoryjs.setHardwareBreakpoint(processId, address, register, trigger, size);
const timeout = 100;
setInterval(() => {
// `debugEvent` can be null if no event occurred
const debugEvent = memoryjs.awaitDebugEvent(register, timeout);
// If a breakpoint occurred, handle it
if (debugEvent) {
memoryjs.handleDebugEvent(debugEvent.processId, debugEvent.threadId);
}
}, timeout);
Hinweis: Eine Schleife ist nicht erforderlich, z. B. ist keine Schleife erforderlich, wenn Sie einfach warten möchten, bis die erste Erkennung der Adresse erfolgt, auf die zugegriffen oder in die geschrieben wird.
Gehen Sie in das Stammverzeichnis des Moduls und führen Sie einen der folgenden Befehle aus:
# will automatically compile based on the detected Node architecture
npm run debug
# compile to target 32 bit processes
npm run debug32
# compile to target 64 bit processes
npm run debug64
index.js
so, dass das Debug-Modul erforderlich ist Gehen Sie in das Stammverzeichnis und ändern Sie die Zeile in index.js
von:
const memoryjs = require('./build/Release/memoryjs');
Zu folgendem:
const memoryjs = require('./build/Debug/memoryjs');
Öffnen Sie die Projektmappe binding.sln
in Visual Studio, die sich im build
Ordner im Stammverzeichnis des Projekts befindet.
node.exe
Datei fest (z. B. C:nodejsnode.exe
).C:projecttest.js
). Erkunden Sie die Projektdateien in Visual Studio (durch Erweitern von ..
und dann lib
im Projektmappen-Explorer). Header-Dateien können angezeigt werden, indem Sie Alt
gedrückt halten und auf die Header-Dateinamen oben in den Quellcodedateien klicken.
Haltepunkte werden durch Klicken links neben der Zeilennummer gesetzt.
Starten Sie das Debuggen, indem Sie entweder F5
drücken, in der Symbolleiste auf „Debuggen“ und dann auf „Debuggen starten“ klicken oder auf „Lokaler Windows-Debugger“ klicken.
Das Skript, das Sie in Schritt 4 als Befehlsargument festgelegt haben, wird ausgeführt, und Visual Studio hält an den festgelegten Haltepunkten an und ermöglicht Ihnen, den Code Zeile für Zeile durchzugehen und Variablen zu überprüfen.