Wie genau funktioniert __attribute __ ((Konstruktor))?


347

Es scheint ziemlich klar zu sein, dass es die Dinge einrichten soll.

  1. Wann genau läuft es?
  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 läuft __attribute__((destructor))?

Beispiel in Ziel-C :

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

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

Antworten:


273
  1. Es wird ausgeführt, wenn eine gemeinsam genutzte Bibliothek geladen wird, normalerweise während des Programmstarts.
  2. So sind alle GCC-Attribute; vermutlich, um sie von Funktionsaufrufen zu unterscheiden.
  3. GCC-spezifische Syntax.
  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, normalerweise beim Beenden des Programms.

Die Konstruktoren und Destruktoren funktionieren also so, dass die gemeinsam genutzte Objektdatei spezielle Abschnitte (.ctors und .dtors in ELF) enthält, die Verweise auf die Funktionen enthalten, die mit den Konstruktor- bzw. Destruktorattributen gekennzeichnet sind. Wenn die Bibliothek geladen / entladen wird, prüft das dynamische Ladeprogramm (ld.so oder so), ob solche Abschnitte vorhanden sind, und ruft in diesem Fall die darin genannten Funktionen auf.

Wenn Sie sich das vorstellen, gibt es wahrscheinlich eine ähnliche Magie im normalen statischen Linker, so dass beim Starten / Herunterfahren derselbe Code ausgeführt wird, unabhängig davon, ob der Benutzer statische oder dynamische Verknüpfung wählt.


49
Die doppelten Klammern erleichtern das "Makro-Out" ( #define __attribute__(x)). Wenn Sie mehrere Attribute haben, z. B. __attribute__((noreturn, weak))wäre es schwierig, ein "Makro zu erstellen", wenn nur ein Satz von Klammern vorhanden wäre.
Chris Jester-Young

7
Es ist noch nicht fertig .init/.fini. (Sie können gültig mehrere Konstruktoren und Destruktoren in einer einzigen Übersetzungseinheit haben, niemals mehrere in einer einzigen Bibliothek - wie würde das funktionieren?) Stattdessen werden auf Plattformen, die das ELF-Binärformat (Linux usw.) verwenden, auf die Konstruktoren und Destruktoren verwiesen in den .ctorsund .dtorsAbschnitten der Kopfzeile. Zwar wurden früher Funktionen benannt initund finibeim Laden und Entladen dynamischer Bibliotheken ausgeführt, wenn sie vorhanden waren, aber das ist jetzt veraltet und wird durch diesen besseren Mechanismus ersetzt.
Ephemient

7
@jcayzac Nein, da verschiedene Makros eine gcc-Erweiterung sind und der Hauptgrund für das Makro-Out darin __attribute__besteht, dass Sie gcc nicht verwenden, da auch dies eine gcc-Erweiterung ist.
Chris Jester-Young

9
@ ChrisJester-Young Variadic-Makros sind eine Standard-C99-Funktion, keine GNU-Erweiterung.
JCayzac

4
„Ihre Verwendung von Präsens (“ make“statt‚gemacht‘- die Doppel Pars noch machen sie einfach zu makro Sie bellte die falsche pedantisch Baum..
Jim Balter

64

.init/ .finiIst nicht veraltet. Es ist immer noch Teil des ELF-Standards und ich würde sagen, es wird für immer sein. Code in .init/ .finiwird vom Loader / Runtime-Linker ausgeführt, wenn Code geladen / entladen wird. Dh bei jedem ELF-Ladevorgang (z. B. einer gemeinsam genutzten Bibliothek) wird Code .initausgeführt. Es ist immer noch möglich, diesen Mechanismus zu verwenden, um ungefähr das Gleiche wie mit zu erreichen __attribute__((constructor))/((destructor)). Es ist altmodisch, hat aber einige Vorteile.

.ctors/ .dtorsMechanismus zum Beispiel erfordern Unterstützung durch system-rtl / loader / linker-script. Dies ist keineswegs sicher auf allen Systemen verfügbar, beispielsweise auf tief eingebetteten Systemen, auf denen Code auf Bare-Metal-Basis ausgeführt wird. Das heißt, selbst wenn __attribute__((constructor))/((destructor))es von GCC unterstützt wird, ist es nicht sicher, ob es ausgeführt wird, da es Sache des Linkers ist, es zu organisieren, und des Loaders (oder in einigen Fällen des Boot-Codes), es auszuführen. Um .init/ .finistattdessen zu verwenden, ist es am einfachsten, Linker-Flags zu verwenden: -init & -fini (dh über die GCC-Befehlszeile wäre die Syntax -Wl -init my_init -fini my_fini).

Auf einem System, das beide Methoden unterstützt, besteht ein möglicher Vorteil darin, dass Code in .initvorher .ctorsund Code in .fininachher ausgeführt wird .dtors. Wenn die Reihenfolge relevant ist, ist dies mindestens eine grobe, aber einfache Möglichkeit, zwischen Init / Exit-Funktionen zu unterscheiden.

Ein Hauptnachteil ist, dass Sie nicht einfach mehr als eine _initund eine _finiFunktion pro ladbarem Modul haben können und wahrscheinlich Code mehr .soals motiviert fragmentieren müssten . Ein weiterer Grund ist, dass bei Verwendung der oben beschriebenen Linkermethode die ursprünglichen _init- und _finiStandardfunktionen (bereitgestellt von crti.o) ersetzt werden. Hier finden normalerweise alle Arten von Initialisierungen statt (unter Linux wird hier die globale Variablenzuweisung initialisiert). Ein Weg, um das zu umgehen, wird hier beschrieben

Beachten Sie im obigen Link, dass eine Kaskadierung zum Original _init()nicht erforderlich ist, da es noch vorhanden ist. Die callInline-Assembly ist jedoch x86-mnemonisch, und das Aufrufen einer Funktion aus der Assembly würde für viele andere Architekturen (wie z. B. ARM) völlig anders aussehen. Dh Code ist nicht transparent.

.init/ .finiund .ctors/ .detorsMechanismen sind ähnlich, aber nicht ganz. Code in .init/ .finiläuft "wie es ist". Das heißt, Sie können mehrere Funktionen in .init/ haben .fini, aber es ist AFAIK syntaktisch schwierig, sie in reinem C vollständig transparent dort abzulegen, ohne den Code in vielen kleinen .soDateien aufzubrechen.

.ctors/ .dtorsWerden anders als organisiert .init/ .fini. .ctors/ .dtorsAbschnitte sind beide nur Tabellen mit Zeigern auf Funktionen, und der "Aufrufer" ist eine vom System bereitgestellte Schleife, die jede Funktion indirekt aufruft. Das heißt, der Schleifenaufrufer kann architekturspezifisch sein, aber da er Teil des Systems ist (falls überhaupt vorhanden, dh), spielt es keine Rolle.

Das folgende Snippet fügt dem Funktionsarray neue Funktionszeiger hinzu .ctors, hauptsächlich auf die gleiche Weise wie __attribute__((constructor))(Methode kann koexistieren mit __attribute__((constructor))).

#define SECTION( S ) __attribute__ ((section ( S )))
void test(void) {
   printf("Hello\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 völlig anderen selbst erfundenen Abschnitt hinzufügen. In diesem Fall wird ein modifiziertes Linker-Skript und eine zusätzliche Funktion benötigt, die den Loader .ctors/ die .dtorsLoop nachahmt . Aber damit kann man eine bessere Kontrolle über die Ausführungsreihenfolge erreichen, In-Argument hinzufügen und die Code-Behandlung eta zurückgeben (in einem C ++ - Projekt wäre es beispielsweise nützlich, wenn etwas vor oder nach globalen Konstruktoren ausgeführt werden muss).

Ich würde es vorziehen, __attribute__((constructor))/((destructor))wenn es möglich ist, es ist eine einfache und elegante Lösung, auch wenn es sich wie Betrug anfühlt. Für Bare-Metal-Codierer wie mich ist dies nicht immer eine Option.

Einige gute Hinweise im Buch Linker & Loader .


Wie kann der Lader diese Funktionen aufrufen? Diese Funktionen können globale und andere Funktionen im Prozessadressraum verwenden, aber Loader ist ein Prozess mit einem eigenen Adressraum, nicht wahr?
user2162550

@ user2162550 Nein, ld-linux.so.2 (der übliche "Interpreter", der Loader für dynamische Bibliotheken, der auf allen dynamisch verknüpften ausführbaren Dateien ausgeführt wird) wird im Adressraum der ausführbaren Datei selbst ausgeführt. Im Allgemeinen ist der dynamische Bibliothekslader selbst spezifisch für den Benutzerbereich und wird im Kontext des Threads ausgeführt, der versucht, auf eine Bibliotheksressource zuzugreifen.
Paul Stelian

Wenn ich execv () aus dem Code aufrufe, __attribute__((constructor))/((destructor))läuft der Destruktor nicht. Ich habe einige Dinge ausprobiert, wie das Hinzufügen eines Eintrags zu .dtor, wie oben gezeigt. Aber kein Erfolg. Das Problem kann leicht dupliziert werden, indem der Code mit numactl ausgeführt wird. Angenommen, test_code enthält den Destruktor (fügen Sie den Konstruktor- und Desktorfunktionen ein printf hinzu, um das Problem zu beheben). Dann renne LD_PRELOAD=./test_code numactl -N 0 sleep 1. Sie werden sehen, dass der Konstruktor zweimal aufgerufen wird, der Destruktor jedoch nur einmal.
B Abali

39

Diese Seite bietet ein gutes Verständnis für die constructorunddestructor Implementierung von Attributen sowie der Abschnitte in ELF, in denen sie funktionieren können. Nachdem ich die hier bereitgestellten Informationen verdaut hatte, stellte ich einige zusätzliche Informationen zusammen und erstellte (aus dem obigen Abschnittsbeispiel von Michael Ambrus) ein Beispiel, um die Konzepte zu veranschaulichen und mein Lernen zu erleichtern. Diese Ergebnisse werden unten zusammen mit der Beispielquelle bereitgestellt.

Wie in diesem Thread erläutert, die constructorund destructorerstellen Attribute Einträge in der .ctorsund .dtorsAbschnitt der Objektdatei. Sie können Verweise auf Funktionen in beiden Abschnitten auf drei Arten platzieren. (1) Verwenden eines der sectionAttribute; (2) constructorund destructorAttribute oder (3) mit einem Inline-Assembly-Aufruf (wie auf den Link in Ambrus 'Antwort verwiesen).

Durch die Verwendung von constructorund destructorAttributen können Sie dem Konstruktor / Destruktor zusätzlich eine Priorität zuweisen, um seine Ausführungsreihenfolge vor dem main()Aufruf oder nach der Rückkehr zu steuern . Je niedriger der angegebene Prioritätswert ist, desto höher ist die Ausführungspriorität (niedrigere Prioritäten werden vor höheren Prioritäten vor main () ausgeführt - und nach höheren Prioritäten nach main ()). Die von Ihnen angegebenen Prioritätswerte müssen größer sein als,100 da der Compiler Prioritätswerte zwischen 0 und 100 für die Implementierung reserviert. Ein constructoroder destructormit Priorität angegeben wird vor einem constructoroder destructorohne Priorität angegeben ausgeführt.

Mit dem Attribut 'section' oder mit der Inline-Assembly können Sie auch Funktionsreferenzen in den Abschnitt .initund den .finiELF-Codeabschnitt einfügen, die vor jedem Konstruktor bzw. nach jedem Destruktor ausgeführt werden. Alle Funktionen, die von der im .initAbschnitt platzierten Funktionsreferenz aufgerufen werden , werden (wie üblich) vor der Funktionsreferenz selbst ausgeführt.

Ich habe versucht, diese im folgenden Beispiel zu veranschaulichen:

#include <stdio.h>
#include <stdlib.h>

/*  test function utilizing attribute 'section' ".ctors"/".dtors"
    to create constuctors/destructors without assigned priority.
    (provided by Michael Ambrus in earlier answer)
*/

#define SECTION( S ) __attribute__ ((section ( S )))

void test (void) {
printf("\n\ttest() utilizing -- (.section .ctors/.dtors) w/o priority\n");
}

void (*funcptr1)(void) SECTION(".ctors") =test;
void (*funcptr2)(void) SECTION(".ctors") =test;
void (*funcptr3)(void) SECTION(".dtors") =test;

/*  functions constructX, destructX use attributes 'constructor' and
    'destructor' to create prioritized entries in the .ctors, .dtors
    ELF sections, respectively.

    NOTE: priorities 0-100 are reserved
*/
void construct1 () __attribute__ ((constructor (101)));
void construct2 () __attribute__ ((constructor (102)));
void destruct1 () __attribute__ ((destructor (101)));
void destruct2 () __attribute__ ((destructor (102)));

/*  init_some_function() - called by elf_init()
*/
int init_some_function () {
    printf ("\n  init_some_function() called by elf_init()\n");
    return 1;
}

/*  elf_init uses inline-assembly to place itself in the ELF .init section.
*/
int elf_init (void)
{
    __asm__ (".section .init \n call elf_init \n .section .text\n");

    if(!init_some_function ())
    {
        exit (1);
    }

    printf ("\n    elf_init() -- (.section .init)\n");

    return 1;
}

/*
    function definitions for constructX and destructX
*/
void construct1 () {
    printf ("\n      construct1() constructor -- (.section .ctors) priority 101\n");
}

void construct2 () {
    printf ("\n      construct2() constructor -- (.section .ctors) priority 102\n");
}

void destruct1 () {
    printf ("\n      destruct1() destructor -- (.section .dtors) priority 101\n\n");
}

void destruct2 () {
    printf ("\n      destruct2() destructor -- (.section .dtors) priority 102\n");
}

/* main makes no function call to any of the functions declared above
*/
int
main (int argc, char *argv[]) {

    printf ("\n\t  [ main body of program ]\n");

    return 0;
}

Ausgabe:

init_some_function() called by elf_init()

    elf_init() -- (.section .init)

    construct1() constructor -- (.section .ctors) priority 101

    construct2() constructor -- (.section .ctors) priority 102

        test() utilizing -- (.section .ctors/.dtors) w/o priority

        test() utilizing -- (.section .ctors/.dtors) w/o priority

        [ main body of program ]

        test() utilizing -- (.section .ctors/.dtors) w/o priority

    destruct2() destructor -- (.section .dtors) priority 102

    destruct1() destructor -- (.section .dtors) priority 101

Das Beispiel hat dazu beigetragen, das Konstruktor- / Destruktorverhalten zu festigen, hoffentlich ist es auch für andere nützlich.


Wo haben Sie festgestellt, dass "die von Ihnen angegebenen Prioritätswerte größer als 100 sein müssen"? Diese Informationen sind in der Dokumentation
Justin

4
IIRC gab es einige Referenzen, PATCH: Unterstützungsprioritätsargument für Konstruktor / Destruktor-Argumente ( MAX_RESERVED_INIT_PRIORITY), und dass sie mit C ++ ( init_priority) identisch waren. 7.7 C ++ - Spezifische Variablen-, Funktions- und Typattribute . Dann habe ich es versucht mit 99: warning: constructor priorities from 0 to 100 are reserved for the implementation [enabled by default] void construct0 () __attribute__ ((constructor (99)));.
David C. Rankin

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

1
Welche Priorität haben statische globale Variablen (statische Ctoren)?
Bindestrich

2
Der Effekt und die Sichtbarkeit eines statischen Globals hängen davon ab, wie Ihr Programm strukturiert ist (z. B. einzelne Datei, mehrere Dateien ( Übersetzungseinheiten )) und in welchem ​​das Globale deklariert ist. Siehe: Statisch (Schlüsselwort) , insbesondere die Beschreibung der statischen globalen Variablen .
David C. Rankin

7

Hier ist ein "konkretes" (und möglicherweise nützliches ) Beispiel dafür, wie, warum und wann diese praktischen, aber unansehnlichen Konstrukte verwendet werden sollen ...

Xcode verwendet einen "globalen" "Benutzerstandard", um zu entscheiden, welche XCTestObserverKlasse der bedrängten Konsole das Herz ausspuckt .

In diesem Beispiel ... wenn ich diese Pseudobibliothek implizit lade, nennen wir sie ... libdemure.aüber ein Flag in meinem Testziel á la ..

OTHER_LDFLAGS = -ldemure

Ich will..

  1. XCTestÜberschreiben Sie beim Laden (dh beim Laden meines Testpakets) die "Standard" XCTest-Klasse "Beobachter" ... (über die constructorFunktion) PS: Soweit ich das beurteilen kann, kann alles, was hier getan wird, mit gleichem Effekt in meinem Klasse ' + (void) load { ... }Methode.

  2. Führen Sie meine Tests aus .... in diesem Fall mit weniger irrsinniger Ausführlichkeit in den Protokollen (Implementierung auf Anfrage)

  3. Bringen Sie die "globale" XCTestObserverKlasse in ihren ursprünglichen Zustand zurück, um andere XCTestLäufe, die nicht auf den Zug gekommen sind (auch bekannt als "verknüpft mit" libdemure.a), nicht zu beschmutzen . Ich denke, das wurde historisch in dealloc... gemacht, aber ich werde nicht anfangen, mich mit dieser alten Hexe anzulegen.

Damit...

#define USER_DEFS NSUserDefaults.standardUserDefaults

@interface      DemureTestObserver : XCTestObserver @end
@implementation DemureTestObserver

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

/*! here I totally hijack the default logging, but you CAN
    use multiple observers, just CSV them, 
    i.e. "@"DemureTestObserverm,XCTestLog"
*/
  [USER_DEFS setObject:@"DemureTestObserver" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

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

  // Clean up, and it's as if we had never been here.
  [USER_DEFS setObject:@"XCTestLog" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

...
@end

Ohne die Linker-Flagge ... (Cupertino, ein Schwarm der Modepolizei, fordert Vergeltung , doch hier herrscht, wie gewünscht, Apples Standard vor. )

Geben Sie hier die Bildbeschreibung ein

MIT der -ldemure.aLinker-Flagge ... (Verständliche Ergebnisse, keuchen ... "danke constructor/ destructor" ... Crowd Cheers ) Geben Sie hier die Bildbeschreibung ein


1

Hier ist ein weiteres konkretes Beispiel. Es handelt sich um eine gemeinsam genutzte Bibliothek. Die Hauptfunktion der gemeinsam genutzten Bibliothek besteht in der Kommunikation mit einem Smartcard-Leser. Es kann aber auch 'Konfigurationsinformationen' zur Laufzeit über udp empfangen. Die udp wird von einem Thread verarbeitet , die MUSS bei init Zeit gestartet werden.

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

  }

Die Bibliothek wurde in c geschrieben.


1
Eine seltsame Wahl, wenn die Bibliothek in C ++ geschrieben ist, da gewöhnliche globale Variablenkonstruktoren die idiomatische Methode sind, um Code vor dem Main in C ++ auszuführen.
Nicholas Wilson

@NicholasWilson Die Bibliothek wurde tatsächlich in c geschrieben. Ich weiß nicht, wie ich c ++ anstelle von c eingegeben habe.
Drlolly
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.