.init
/.fini
ist nicht veraltet. Es ist immer noch Teil des ELF-Standards, und ich würde sogar behaupten, dass es für immer so bleiben wird. Der Code in .init
/.fini
wird vom Loader/Runtime-Linker ausgeführt, wenn der Code geladen/entladen wird. D.h. bei jedem ELF-Laden (zum Beispiel einer Shared Library) wird der Code in .init
ausgeführt. Es ist immer noch möglich, diesen Mechanismus zu verwenden, um ungefähr dasselbe wie mit __attribute__((constructor))/((destructor))
zu erreichen. Es ist altmodisch, aber es hat einige Vorteile.
Der .ctors
/.dtors
-Mechanismus erfordert beispielsweise die Unterstützung durch das System-RTL/Loader/Linker-Skript. Dies ist bei weitem nicht auf allen Systemen verfügbar, z.B. bei tief eingebetteten Systemen, auf denen der Code direkt auf der Hardware ausgeführt wird. D.h. selbst wenn __attribute__((constructor))/((destructor))
von GCC unterstützt wird, ist es nicht sicher, dass es ausgeführt wird, da es Aufgabe des Linkers ist, dies zu organisieren, und des Loaders (oder in einigen Fällen des Boot-Codes), es auszuführen. Um stattdessen .init
/.fini
zu verwenden, ist der einfachste Weg die Verwendung von Linker-Flags: -init & -fini (z.B. von der GCC-Befehlszeile aus wäre die Syntax -Wl -init my_init -fini my_fini
).
Bei Systemen, die beide Methoden unterstützen, ist ein möglicher Vorteil, dass der Code in .init
vor .ctors
und der Code in .fini
nach .dtors
ausgeführt wird. Wenn die Reihenfolge wichtig ist, ist das zumindest eine grobe, aber einfache Möglichkeit, zwischen Initialisierungs-/Beendigungsfunktionen zu unterscheiden.
Ein großer Nachteil ist, dass man nicht einfach mehr als eine _init
- und eine _fini
-Funktion pro ladbares Modul haben kann und wahrscheinlich den Code in mehr .so
-Dateien fragmentieren müsste, als es erforderlich ist. Ein weiterer Nachteil ist, dass bei Verwendung der oben beschriebenen Linkermethode man die originalen _init- und _fini
-Standardfunktionen (bereitgestellt von crti.o
) ersetzt. Hier wird normalerweise jegliche Initialisierung durchgeführt (auf Linux wird hier z.B. die Initialisierung der globalen Variablenzuweisung durchgeführt). Ein Weg, dies zu umgehen, wird hier beschrieben.
Beachten Sie in dem obigen Link, dass ein Übergang zur originalen _init()
nicht erforderlich ist, da es immer noch vorhanden ist. Der call
im Inline-Assembler ist jedoch x86-Befehlssatz und der Aufruf einer Funktion aus dem Assembler würde für viele andere Architekturen (wie ARM zum Beispiel) völlig anders aussehen. D.h. der Code ist nicht transparent.
.init
/.fini
und .ctors
/.detors
Mechanismen sind ähnlich, aber nicht ganz gleich. Der Code in .init
/.fini
wird "wie er ist" ausgeführt. D.h. Sie können mehrere Funktionen in .init
/.fini
haben, aber es ist meines Wissens nach syntaktisch schwierig, sie vollständig transparent in reinem C dort zu platzieren, ohne den Code in viele kleine .so
-Dateien aufzuteilen.
.ctors
/.dtors
sind anders organisiert als .init
/.fini
. Die .ctors
/.dtors
-Abschnitte sind einfach Tabellen mit Zeigern auf Funktionen, und der "Aufrufer" ist eine vom System bereitgestellte Schleife, die jede Funktion indirekt aufruft. D.h. die Schleifen-Aufrufer können architekturspezifisch sein, aber da sie Teil des Systems sind (falls sie überhaupt vorhanden sind), spielt es keine Rolle.
Der folgende Code fügt neue Funktionszeiger zum .ctors
-Funktionsarray auf praktisch dieselbe Weise hinzu, wie es __attribute__((constructor))
tut (die Methode kann nebeneinander mit __attribute__((constructor))
existieren).
#define SECTION( S ) __attribute__ ((section ( S )))
void test(void) {
printf("Hallo\n");
}
void (*funcptr)(void) SECTION(".ctors") =test;
void (*funcptr2)(void) SECTION(".ctors") =test;
void (*funcptr3)(void) SECTION(".dtors") =test;
Man kann die Funktionszeiger auch zu einem vollständig anderen selbst erfundenen Abschnitt hinzufügen. In einem solchen Fall ist ein modifiziertes Linkerskript und eine zusätzliche Funktion, die die Loader-Schleife .ctors
/.dtors
nachahmt, erforderlich. Aber damit kann man eine bessere Kontrolle über die Ausführungsreihenfolge erreichen, Eingangsargumente und Rückgabecodehandling hinzufügen usw. (In einem C++-Projekt wäre es z.B. nützlich, wenn man etwas vor oder nach den globalen Konstruktoren ausführen müsste).
Ich ziehe __attribute__((constructor))/((destructor))
immer vor, wenn möglich, es ist eine einfache und elegante Lösung, auch wenn es sich wie Betrug anfühlt. Für Baremetal-Programmierer wie mich ist dies einfach nicht immer eine Option.
Einige gute Referenzen im Buch Linkers & loaders.