Durch den Callstack wandern
« | 19 Feb 2019 | »Verwaltete Sprachen wie
Java oder jene des
dotNet Frameworks definieren
einen Standard wie Funktionen ausgestalten sein müssen.
Damit kann das Framework seine eigenen Funktions-Aufrufbäume genau
zurückverfolgen.
Das merkt man vor allem bei Exception, die ganz genau wissen woher sie kamen.
C und C++ kennen einen solchen Standard für den Aufbau des Callstacks nicht, weil im Zuge der Optimierung Funktionen eingebettet (Inlining) werden können oder andere interne Calling-Conventions benutzt werden dürfen.
Will man aber dennoch “seine Herkunft” zur Laufzeit ermitteln muss man selbst Hand anlegen.
Es kann schon mal sinnvoll sein, wenn ein Exception-Konstruktor auch gleich den Callstack wegspeichert und in Debugging-Logs mitführt. Mit diesem Datensatz kann man in einigen Fällen Probleme feststellen, ohne dass gleich ein Remote-Debugger benutzt werden muss.
Und es ist auch ein Vorteil, dass Signal-Handler oder Structured-Exception-Handler den aktuellen Callstack ausgeben oder zwischenspeichern, bevor der Prozess beendet werden muss.
Linux
Die Funktion backtrace()
ist hier der beste Anlaufplatz, denn sie bildet den Callstack selbst ab.
Die zurückgegebenen Pointer zeigen auf die Funktionseinsprungpunkte und
mit der Funktion backtrace_symbols()
können die Namen der einzelnen
Funktionen im Callstack bestimmt werden, falls sie einkompiliert
wurden. (Sie sind meist nur im Debug-Modus drinnen.)
Ich würde backtrace()
schon fast als “prefekte” API ansehen, weil keine
Details über CPU-Register oder zusätzlicher Assemblercodes erforderlich
sind.
Windows NT
In der 64-bit Variante von Kernel32.dll
befindet sich die Funktion
RtlCaptureContext()
,
die uns alle Register vor Ihrem Aufruf zur Verfügung stellt. Für 32-bit
Prozesse darf man noch den guten alten Inline-Assembler nutzen
und diese Daten selbst beziehen.
Wer gerne mit dem MinGW GCC (32-bit) arbeitet, sollte mit folgenden Zeilen auskommen:
Für das Durchwandern des Callstacks braucht man im allgemeinen Fall
zwingend Dbghelp.dll
, denn in ihr ist die Funktion
StackWalk64()
verborgen, die bei einem Stackframe beginnt und sich weiter zurück arbeiten kann.
Ab Windows Vista
zieht die Funktion
RtlCaptureStackBackTrace()
übergreifend in kernel32.dll
ein, und ist ähnlich komfortabel wie
backtrace()
in der Linux-Welt.
Windows CE
CE hat wieder einen anderen API-Namen gewählt und nutzt
GetThreadCallStack()
um analog zu backtrace()
alle Funktionsadressen im Callstack aufzulisten.
Fazit
Die Möglichkeit den Callstack zur Laufzeit zu speichern sollte stets bedacht und im Framework implementiert sein. Auch wenn in der Praxis C++ Callstacks nur für Experten aussagekräftig sind, so sind sie immer wieder ein gutes Hilfsmittel beim Debuggen.
Selbstverständlich setzt das ein konsequentes Archivieren der Programmsymbole voraus, denn mit Adressen lässt sich wenig anfangen, wenn man nicht weiß, welche namentlich bekannte Funktion damit in Verbindung steht.