Windows mit C++: x64-Debugging mit Pseudovariab...
Windows mit C++
x64-Debugging mit Pseudovariablen und Formatbezeichnern
Kenny Kerr
Viele Jahre lang enthielt Visual C++ einen Satz an Pseudovariablen und Formatbezeichnern für das Debuggen. Mit Pseudovariablen sind Begriffe gemeint, die in ein Debuggerüberwachungsfenster eingegeben werden können, damit ein Wert angezeigt wird, der sich nicht unbedingt auf eine C++-Variable bezieht. Leider wurden Pseudovariable nie gut dokumentiert. Obwohl ich nicht über genügend Insiderwissen verfüge, um diese Variablen selbst zu dokumentieren, möchte ich Ihnen einige der Pseudovariablen und Formatbezeichner aufzeigen, die ich am nützlichsten finde. Vielleicht fühlen Sie sich durch diese Diskussion inspiriert, mehr zu diesem Thema zu lesen.
Bevor Pseudovariable (manchmal auch Pseudoregister genannt) vorgestellt werden, muss etwas zu Prozessorarchitekturen und Registern gesagt werden, da Pseudovariable mit ein wenig Hintergrundwissen sehr viel wertvoller sein können. Außerdem können Sie dadurch verstehen, auf welche Weise sich die 64-Bit-Versionen Ihrer Anwendung von traditionellen 32-Bit Anwendungen unterscheiden. Da es in diesem Artikel mehr um Visual C++ als um die Einzelheiten von Prozessorarchitekturen geht, wird diese Diskussion auf x86- und x64-Prozessorarchitekturen beschränkt. Weitere Informationen zu den Unterschieden, die mit dem Itanium-Prozessor eingeführt wurden, finden Sie in der Dokumentation der MSDN-Bibliothek.

Prozessorarchitekturen
Für einfache Sterbliche ist es schwer, das Konzept von Prozessorarchitekturen vollständig zu begreifen. Deswegen hatten die Sprache C und ihre Nachfolger solch weit greifende Auswirkungen auf die Softwarebranche. Sie sollten zumindest eine Vorstellung davon haben, wofür die x86- und x64-Prozessorarchitekturen stehen, da die große Mehrheit der Benutzer genau diese Prozessoren besitzt. Windows und Visual C++ unterstützen auch den Itanium-Prozessor, aber sehr wenige Entwickler genießen den Luxus, dafür Programme zu entwickeln.
Über die letzten Jahrzehnte hinweg hat der x86 wohl oder übel die Computerwelt beherrscht. Er lässt sich auf den 8080-Prozessor mit 8 Bit von Intel zurückverfolgen. AMD und Intel haben mit dem x64 eine pragmatische Wahl getroffen, um die Abwärtskompatibilität mit dem x86 beizubehalten. Mit Itanium hingegen wurde eine leistungsfähige neue Architektur eingeführt, die durch das Vermächtnis des x86 nicht eingeschränkt wird. Gewiss werden dadurch weit weniger Anwendungen unterstützt. Doch dieser Prozessor steht für die Vision von David Cutler (der die Entwicklung von Windows NT geleitet hat) zu einem portablen Betriebssystem, durch die Windows im Lauf der Jahre relativ problemlos neue Architekturen annehmen konnte.
Viele der Unterschiede zwischen den Prozessorarchitekturen sind hinter dem C++-Compiler verborgen. Doch Entwickler können aufgrund des Schritts hin zu einer einzelnen Aufrufkonvention einen deutlichen Vorteil sehen. Die x64-Version des C++-Compilers unterstützt nur eine einzelne Aufrufkonvention, während der x86-Compiler mehrere unterstützt. Dies ist eine willkommene Änderung. Dadurch werden nämlich viele der potenziellen Fehler beseitigt, die durch Nichtübereinstimmung von Aufrufkonventionen entstehen. Das gilt insbesondere für das Zusammenwirken von verwaltetem Code, bei dem die ursprünglichen Aufrufkonventionen in den C++-Headerdateien nicht von Microsoft .NET Framework-Compilern wie C# und Visual Basic .NET geprüft werden können. Stattdessen werden manuell erstellte Attribute herangezogen, die leicht falsch definiert werden können, was zu verschiedenen Stapelbeschädigungsfehlern führt.
Das ecx-Register z. B. wird von der systemeigenen C++-Aufrufkonvention verwendet, die als „thiscall“ bekannt ist, um den Zeiger „this“ zu speichern, bevor eine Memberfunktion aufgerufen wird. Diese Informationen sind beim Debuggen nützlich, hängen aber von der Aufrufkonvention ab, die möglicherweise durch die alleinige Betrachtung des Assemblycodes und gewisser Registerwerte nicht ersichtlich wird. Auf dem x64 ist dies sehr viel einfacher, da der this-Zeiger immer als erster Parameter eingefügt und folglich im rcx-Register gespeichert wird.
Natürlich ist die Verwendung von Registern beim Debuggen sehr prozessorspezifisch, aber glücklicherweise kann die Beziehung zwischen x86 und x64 ausgenutzt werden. Mit der x86-Architektur wurde das Konzept der Unterregister eingeführt, wobei ein Unterregister aus der unteren Hälfte des jeweiligen Überregisters besteht. „ax“ beispielsweise ist ein 16-Bit-Unterregister, das aus der unteren Hälfte des eax-Registers besteht. Dies wird durch die x64-Architektur erweitert, indem 64-Bit-Register bereitgestellt werden, die die 32-Bit-Register des x86 ersetzen. Unter x64 gibt es z. B. das 32-Bit-Unterregister „eax“, das aus der unteren Hälfte des rax-Registers mit 64 Bit besteht. Sowohl „eax“ als auch „rax“ sind interessant, da sie von x86 bzw. x64 für Zeiger oder Ganzzahlen verwendet werden, die von einer Funktion zurückgegeben werden. Natürlich muss eine Funktion, die unter x86 ausgeführt wird, ein Registerpaar verwenden, um einen 64-Bit-Wert zurückgeben zu können.

Pseudovariable
Die wahrscheinlich gängigste Pseudovariable ist „$err“. Damit wird der letzte Fehlerwert angezeigt, der mit der Funktion „SetLastError“ festgelegt wurde. Mit dem angezeigten Wert wird dargestellt, was von der GetLastError-Funktion zurückgegeben würde.
Mithilfe von Pseudovariablen kann auch der Wert eines Prozessorregisters angezeigt werden. Zum Beispiel wird mit „$ecx“ der Wert des ecx-Registers der x86-Architektur angezeigt. Der neuen Generation von Entwicklern, die .NET verwenden, erscheint es möglicherweise ein wenig rätselhaft, sich mit Prozessorregisterwerten zu beschäftigen. Doch dies kann einerseits bei der Diagnose schwer zu lösender Fehler helfen, andererseits dabei, das Verhalten eines Programms zur Laufzeit nachzuvollziehen. Mit „$ecx“ können Sie die Adresse des Zeigers „this“ anzeigen. Dabei wird davon ausgegangen, dass Sie die systemeigene C++-Aufrufkonvention unter x86 verwenden. Mithilfe von „$eax“ können Sie in beiden Architekturen auch einen 32-Bit-Rückgabewert anzeigen. Es gibt viele weitere Register, die je nach Ihren Debugginganforderungen von Nutzen sein können.
Weitere nützliche Pseudovariable sind „$handles“, womit Kernelobjekten im Prozess die Anzahl offener Handles angezeigt wird, sowie „$user“ zum Anzeigen äußerst ausführlicher Informationen zum aktuellen Prozess und zu Threadtoken. Letztere kann beim Debuggen von Code zu einem Identitätswechsel nützlich sein. In Abbildung 1 sind einige gängige Pseudovariable aufgeführt.
Pseudovariable Beschreibung
$handles Anzahl der Handles für Kernelobjekte
$vframe Aktuelle Stapelrahmenadresse
$TID Aktueller Threadbezeichner
$registername Inhalte des angegebenen Registers
$clk Zeit in Uhryzklen
$user Informationen zu Prozess- und Threadtoken

Formatbezeichner
Formatbezeichner sind nützlich, wenn die Anzeige des Werts einer Variablen, einer Pseudovariablen oder eines Ausdrucks in einem Überwachungsfenster geändert werden soll. In den meisten Fällen findet das Überwachungsfenster das beste Format für den Wert auf der Basis des jeweiligen Typs heraus, doch es gibt Fälle, in denen Sie diese Funktion u. U. außer Kraft setzen müssen. Der Debugger ist möglicherweise nicht in der Lage, die Art der Variablen abzuleiten, oder es liegt lediglich eine Speicheradresse vor, die natürlich keine Typinformationen enthält. Der Formatbezeichner „hr“ z. B. zeigt entsprechend dem Wert „Win32“ oder „HRESULT“ an. Weitere Beispiele umfassen „su“, bei dem der Wert als eine mit Null beendete Unicode-Zeichenfolge angezeigt wird.
Vielleicht möchten Sie den eigentlichen Wert hinter dem formatierten Text feststellen. Mit „!“ ist genau dies möglich. Schließlich können Sie das Überwachungsfenster mit einem Zeiger zwingen, den Bezeichner als Array von „n“ Elementen zu behandeln. Dabei wird ein Komma verwendet, gefolgt von der Anzahl der anzuzeigenden Elemente. In Abbildung 2 sind Formatbezeichner aufgeführt.
Bezeichner Beschreibung
D Dezimalzahl
U Dezimalzahl ohne Vorzeichen
O Oktalzahl
X Hexadezimalzahl
F Gleitkommazahl
E Wissenschaftliche Schreibweise
C Zeichen
S Zeichenfolge
Su Unicode-Zeichenfolge
s8 UTF-8-Zeichenfolge
Hr Fehlercode „HRESULT“ oder „Win32“
wc Windows-Klasse
wm Windows-Nachricht
! Rohformat

Visualisieren von Aufrufkonventionen
Sehen Sie sich jetzt, nachdem die Hintergrundinformationen abgehandelt sind, einige Beispiele an. Nehmen Sie beispielsweise den unten angezeigten Codeausschnitt. Der x86-Compiler verwendet für Memberfunktionen standardmäßig die Aufrufkonvention „__thiscall“. Dadurch wird angezeigt, dass die Parameter auf den Stapel verschoben werden und dass der Zeiger „this“ im ecx-Register gespeichert wird:
struct Sample
{
    size_t m;
    Sample() : m(0x11223344) {}

    size_t Method(size_t p1, size_t p2)
    {
        return 0x44556677;
    }
};
In Abbildung 3 wird dies und Weiteres veranschaulicht. Im Überwachungsfenster werden interessante Variable angezeigt. Von „$vframe“ wird die Adresse des aktuellen Stapelrahmens bereitgestellt. Diese Pseudovariable bietet eine prozessorneutrale Möglichkeit, die Adresse aus dem Stapelrahmen abzurufen. Als Nächstes folgen die Adressen der beiden Parameter. Deren Werte lauten 0x22334455 und 0x33445566.
Abbildung 3 32-Bit-Stapel und -Register (zum Vergrößern auf das Bild klicken)
Im ersten Speicherfenster wird auch der aktuelle Stapelrahmen angezeigt. Sie können die Werte dieser Parameter auf dem Stapel sehen. Dann kommt das ecx-Register. Dieses wurde als Zeiger zu einem „Sample“ eingesetzt, um die Mitglieder des Objekts anzuzeigen. Im zweiten Speicherfenster wird außerdem das Objekt im Speicher angezeigt. Weiter unten im Stapel sehen Sie die Membervariable. Danach wird mit der Variablen „$eax“ der Wert des Registers angezeigt, in dem das Ergebnis des Funktionsaufrufs gespeichert wird.
Wenn der gleiche C++-Code vom x64-Compiler kompiliert wird, erhalten Sie auffallend unterschiedliche Ergebnisse. Der Compiler nutzt die erhöhte Anzahl der unter x64 verfügbaren Register voll aus, um zu vermeiden, dass Werte unnötig auf den Stapel verschoben werden. Insbesondere werden die ersten vier Zeiger oder Ganzzahlparameter in den Registern „rcx“, „rdx“, „r8“ und „r9“ platziert. Bei Memberfunktionen wird der this-Zeiger als erster Parameter behandelt. Es wird Raum reserviert für den Fall, dass die Funktion vorübergehend Parameter im Stapel speichern muss. Abschließend wird jedes Zeiger- oder Ganzzahlergebnis im rax-Register zurückgegeben.
In Abbildung 4 ist dies dargestellt. Mit „$vframe“ wird wieder die Adresse des aktuellen Stapelrahmens angezeigt. Diesmal können Sie sehen, dass alle Werte 64-Bit sind. Als Nächstes folgen die Adressen der beiden Parameter. Obwohl die Parameter mit Registern an die aufgerufene Funktion übergeben wurden, befinden sie sich im aktuellen Stapelrahmen. Dies ist ein Debugbuild. Mithilfe der Funktion wurden die Parameter im Stapel gespeichert. Mit den nächsten drei Variablen, „$rcx“, „$r8“ und „$rdx“ werden die Werte der drei Parameter bereitgestellt, einschließlich des vorangestellten this-Zeigers. Abschließend wird mit „$rax“ der Wert des Registers angezeigt, in dem das Ergebnis des Funktionsaufrufs gespeichert wird.
Abbildung 4 64-Bit-Stapel und -Register (zum Vergrößern auf das Bild klicken)

Fehlercodes
Pseudovariable und Formatbezeichner sind auch bei der Betrachtung von Fehlercodes in verschiedenen Formen und an verschiedenen Speicherorten sehr nützlich. In Abbildung 5 sind einige Beispiele aufgeführt, z. B. „$err,hr“, mit dem der Wert und die Beschreibung für den letzten Fehlercode bereitgestellt werden. Dies entspricht dem Fehlercode „ERROR_VOLMGR_DISK_NOT_EMPTY“ des Volumeschattenkopie-Diensts. Mit der Pseudovariablen „$err“ wird der Wert bereitgestellt. Der Formatbezeichner „hr“ teilt dem Überwachungsfenster mit, den Wert als Fehlercode zu formatieren. Beachten Sie außerdem, dass „hresult“ eine C++-Variable ist und dass im Überwachungsfenster automatisch die bekannte Konstante angezeigt wird, die den entsprechenden Wert repräsentiert. Anhand von „E_INVALIDARG,x“ können Sie sehen, wie sich die automatische Formatierung außer Kraft setzen lässt. In diesem Fall wird das Überwachungsfenster vom Formatbezeichner „x“ angewiesen, die Konstante als hexadezimalen Wert anzuzeigen. Im letzten Beispiel wird gezeigt, wie Sie nahezu jede Adresse in ein Array umwandeln können.
Abbildung 5 Fehlercodes (zum Vergrößern auf das Bild klicken)

Debuggen des Sicherheitskontexts
In Abbildung 6 ist die beeindruckende Pseudovariable „$user“ abgebildet. Sie können sehen, dass Bob die Identität von Alice angenommen hat! Mit dieser Pseudovariablen werden viele Einzelheiten sowohl über den Prozess als auch über die Threadtoken bereitgestellt. Sie ist nützlich zum Debuggen von Identitätswechselproblemen in Client/Serveranwendungen.
Abbildung 6 Die Pseudovariable „$user“ (zum Vergrößern auf das Bild klicken)
Es gibt noch mehr Tricks, die Sie in Überwachungsfenstern von Visual Studio verwenden können, sowie einige besondere Tricks zum Debuggen von C#-Code. Außerdem verfügen die Debuggingtools für Windows über einen umfassenden Satz an Pseudovariablen. Weitere Tipps zum Debuggen finden Sie unter Erweitertes Debuggen unter Windows.

Senden Sie Fragen und Kommentare (in englischer Sprache) an mmwincpp@microsoft.com.

Kenny Kerr ist Softwarespezialist, der auf die Softwareentwicklung für Windows spezialisiert ist. Er schreibt leidenschaftlich gern und unterrichtet Entwickler in den Bereichen Programmierung und Softwareentwurf. Sie erreichen Kenny Kerr unter weblogs.asp.net/kennykerr.

Page view tracker