DllMain unter Linux

Windows-API Nutzer kennen natürlich die globale Funktion DllMain, die vom Betriebssystem automatisch beim Laden und Entladen einer DLL ausgeführt wird. (Dass das auch bei jedem neuen Thread geschieht lassen wir hier mal unter den Tisch fallen.)

Und Linux? Kann man auch in Linux erzwingen, dass Code ausgeführt wird, ohne dass explizit eine Funktion aufgerufen wird?


Ein erfahrener C++ Entwickler käme jetzt auf die Idee, dass “globale” Objekte die Lösung wären.
Denn der Standard garantiert, dass diese vor dem Aufruf von main konstruiert und nach dem Verlassen von main zerstört werden. Also etwas Code in den Konstruktor und Destruktor und wir sind fertig …

Allerdings steht im C/C++ Standard nichts über DLLs oder SOs, also Bibliotheken, die erst zur Laufzeit geladen und entladen werden.

Zum Glück haben alle mir bekannten Compiler Hersteller diesen undefinierten Bereich genutzt und mit jener Regel ausgefüllt:

Globale Objekte werden beim Laden die Bibliothek konstruiert noch bevor die Lade-Routine (LoadLibrary bzw. dlopen) zurückkehrt. Und sie werden ebenso zerstört, bevor das Entladen (FreeLibrary bzw. dlclose) abgeschlossen ist.

Natürlich gibt es keine Reihenfolge, wie das ablaufen soll, außer dass das Ausnullen von globalen Datenfeldern noch vor den Konstruktoren stattfindet.

Doch Betriebssysteme wissen nichts von C++ Objekten, also wie läuft das intern ab?

Nun, ein Blick in die Implementierung der C Runtime hilft einem da weiter:

Windows

Unter Windows führt das System in Wahrheit nicht DllMain aus, sondern CRTDllMain. Und diese Funktion implementieren alle Windows Compiler automatisch mit Code, der die Liste der globalen Objekte durchgeht und deren Konstruktoren aufruft, nachdem elementare CRT Initialisierungsroutinen durchgelaufen sind.
Ist das erledigt, wird die “programmierbare” DllMain(..., DLL_PROCESS_ATTACH, ...) aufgerufen.

Beim Entladen läuft das ganze rückwärts ab. CRTDllMain(..., DLL_PROCESS_DETACH, ...) ruft zuerst “unsere” DllMain auf, um im Anschluss alle globalen Objekte per Destruktor-Funktion zu zerstören.

Linux

Das unter Linux verbreitete ELF Format für ausführbare Binärdateien beinhaltet Spezifikationen für besondere Sektionen (.ctors und .dtors). Wenn der Linker dort Funktionen ablegt, werden diese beim Laden und Entladen der Bibliothek hintereinander ausgeführt.

Im GCC erreicht man den Eintrag einer Funktion in solche Sektionen mit den Attributen __attribute__((constructor)) und __attribute__((destructor)).
Linux schreibt selbst also keinen binären Standard vor, doch an dieser Stelle springt der GCC ein und setzt einen de-facto Standard

Es gibt somit keine sichtbare zentrale Funktion (wie DllMain), die man implementieren kann, sondern man schreibt sich einfach selbst Funktionen wie:

 1void __attribute__((constructor)) SO_init()
 2{
 3  /* do some global initialization */
 4}
 5
 6void __attribute__((destructor)) SO_uninit()
 7{
 8  /* do some global cleanup */
 9}

Für globale C++ Objekte legt der GCC automatisch entsprechende Funktionseinträge an.

Wozu braucht man das?

Es herrscht in einigen (fremden) Projekten die Meinung vor:

DllMain sollte nicht benutzt werden, da es nicht auf Linux portiert werden kann.

Nun, das stimmt nicht.

Wenn es möglich ist, sollte man stets vor dem Entladen eines Bibliothek versuchen, Ressourcen freizugeben und Datenverknüpfungen zu lösen.

Natürlich sollte eine “gute” Bibliothek ein init und ein uninit anbieten, doch wer garantiert uns, dass der Anwender diese Funktionen zur rechten Zeit aufruft.

Daher sollte man solche Spezialitäten auch beim Entladen durchführen und zwar für alles, was nicht schon zuvor freigegeben wurde.


Übrigens, kleiner Fun-Fact am Rande:
Da hat sich wohl jemand als Hacker versucht und eine tolle Doku erstellt, wie man sogar eine EXE Datei wie eine DLL laden und nutzen kann.

Das Thema hört sich spannend an und ich nehme es hiermit in meine Liste der “hoch interessanten” Themen auf.