Assembler für C++-Programmierer

Heute muss (fast) niemand mehr in der Assemblersprache programmieren. Selbst hardware-nahe Programme, wie Betriebssysteme und Treiber, werden inzwischen zum größten Teil in Hochsprachen wie C oder C++ geschrieben.

Low level debugging

Aber zum Debuggen ist es immer noch hilfreich, wenn man die Assemblersprache bzw. Maschinencode lesen kann. Mit der Ansicht des erzeugten Maschinen-Code kann man sehen, welche Anweisungen der Computer ausführt bzw. welche Anweisungen der Compiler generiert hat. Nur wenn man den erzeugten Assembler-Code lesen kann, kann man überprüfen, ob der Compiler den C/C++-Code genau so verstanden hat, wie der man ihn schreiben wollte. Bestimmte Seiteneffekte kann man ebenfalls nur durch Lesen des generierten Maschinencode in Assemblersprache überprüfen. Wenn man z.B. mit verschieden Programmiersprache arbeitet und die Parameter werden nicht korrekt übergeben, werden meistens die Calling Convention nicht korrekt beachtet oder die Typen werden nicht korrekt umgesetzt. Die Funktionsaufrufen sehen in der jeweiligen Hochsprache korrekt aus. Erst im gemeinsamen Nenner, des generierten Maschinencode, können die Probleme analysiert werden.

Dabei wird der Entwickler von modernen Debuggern/Entwicklungsumgebungen wie z.B. dem Visual Studio unterstützt. So kann man z.B. im Debugger des Visual Studios den erzeugten Maschinencode während des debuggens betrachten. (Short-Cut: "<Alt>+8" Disassembly window)

Mit dem Wechsel der Ansicht wechselt gleichzeitig das Verhalten des Debuggers:

  • In der C/C++-Ansicht bedeutet ein Schritt, eine Zeile des C/C++-Programms.
  • In der Maschinencode-Ansicht wird jede Maschinenanweisung als ein Schritt umgesetzt.

Da eine Zeile C/C++-Code vom Compiler in mehrere Maschinen-Code Anweisungen umgesetzt wird, benötigt man in der Maschinencode-Ansicht mehrere Schritte um zur nächsten C/C++-Zeile zu gelangen. Was hier recht kompliziert klingt, funktioniert in der Praxis ganz intuitiv. Das Verständnis dieser Funktionsweise und ein paar Tastenkürzel helfen das Debuggen effizient zu gestallten. So kann man z.B. :

  • im Maschinecode überprüfen ob eine Zeile C/C++-Code einen Funktionsaufruf generiert hat oder ob die Funktion inline erzeugt wurde,
  • durch das Umschalten in die (C/C++-)Quellcode-Ansicht ganz schnell einige Schritte im Maschinencode überspringen, oder
  • fehlerhaftes Verhalten im Sourcecode-Debugger umgehen. Das Visual Studio (2008) überspringt in seltenen Situation mehrere Zeilen wenn eine Funktion schrittweise ge"debug"t. Das schrittweise Debuggen im Maschinencode funktioniert zuverlässiger - ist jedoch aufwändiger.

Basiswissen Assembler

Die folgenden Texte gehen von der 32-Bit Programmierung unter Windows mit Hilfe eines C++-Compilers aus. Die grundlegenden Register und Assembler-Anweisungen sollten bekannt sein oder finden sich hier:

Lesen des Maschinen-Codes

Ein Compiler erzeugt Maschinencode nach gewissen Regeln. Diese sorgen dafür, dass die Objektdateien die von einem Compiler erzeugt wurden, auch von einem anderen Compiler verwendet werden können. (Siehe: Aufrufkonventionen) Gleichzeitig hilft ein Verständnis dieser Regeln den erzeugten Code zu lesen.

Jeder Code in einem C/C++-Programm befindet sich in irgendeiner Funktion. Und diese Funktion wird von einer anderen Funktion aufgerufen. Daher sollte man den Aufbau einer Funktion im Assembler-Code verstanden haben, um Maschinencode lesen zu können. Außerdem wird dieses Wissen benötigt um den Zugriff auf lokale Variablen, Parameter und Membervariablen zu verstehen.

Aufbau von Funktionen im Maschinencode.