448 Stimmen

Wie funktioniert __attribute__((constructor)) genau?

Es scheint ziemlich klar zu sein, dass es dazu bestimmt ist, Dinge einzurichten.

  1. Wann genau wird es ausgeführt?
  2. Warum gibt es zwei Klammern?
  3. Ist __attribute__ eine Funktion? Ein Makro? Syntax?
  4. Funktioniert das in C? C++?
  5. Muss die Funktion, mit der es funktioniert, statisch sein?
  6. Wann wird __attribute__((destructor)) ausgeführt?

Beispiel in Objective-C:

__attribute__((constructor))
static void initialize_navigationBarImages() {
  navigationBarImages = [[NSMutableDictionary alloc] init];
}

__attribute__((destructor))
static void destroy_navigationBarImages() {
  [navigationBarImages release];
}

357voto

janneb Punkte 33959
  1. Es läuft, wenn eine gemeinsam genutzte Bibliothek geladen wird, in der Regel beim Programmstart.
  2. Das ist bei allen GCC-Attributen so; vermutlich um sie von Funktionsaufrufen zu unterscheiden.
  3. Spezifische Syntax von GCC.
  4. Ja, das funktioniert in C und C++.
  5. Nein, die Funktion muss nicht statisch sein.
  6. Der Destruktor wird ausgeführt, wenn die gemeinsam genutzte Bibliothek entladen wird, in der Regel beim Programmende.

Also, die Art und Weise, wie die Konstruktoren und Destruktoren funktionieren, ist so, dass die gemeinsam genutzte Objektdatei spezielle Abschnitte enthält (.ctors und .dtors auf ELF), die Verweise auf die mit den Konstruktor- bzw. Destruktorattributen markierten Funktionen enthalten. Wenn die Bibliothek geladen/entladen wird, überprüft das dynamische Ladeprogramm (ld.so oder Ähnliches), ob solche Abschnitte existieren, und ruft gegebenenfalls die darin referenzierten Funktionen auf.

Wenn man genauer darüber nachdenkt, gibt es wahrscheinlich eine ähnliche Magie im normalen statischen Linker, sodass derselbe Code beim Starten/Beenden unabhängig davon ausgeführt wird, ob der Benutzer eine statische oder dynamische Verknüpfung wählt.

64 Stimmen

Die doppelten Klammern machen es leicht, sie "als Makro auszumakro" (#define __attribute__(x)). Wenn Sie mehrere Attribute haben, z.B. __attribute__((noreturn, weak)), wäre es schwierig, sie "als Makro auszumakro", wenn es nur einen Satz von Klammern gäbe.

7 Stimmen

Es ist nicht mit .init/.fini erledigt. (Man kann gültig mehrere Konstruktoren und Destruktoren in einer Übersetzungseinheit haben, geschweige denn mehrere in einer einzigen Bibliothek - wie würde das funktionieren?) Stattdessen werden auf Plattformen, die das ELF-Binärformat verwenden (Linux usw.), die Konstruktoren und Destruktoren in den Abschnitten .ctors und .dtors des Headers referenziert. Früher wurden tatsächlich Funktionen mit den Namen init und fini bei dynamischem Laden und Entladen der Bibliothek ausgeführt, wenn sie vorhanden waren, aber das ist jetzt veraltet und durch diesen besseren Mechanismus ersetzt worden.

0 Stimmen

@ephemient: Danke, ich hatte vergessen, den neuen und verbesserten Weg zu gehen. Antwort entsprechend aktualisiert.

76voto

Michael Ambrus Punkte 924

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

0 Stimmen

Wie kann der Loader diese Funktionen aufrufen? Können diese Funktionen globale Variablen und andere Funktionen im Prozessadressraum verwenden, obwohl der Loader ein Prozess mit eigenem Adressraum ist, oder?

1 Stimmen

@user2162550 Nein, ld-linux.so.2 (der übliche "Interpreter", der Loader für dynamische Bibliotheken, der auf allen dynamisch verknüpften ausführbaren Dateien läuft) läuft im selben Adressraum der ausführbaren Datei selbst. Im Allgemeinen ist der dynamische Bibliotheks-Loader selbst etwas Spezifisches für den Benutzerbereich, der im Kontext des Threads läuft, der versucht auf eine Bibliotheksressource zuzugreifen.

0 Stimmen

Wenn ich execv() aus dem Code aufrufe, der __attribute__((constructor))/((destructor)) hat, wird der Destruktor nicht ausgeführt. Ich habe ein paar Dinge ausprobiert, wie z.B. das Hinzufügen eines Eintrags zu .dtor wie oben gezeigt. Aber ohne Erfolg. Das Problem ist leicht zu reproduzieren, indem man den Code mit numactl ausführt. Zum Beispiel, nehmen wir an, test_code den Destruktor enthält (fügen Sie ein printf zu den Konstruktor- und Destruktor-Funktionen hinzu, um das Problem zu debuggen). Führen Sie dann LD_PRELOAD=./test_code numactl -N 0 sleep 1 aus. Sie werden sehen, dass der Konstruktor zweimal aufgerufen wird, aber der Destruktor nur einmal.

58voto

David C. Rankin Punkte 75069

Diese Seite bietet ein großartiges Verständnis für die Konstruktor- und Destruktor-Attributimplementierung und die Abschnitte innerhalb von ELF, die es ihnen ermöglichen zu funktionieren. Nachdem ich die hier bereitgestellten Informationen verdaut hatte, habe ich ein wenig zusätzliche Informationen zusammengestellt und (nach dem Beispielabschnitt von Michael Ambrus oben ausgeliehen) ein Beispiel erstellt, um die Konzepte zu veranschaulichen und mein Lernen zu unterstützen. Die Ergebnisse werden unten zusammen mit dem Beispielquellcode bereitgestellt.

Wie in diesem Thread erklärt, erstellen die Konstruktor- und Destruktor-Attribute Einträge im .ctor- und .dtor-Abschnitt der Objektdatei. Sie können Verweise auf Funktionen entweder auf drei Arten platzieren. (1) unter Verwendung des section-Attributs; (2) Konstruktor- und Destruktor-Attribute oder (3) mit einem Inline-Assembly-Aufruf (wie im Link in Ambrus' Antwort referenziert).

Die Verwendung der Konstruktor- und Destruktor-Attribute ermöglicht es Ihnen zusätzlich, einem Konstruktor/Destruktor eine Priorität zuzuweisen, um dessen Ausführungsreihenfolge vor dem Aufruf von main() oder nach dessen Rückkehr zu steuern. Je niedriger der gegebene Prioritätswert ist, desto höher ist die Ausführungspriorität (niedrige Prioritäten werden vor höheren Prioritäten vor main() ausgeführt - und nach höheren Prioritäten nach main()). Die Prioritätswerte, die Sie angeben, müssen größer als 100 sein, da der Compiler Prioritätswerte zwischen 0 und 100 für die Implementierung reserviert. Ein Konstruktor oder ein Destruktor mit Priorität wird vor einem Konstruktor oder einem Destruktor ohne Priorität ausgeführt.

Mit dem 'section'-Attribut oder mit Inline-Assembly können Sie auch Funktionsverweise im ELF-Codeabschnitt .init und .fini platzieren, die jeweils vor einem Konstruktor und nach einem Destruktor ausgeführt werden. Alle Funktionen, die durch den Funktionsverweis im .init-Abschnitt aufgerufen werden, werden vor dem Funktionsverweis selbst ausgeführt (wie üblich).

Ich habe versucht, jedes dieser Beispiele unten zu veranschaulichen:

#include 

`

Ausgabe:

init_some_function() aufgerufen von elf_init()

    elf_init() -- (.section .init)

    construct1() Konstruktor -- (.section .ctors) Priorität 101

    construct2() Konstruktor -- (.section .ctors) Priorität 102

        test() mit der Verwendung von -- (.section .ctors/.dtors) ohne Priorität

        test() mit der Verwendung von -- (.section .ctors/.dtors) ohne Priorität

        [ Hauptteil des Programms ]

        test() mit der Verwendung von -- (.section .ctors/.dtors) ohne Priorität

    destruct2() Destruktor -- (.section .dtors) Priorität 102

    destruct1() Destruktor -- (.section .dtors) Priorität 101

Das Beispiel half, das Verhalten der Konstruktoren/Destruktoren zu festigen, hoffentlich wird es auch für andere nützlich sein.

`

0 Stimmen

Wo hast du die Information gefunden, dass "die Prioritätswerte, die du angibst, größer als 100 sein müssen"? Diese Information ist nicht auf der Dokumentation zu den GCC-Funktionsattributen vorhanden.

7 Stimmen

Soweit ich mich erinnere, gab es ein paar Verweise auf PATCH: Unterstützung des Prioritätsarguments für Konstruktor-/Destruktorargumente (MAX_RESERVED_INIT_PRIORITY), und dass sie dieselben waren wie in C++ (init_priority) 7.7 C++-spezifische Variablen-, Funktions- und Typattribute. Dann habe ich es mit 99 versucht: Warnung: Konstruktorprioritäten von 0 bis 100 sind für die Implementation reserviert [standardmäßig aktiviert] void construct0 () __attribute__ ((constructor (99)));.

1 Stimmen

Ah. Ich habe Prioritäten < 100 mit clang ausprobiert und es schien zu funktionieren, aber mein einfacher Testfall (eine einzige Kompilationseinheit) war zu einfach.

8voto

Alex Gray Punkte 15268

Hier ist ein "konkretes" (und möglicherweise nützliches) Beispiel dafür, wie, warum und wann man diese praktischen, aber unschönen Konstrukte verwenden kann...

Xcode verwendet eine "globale" "Benutzervoreinstellung", um zu entscheiden, welcher XCTestObserver Klasse es sein Herz ausschüttet an die geplagte Konsole.

In diesem Beispiel... wenn ich diese Pseudo-Bibliothek implizit lade, nennen wir sie... libdemure.a, über einen Flag in meinem Testziel, so wie...

OTHER_LDFLAGS = -ldemure

Ich möchte...

  1. Beim Laden (d.h. wenn XCTest mein Testbündel lädt), die "standardmäßige" XCTest "Observer" Klasse überschreiben... (über die constructor Funktion) PS: Soweit ich das beurteilen kann... was hier gemacht wird, könnte mit äquivalentem Effekt auch innerhalb der + (void) load { ... } Methode meiner Klasse erledigt werden.

  2. meine Tests durchführen... in diesem Fall mit weniger sinnloser Verbose in den Logs (Implementierung auf Anfrage)

  3. Die "globale" XCTestObserver Klasse auf ihren ursprünglichen Zustand zurücksetzen... damit andere XCTest Durchläufe, die nicht auf den Zug aufgesprungen sind (also nicht mit libdemure.a verbunden sind), nicht durcheinander geraten. Ich vermute, dass dies historisch in dealloc gemacht wurde... aber ich werde nicht anfangen, mit dieser alten Dame herumzufummeln.

Also...

#define USER_DEFS NSUserDefaults.standardUserDefaults

@interface      DemureTestObserver : XCTestObserver @end
@implementation DemureTestObserver

__attribute__((constructor)) static void hijack_observer() {

/*! hier übernehme ich vollständig das Standard-Logging, aber DU KANNST auch
    mehrere Observer verwenden, einfach per CSV, 
    z.B. "@"DemureTestObserverm,XCTestLog"
*/
  [USER_DEFS setObject:@"DemureTestObserver" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

__attribute__((destructor)) static void reset_observer()  {

  // Aufräumen, und es ist, als wären wir nie hier gewesen.
  [USER_DEFS setObject:@"XCTestLog" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

...
@end

Ohne das Linker-Flag... (Die Modepolizei belagert Cupertino fordert Vergeltung, doch Apples Standard setzt sich durch, wie gewünscht hier)

Bildbeschreibung eingeben

MIT dem -ldemure.a Linker-Flag... (Verständliche Ergebnisse, keuch... "danke constructor/destructor"... Menge jubelt) Bildbeschreibung eingeben

1voto

drlolly Punkte 149

Hier ist ein weiteres konkretes Beispiel. Es handelt sich um eine gemeinsam genutzte Bibliothek. Die Hauptfunktion der gemeinsam genutzten Bibliothek besteht darin, mit einem Smartcard-Lesegerät zu kommunizieren, aber sie kann auch zur Laufzeit über UDP 'Konfigurationsinformationen' empfangen. Das UDP wird von einem Thread bearbeitet, der unbedingt zur Initialisierungszeit gestartet werden muss.

__attribute__((constructor))  static void startUdpReceiveThread (void) {
    pthread_create( &tid_udpthread, NULL, __feigh_udp_receive_loop, NULL );
    return;

  }

Die Bibliothek wurde in C geschrieben.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X