Come funziona esattamente __attribute __ ((costruttore))?


347

Sembra abbastanza chiaro che dovrebbe sistemare le cose.

  1. Quando corre esattamente?
  2. Perché ci sono due parentesi?
  3. È __attribute__una funzione? Una macro? Sintassi?
  4. Funziona in C? C ++?
  5. La funzione con cui funziona deve essere statica?
  6. Quando __attribute__((destructor))corre?

Esempio in Objective-C :

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

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

Risposte:


273
  1. Funziona quando viene caricata una libreria condivisa, in genere durante l'avvio del programma.
  2. Ecco come sono tutti gli attributi GCC; presumibilmente per distinguerli dalle chiamate di funzione.
  3. Sintassi specifica di GCC.
  4. Sì, funziona in C e C ++.
  5. No, la funzione non deve essere statica.
  6. Il distruttore viene eseguito quando viene scaricata la libreria condivisa, in genere all'uscita dal programma.

Quindi, il modo in cui funzionano i costruttori e i distruttori è che il file oggetto condiviso contiene sezioni speciali (.ctors e .dtors su ELF) che contengono riferimenti alle funzioni contrassegnate rispettivamente con gli attributi costruttore e distruttore. Quando la libreria viene caricata / scaricata, il programma di caricamento dinamico (ld.so o somesuch) controlla se tali sezioni esistono e, in tal caso, chiama le funzioni a cui fa riferimento.

Pensaci, probabilmente c'è qualche magia simile nel normale linker statico in modo che lo stesso codice venga eseguito all'avvio / spegnimento indipendentemente dal fatto che l'utente scelga il collegamento statico o dinamico.


49
Le doppie parentesi li rendono facilmente "macro out" ( #define __attribute__(x)). Se hai più attributi, ad esempio, __attribute__((noreturn, weak))sarebbe difficile "eliminare" se esistesse solo un set di parentesi.
Chris Jester-Young,

7
Non è finito .init/.fini. (È possibile validamente avere più costruttori e distruttori in una singola unità di traduzione, non pensarne mai più in una singola libreria - come funzionerebbe?) Invece, su piattaforme che usano il formato binario ELF (Linux, ecc.), I costruttori e i distruttori sono referenziati nelle sezioni .ctorse .dtorsdell'intestazione. È vero, ai vecchi tempi, le funzioni denominate inite finivenivano eseguite sul caricamento e lo scaricamento dinamici della libreria se esistessero, ma che ora è deprecato, sostituito da questo meccanismo migliore.
effimero

7
@jcayzac No, perché le macro variadiche sono un'estensione gcc, e il motivo principale per il macro out __attribute__è se non stai usando gcc, dal momento che anche questa è un'estensione gcc.
Chris Jester-Young,

9
@ ChrisJester-Le macro variadiche sono una funzione C99 standard, non un'estensione GNU.
jcayzac,

4
"il tuo uso del tempo presente (" make "anziché" made "- le doppie parentesi le rendono ancora facili da eliminare. Hai abbaiato l'albero pedante sbagliato.
Jim Balter,

64

.init/ .fininon è deprecato. Fa ancora parte dello standard ELF e oserei dire che lo sarà per sempre. Il codice in .init/ .finiviene eseguito dal caricatore / runtime-linker quando il codice viene caricato / scaricato. Vale a dire su ogni carico ELF (ad esempio una libreria condivisa) .initverrà eseguito il codice in . È ancora possibile utilizzare quel meccanismo per ottenere quasi la stessa cosa di __attribute__((constructor))/((destructor)). È vecchia scuola ma ha alcuni vantaggi.

.ctorsIl .dtorsmeccanismo / , ad esempio, richiede il supporto di system-rtl / loader / linker-script. Questo è tutt'altro che certo disponibile su tutti i sistemi, ad esempio i sistemi profondamente integrati in cui il codice viene eseguito su bare metal. Vale a dire anche se __attribute__((constructor))/((destructor))è supportato da GCC, non è certo che verrà eseguito poiché spetta al linker organizzarlo e al caricatore (o in alcuni casi, codice di avvio) per eseguirlo. Per usare .init/ .finiinvece, il modo più semplice è usare i flag di linker: -init & -fini (cioè dalla riga di comando di GCC, la sintassi sarebbe -Wl -init my_init -fini my_fini).

Sul sistema che supporta entrambi i metodi, un possibile vantaggio è che il codice in .initviene eseguito prima .ctorse il codice in .finidopo .dtors. Se l'ordine è rilevante, questo è almeno un modo semplice ma semplice per distinguere tra le funzioni init / exit.

Un grosso svantaggio è che non puoi avere facilmente più di una _inite una _finifunzione per ciascun modulo caricabile e probabilmente dovresti frammentare il codice in più .soche motivato. Un altro è che quando si utilizza il metodo linker sopra descritto, si sostituiscono le funzioni originali _init e _finipredefinite (fornite da crti.o). Questo è dove di solito si verificano tutti i tipi di inizializzazione (su Linux è qui che viene inizializzata l'assegnazione delle variabili globali). Un modo per aggirare questo è descritto qui

Si noti nel collegamento sopra che _init()non è necessario un collegamento in cascata all'originale in quanto è ancora in atto. L' callassemblaggio in linea è tuttavia x86-mnemonico e la chiamata di una funzione dall'assemblaggio sembrerebbe completamente diversa per molte altre architetture (come ad esempio ARM). Cioè il codice non è trasparente.

.initI meccanismi / .finie .ctors/ .detorssono simili, ma non del tutto. Il codice in .init/ .finiviene eseguito "così com'è". Vale a dire che puoi avere diverse funzioni in .init/ .fini, ma sintatticamente AFAIK è difficile metterle lì in modo completamente trasparente in C puro senza rompere il codice in molti piccoli .sofile.

.ctors/ .dtorssono organizzati diversamente da .init/ .fini. .ctorsLe .dtorssezioni / sono entrambe tabelle con puntatori a funzioni e il "chiamante" è un ciclo fornito dal sistema che chiama indirettamente ciascuna funzione. Vale a dire il loop-caller può essere specifico dell'architettura, ma poiché fa parte del sistema (se esiste affatto, cioè) non importa.

Il frammento seguente aggiunge nuovi puntatori di .ctorsfunzione all'array di funzioni, principalmente allo stesso modo __attribute__((constructor))(il metodo può coesistere con __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;

Si possono anche aggiungere i puntatori di funzione a una sezione auto-inventata completamente diversa. In questo caso sono necessari uno script linker modificato e una funzione aggiuntiva che imita il caricatore .ctors/ .dtorsloop. Ma con esso si può ottenere un migliore controllo sull'ordine di esecuzione, aggiungere l'argomento e la gestione del codice di ritorno eta (In un progetto C ++, ad esempio, sarebbe utile se fosse necessario eseguire qualcosa prima o dopo i costruttori globali).

Preferirei __attribute__((constructor))/((destructor))dove possibile, è una soluzione semplice ed elegante anche se ti sembra di barare. Per i programmatori bare metal come me, questa non è sempre un'opzione.

Qualche buona referenza nel libro Linker e caricatori .


come può il caricatore chiamare quelle funzioni? quelle funzioni possono usare i globi e altre funzioni nello spazio degli indirizzi di processo, ma il caricatore è un processo con un proprio spazio di indirizzi, non è vero?
user2162550

@ user2162550 No, ld-linux.so.2 (il solito "interprete", il caricatore per le librerie dinamiche che gira su tutti gli eseguibili collegati dinamicamente) viene eseguito nello spazio degli indirizzi dell'eseguibile stesso. In generale, il caricatore dinamico di librerie stesso è qualcosa di specifico per lo spazio utente, in esecuzione nel contesto del thread che tenta di accedere a una risorsa della libreria.
Paul Stelian,

Quando chiamo execv () dal codice che ha __attribute__((constructor))/((destructor))il distruttore non viene eseguito. Ho provato alcune cose come aggiungere una voce a .dtor come mostrato sopra. Ma nessun successo. Il problema è facile da duplicare eseguendo il codice con numactl. Ad esempio, si supponga che test_code contenga il distruttore (aggiungere un printf alle funzioni di costruzione e descrizione per eseguire il debug del problema). Quindi corri LD_PRELOAD=./test_code numactl -N 0 sleep 1. Vedrai che il costruttore viene chiamato due volte ma distruttore solo una volta.
B Abali,

39

Questa pagina offre una grande comprensione circa la constructore destructorimplementazione attributi e le sezioni all'interno di all'interno di ELF che li permettono di lavoro. Dopo aver digerito le informazioni fornite qui, ho compilato un po 'di informazioni aggiuntive e (prendendo in prestito l'esempio di sezione da Michael Ambrus sopra) ho creato un esempio per illustrare i concetti e aiutare il mio apprendimento. Tali risultati sono forniti di seguito insieme alla fonte di esempio.

Come spiegato in questo thread, gli attributi constructore destructorcreano voci nella sezione .ctorse .dtorsdel file oggetto. È possibile inserire riferimenti a funzioni in entrambe le sezioni in uno dei tre modi. (1) utilizzando l' sectionattributo; (2) constructore destructorattributi o (3) con una chiamata in-assembly (come indicato nel collegamento nella risposta di Ambrus).

L'uso di constructore gli destructorattributi ti consentono di assegnare una priorità al costruttore / distruttore per controllare il suo ordine di esecuzione prima che main()venga chiamato o dopo che ritorni. Più basso è il valore di priorità assegnato, maggiore è la priorità di esecuzione (priorità inferiori vengono eseguite prima di priorità più alte prima di main () - e successivamente a priorità più alte dopo main ()). I valori di priorità forniti devono essere maggiori di100 poiché il compilatore riserva valori di priorità compresi tra 0 e 100 per l'implementazione. A constructoro destructorspecificato con priorità viene eseguito prima di constructoro destructorspecificato senza priorità.

Con l'attributo 'sezione' o con inline-assembly, puoi anche inserire riferimenti di funzione nella sezione del codice ELF .inite .finiche verranno eseguiti rispettivamente prima di ogni costruttore e dopo ogni distruttore. Qualsiasi funzione chiamata dal riferimento di funzione posizionato nella .initsezione, verrà eseguita prima del riferimento di funzione stesso (come al solito).

Ho cercato di illustrare ciascuno di quelli nell'esempio seguente:

#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;
}

produzione:

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

L'esempio ha contribuito a cementare il comportamento del costruttore / distruttore, si spera che possa essere utile anche agli altri.


Dove hai trovato che "i valori di priorità che dai devono essere maggiori di 100"? Tali informazioni non sono presenti nella documentazione
Giustino,

4
IIRC, c'erano un paio di riferimenti, PATCH: supporto argomento prioritario per argomenti costruttore / distruttore ( MAX_RESERVED_INIT_PRIORITY), e che erano uguali a C ++ ( init_priority) 7.7 C ++ - Attributi specifici di variabili, funzioni e tipi . Poi ho provato con 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. Ho provato le priorità <100 con clang e sembrava funzionare, ma il mio semplice caso di test (una singola unità di compilazione) era troppo semplice .
Giustino,

1
Qual è la priorità delle variabili globali statiche (statici ctors)?
dashesy

2
L'effetto e la visibilità di un globale statico dipenderanno dalla struttura del programma (ad es. Singolo file, più file ( unità di traduzione )) e in cui viene dichiarato il globale. Vedere: Statico (parola chiave) , in particolare la descrizione della variabile globale Statica .
David C. Rankin,

7

Ecco un esempio "concreto" (e forse utile ) di come, perché e quando usare questi costrutti pratici, ma sgradevoli ...

Xcode utilizza un "predefinito" "predefinito" per l'utente per decidere quale XCTestObserverclasse viene trasmessa alla console assediata .

In questo esempio ... quando carico implicitamente questa psuedo-libreria, chiamiamolo ... libdemure.a, tramite un flag nel mio obiettivo di test in ...

OTHER_LDFLAGS = -ldemure

Voglio..

  1. Al caricamento (ad es. Quando XCTestcarica il mio pacchetto di test), sovrascrive la XCTestclasse "predefinita" "osservatore" ... (tramite la constructorfunzione) PS: per quanto ne so .. qualsiasi cosa fatta qui potrebbe essere fatta con un effetto equivalente all'interno del mio + (void) load { ... }metodo di classe .

  2. eseguo i miei test .... in questo caso, con verbosità meno folle nei registri (implementazione su richiesta)

  3. Riporta la XCTestObserverclasse "globale" al suo stato incontaminato ... in modo da non sporcare altre XCTestpiste che non sono salite sul carro (noto anche come libdemure.a). Immagino che questo sia stato storicamente fatto in dealloc.. ma non ho intenzione di iniziare a scherzare con quella vecchia strega.

Così...

#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

Senza la bandiera del linker ... (Lo sciame di Fashion-police di Cupertino richiede una punizione , ma il default di Apple prevale, come si desidera, qui )

inserisci qui la descrizione dell'immagine

CON la -ldemure.abandiera del linker ... (Risultati comprensibili, sussulto ... "grazie constructor/ destructor" ... Folla applausi ) inserisci qui la descrizione dell'immagine


1

Ecco un altro esempio concreto: è per una libreria condivisa. La funzione principale della libreria condivisa è quella di comunicare con un lettore di smart card. Ma può anche ricevere "informazioni sulla configurazione" in fase di esecuzione su udp. L'udp è gestito da un thread che DEVE essere avviato al momento dell'inizializzazione.

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

  }

La biblioteca è stata scritta in c.


1
Una scelta strana se la libreria è scritta in C ++, poiché i normali costruttori di variabili globali sono il modo idiomatico per eseguire il codice principale in C ++.
Nicholas Wilson,

@NicholasWilson La biblioteca fu infatti scritta in c. Non so come ho digitato c ++ invece di c.
drlolly,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.