Durch den Callstack wandern

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.

size_t * ptrFrame = 0;
size_t ptrStack = 0;
_asm { mov ptrFrame, ebp }
_asm { mov ptrStack, esp }
ctx.Ebp = (DWORD)reinterpret_cast<size_t>(ptrFrame);
ctx.Esp = (DWORD)ptrStack;
ctx.Eip = (DWORD)*(ptrFrame + 1);

Wer gerne mit dem MinGW GCC (32-bit) arbeitet, sollte mit folgenden Zeilen auskommen:

size_t * ptrFrame = 0;
size_t ptrStack = 0;
asm ( "movl %%ebp, %0;" : "=r" ( ptrFrame ) );
asm ( "movl %%esp, %0;" : "=r" ( ptrStack ) );
ctx.Ebp = (DWORD)reinterpret_cast<size_t>(ptrFrame);
ctx.Esp = (DWORD)ptrStack;
ctx.Eip = (DWORD)*(ptrFrame + 1);

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ückarbeiten 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.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!