Unde venis?

Modulare Programme haben in der Regel zentrale Loggingfunktionen. Nun lässt sich zwar alles wunderschon loggen, doch die wichtigste Information fehlt in der Regel – welches Plugin hat diesen Logeintrag erzeugt?

Ich bin auf die Problematik gestoßen, als mich Timo gefragt hat, wie man herausfinden könnte, von welcher DLL ein Call kommt. Da ich zu dem Zeitpunkt selber gerade dabei war, die Modulfunktionen von Harmony zu entwerfen, habe ich mir darüber ein wenig Gedanken gemacht.

Man kann man natürlich den Log Aufruf mit einem weiteren Parameter ausstatten, um den Pluginnamen anzugeben. Simpel und funktioniert natürlich auch. Aber es geht noch eleganter!

Einige Theorie vorweg:
Um herauszufinden von welcher Dll ein Call kommt benötigt man die Speicheradresse, von der der Aufruf kommt. Wenn man dann noch eine Liste aller geladenen Module hat, lässt sich der Name relativ leicht herausfinden. Die Frage ist daher: Wie kommt man an die Adresse des “Aufrufers”?
Windows (im 32-Bit Modus) und somit auch Ragnarok basieren auf der x86 Prozessorarchitektur. Auch wenn man ein 64-Bit Windows verwendet, denn dann wird über WOW64 eine 32 Umgebung “emuliert”.
Ein Funktionsaufruf wird über den call Befehl ausgeführt, ein Rücksprung über ret. Ein call schreibt die Speicheradresse des Befehls nach dem Aufruf auf den Stack und springt zur Funktionsadresse. Ein ret holt sich den obersten Wert vom Stack und springt dorthin zurück. Soweit ganz simpel.
Lokale Variablen innerhalb der Funktion werden ebenfalls über den Stack realisiert. Man kann folglich nicht davon ausgehen, dass das oberste Element die Rücksprungadresse ist und braucht eine Möglichkeit, diese von jeder Position aus zu finden. An dieser Stelle kommt das ebp Register ins Spiel: Das ebp verweist auf die Stackposition, an der sich die Funktion am Anfang befunden hat. Über den Wert von ebp sollte man daher doch relativ leicht an die Rücksprungadresse kommen…?
Mir war der Aufbau eines C(++) Calls bereits bekannt, aber solche Informationen lassen sich auch über Wikipedia oder normale Google Recherche herausfinden.

Testcode:

Die Ausgabe sieht schonmal vielversprechend aus. Die Rücksprungadressen liegen im Bereich der main() Funktion. Dass sich die Adressen immer ändern hängt mit der Address Space Layout Randomization (ASLR) zusammen, wodurch die Basisadresse des Programms sich bei jedem Start verändert. Nichtsdestotrotz haben wir nun eine gültige Rücksprungadresse!

Nun müssen wir dieser Basisadresse den Namen eines Plugins zuordnen. In der Regel sind Plugins eigenständige Dll Dateien. Über die Compilerdokumentation findet man relativ schnell heraus, dass Dlls im Speicher auf 64Kb Adressen alignt sind – die Basisadresse also ein Vielfaches von 64Kb ist.
Eine 32 Bit Applikation kann maximal 4Gb Speicher adressieren. Normalerweise stehen dem Programm nur 2Gb zur Verfügung, da 2Gb für den Kernel reserviert sind. Allerdings gibt es die /3GB Option und unter 64-Bit Windows sieht das Ganze wieder anders aus. Wir müssen also davon ausgehen, dass wir 4Gb / 64Kb = 65536 mögliche Dll Basisadressen haben. Wenn wir im Programm ein Array verwenden, wo für jede mögliche Basisadresse ein Pointer auf den Namen hinterlegt ist, benötigt dieses array 64Kb * 32 Bit = 256 Kb. Durchaus vertretbar.

Problematisch könnte es nun werden, wenn eine Dll größer als 64Kb ist, also effektiv mehrere Basisadressen einnehmen würde. Um diesen Fall abzuhandeln muss beim Laden der Dll aus dem PE Header die SizeOfImage ausgelesen werden:

The size (in bytes) of the image, including all headers, as the image is loaded in memory.

Mit dieser Information kann man einfach ausrechnen wie viele Basisadressen belegt werden.

Wir sind also in der Lage die Rücksprungadresse einer Funktion zu ermitteln und haben ein Array in dem die Namen der Dlls hinterlegt sind. Daraus könnte dann beispielsweise das folgende Testprogramm resultieren: Klick mich.

Ziel erreicht!
Ein paar Tage nach der Entstehung dieser Zeilen bin ich über die MSDN Library auf eine sehr interessante Funktion des Microsoft Compilers gestoßen: _ReturnAddress(). Damit hätte ich mir den ganzen Aufwand sparen können – aber so ist immerhin dieser Artikel herausgekommen, der hoffentlich ein paar Anregegungen geben konnte. :)

Übrigens:
_ReturnAddress:

Meine Version:

Microsoft hat wohl den selben Ansatz verfolgt ;)

Leave a Reply