Bei der Entwicklung der C/C++-trace Bibliothek, an der ich gerade arbeite, hatte ich ein kleines aber fieses Problem. Und zwar möchte ich nicht nur Funktionsaufrufe und -returns tracen, sondern auch die Argumente der Funktionen, lokale Variablen im caller- und callee-scope, usw. Und zwar inklusive Typ und Wert. Das funktioniert schon recht passabel. Ich kann Strukturen aufdröseln, Pointern folgen, die visibility und den const-Status der Symbole angeben, …
Stop.
Der geneigte Leser mag in diesem Augenblick bereits erfasst haben, was das fiese Problem an der Sache ist. Die Aussage “Ich kann Pointern folgen” sollte Alarmglocken jedes C-Programmierers schrillen lassen. Zur Erinnerung: Ich trace user-code! Was passiert, wenn ich versuche, uninitialisierten oder ungültigen Pointern zu folgen? …genau: Rumms.
Und das noch-schlimmere daran ist, dass dieses “Rumms” dann nicht nur meine trace-library killt, sondern sie auch noch die Applikation mitreißt, in die sie dynamisch gelinkt wurde – sprich, den User Algorithmus. Das ist… doof.
Also überlegte ich, wie ich das Problem lösen könnte. Es gibt ja leider kein “try / catch” für Segfaults. Nach einem Segfault ist das Programm in einem undefinierten Zustand (Auch, wenn der segfault nur lesend war. Hmpf!). Könnte man das Lesen des Wertes in einen eigenen Thread verlagern? Nein, wenn in einem Thread ein Segfault passiert, reißt es den Eltern-Thread mit sich. Bliebe ein eigener Prozess. Allerdings wäre es ein viel zu hoher Overhead, jedesmal einen Prozess zu starten, wenn man versuchen möchte, einem Pointer zu folgen. Aber man muss ihn ja nicht jedesmal dann starten…!
Meine derzeitige Lösung sieht wie folgt aus:
Zu Beginn, also wenn die Trace-Bibliothek geladen wird, wird in ein neuer Thread gestartet. Dieser forkt sofort einen Kindprozess, den “Victim” (Opfer). Heißt so, weil der arme Kerl höchstwahrscheinlich sehr bald sterben wird. Der Elternprozess (also der Thread der ctrace-library, im Folgenden “Watchdog”), der Victim sowie der ctrace-Hauptthread mappen dann ein Stück Shared Memory. Hier findet, von Mutexen geschützt, die Interprozesskommunikation statt. Der Victim fängt in einer Endlosschleife an, auf Arbeit zu warten, und der Watchdog fängt an, den Victim zu überwachen.
Komme ich in der libctrace an eine Stelle, wo ich einem (User-)Pointer folgen muss, übergebe ich die Adresse und Länge an den Victim. Dieser versucht, den Speicherinhalt in das Shared Memory zu kopieren. Klappt dies, setzt er ein Flag, der Hauptthread liest den Wert aus, und der Victim wartet auf den nächsten Job. Crasht der Victim allerdings beim Kopieren, startet der Watchdog einen neuen Victim-Prozess und meldet den Fehler an den Hauptthread.
Auf diese Weise tritt ein größerer Overhead nur genau dann auf, wenn ein Lesevorgang scheitert, da dann ein neuer Prozess geforked werden muss.
Vielleicht geht das Ganze noch eleganter, aber die Lösung funktioniert, und das auch sehr gut!
Bis zum nächsten Hack…
- Tim