Jak dokładnie działa __attribute __ ((konstruktor))?

348

Wydaje się całkiem jasne, że ma to wszystko skonfigurować.

  1. Kiedy dokładnie działa?
  2. Dlaczego są dwa nawiasy?
  3. Jest __attribute__funkcją? Makro? Składnia?
  4. Czy to działa w C? C ++?
  5. Czy funkcja, z którą współpracuje, musi być statyczna?
  6. Kiedy działa __attribute__((destructor))?

Przykład w celu C :

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

__attribute__((destructor))
static void destroy_navigationBarImages() {
  [navigationBarImages release];
}
Casebash
źródło

Odpowiedzi:

273
  1. Działa, gdy ładowana jest biblioteka współdzielona, ​​zwykle podczas uruchamiania programu.
  2. Tak wyglądają wszystkie atrybuty GCC; przypuszczalnie w celu odróżnienia ich od wywołań funkcji.
  3. Składnia specyficzna dla GCC.
  4. Tak, działa w C i C ++.
  5. Nie, funkcja nie musi być statyczna.
  6. Destruktor działa, gdy biblioteka współdzielona jest rozładowywana, zwykle przy wyjściu z programu.

Tak więc sposób działania konstruktorów i destruktorów polega na tym, że plik współdzielonego obiektu zawiera specjalne sekcje (.ctors i .dtors na ELF), które zawierają odniesienia do funkcji oznaczonych odpowiednio atrybutami konstruktora i destruktora. Gdy biblioteka jest ładowana / rozładowywana, program ładujący dynamiczny (ld.so lub coś takiego) sprawdza, czy takie sekcje istnieją, a jeśli tak, wywołuje funkcje w nich określone.

Pomyśl o tym, prawdopodobnie w podobnej magii jest normalny statyczny linker, więc ten sam kod jest uruchamiany podczas uruchamiania / zamykania, niezależnie od tego, czy użytkownik wybierze linkowanie statyczne czy dynamiczne.

janneb
źródło
49
Podwójne nawiasy kwadratowe ułatwiają „wyprowadzenie makra” ( #define __attribute__(x)). Jeśli masz wiele atrybutów, np __attribute__((noreturn, weak)). Trudno byłoby „wyprowadzić makro”, gdyby istniał tylko jeden zestaw nawiasów.
Chris Jester-Young
7
To nie koniec .init/.fini. (Możesz poprawnie mieć wiele konstruktorów i destruktorów w jednej jednostce tłumaczącej, nigdy nie wskazywać wielu w jednej bibliotece - jak by to działało?) Zamiast tego, na platformach używających formatu binarnego ELF (Linux itp.), Odniesienia do konstruktorów i destruktorów w sekcjach .ctorsi .dtorsnagłówka. To prawda, że ​​w dawnych czasach funkcje o nazwach initi finibyłyby uruchamiane przy dynamicznym ładowaniu i zwalnianiu biblioteki, gdyby istniały, ale teraz jest to przestarzałe, zastępowane przez ten lepszy mechanizm.
ephemient
7
@jcayzac Nie, ponieważ makrami variadic jest rozszerzenie gcc, a głównym powodem wykluczania makr __attribute__jest to, że nie używasz gcc, ponieważ to także jest rozszerzenie gcc.
Chris Jester-Young,
9
@ Makra różnorodne ChrisJester-Young są standardową funkcją C99, a nie rozszerzeniem GNU.
jcayzac
4
„twoje użycie czasu teraźniejszego („ make ”zamiast„ made ”- podwójne pareny nadal ułatwiają ich wyskakiwanie z makr. Zaszczekałeś niewłaściwe drzewo pedantyczne.
Jim Balter, 16'14
64

.init/ .fininie jest przestarzałe. Nadal jest częścią standardu ELF i śmiem twierdzić, że będzie na zawsze. Kod w .init/ .finijest uruchamiany przez moduł ładujący / wykonawczy-linker, gdy kod jest ładowany / rozładowywany. Tj. Na każdym ładowaniu ELF (na przykład w bibliotece współdzielonej) .initzostanie uruchomiony. Nadal można użyć tego mechanizmu, aby osiągnąć mniej więcej to samo, co w przypadku __attribute__((constructor))/((destructor)). Jest oldschoolowy, ale ma pewne zalety.

.ctors.dtorsNa przykład mechanizm / wymaga obsługi przez skrypt system-rtl / loader / linker-script. Jest to dalekie od pewności, że będzie dostępne we wszystkich systemach, na przykład głęboko osadzonych systemach, w których kod jest wykonywany na czystym metalu. To __attribute__((constructor))/((destructor))znaczy, nawet jeśli jest obsługiwany przez GCC, nie jest pewne, czy będzie działać, ponieważ to do konsolidatora należy go zorganizować i do modułu ładującego (lub w niektórych przypadkach kodu rozruchowego), aby go uruchomić. Aby użyć .init/ .finizamiast, najprostszym sposobem jest użycie flag linkera: -init & -fini (tj. Z linii poleceń GCC, składnia byłaby -Wl -init my_init -fini my_fini).

W systemie obsługującym obie metody jedną z możliwych korzyści jest to, że kod .initjest uruchamiany przed, .ctorsa kod .finipóźniej .dtors. Jeśli kolejność jest istotna, to przynajmniej jeden prosty, ale łatwy sposób na rozróżnienie między funkcjami inicjowania / kończenia.

Główną wadą jest to, że nie można łatwo mieć więcej niż jednej _initi jednej _finifunkcji na każdy moduł, który można załadować, i prawdopodobnie trzeba by fragmentować kod w więcej .soniż motywowany. Innym jest fakt, że podczas korzystania z metody linkera opisanej powyżej, zastępuje się oryginalną _finifunkcję _init i funkcje domyślne (dostarczone przez crti.o). W tym miejscu zwykle dochodzi do różnego rodzaju inicjalizacji (w Linuksie inicjowane jest przypisywanie zmiennych globalnych). Sposób na to opisano tutaj

Zauważ w powyższym linku, że kaskadowanie do oryginału _init()nie jest potrzebne, ponieważ nadal jest na swoim miejscu. W callzestawie wbudowanym jest jednak mnemonik x86, a wywołanie funkcji z zestawu wyglądałoby zupełnie inaczej dla wielu innych architektur (na przykład ARM). Tj. Kod nie jest przejrzysty.

.initMechanizmy / .finii .ctors/ .detorssą podobne, ale niezupełnie. Kod w .init/ .finidziała „jak jest”. To znaczy , możesz mieć kilka funkcji w .init/ .fini, ale AFAIK trudno syntaktycznie umieścić je tam w pełni transparentnie w czystym C bez rozbijania kodu w wielu małych .soplikach.

.ctors/ .dtorssą inaczej zorganizowane niż .init/ .fini. Sekcje .ctors/ .dtorssą tylko tabelami ze wskaźnikami do funkcji, a „obiekt wywołujący” to dostarczona przez system pętla, która wywołuje każdą funkcję pośrednio. To znaczy, że osoba wywołująca pętlę może być specyficzna dla architektury, ale ponieważ jest częścią systemu (jeśli w ogóle istnieje, tj.), Nie ma to znaczenia.

Poniższy fragment kodu dodaje nowe wskaźniki funkcji do .ctorstablicy funkcji, zasadniczo w ten sam sposób, co __attribute__((constructor))robi (metoda może współistnieć z __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;

Można również dodać wskaźniki funkcji do zupełnie innej wymyślonej przez siebie sekcji. W takim przypadku potrzebny jest zmodyfikowany skrypt linkera i dodatkowa funkcja naśladująca moduł ładujący .ctors/ .dtorspętlę. Ale dzięki niemu można uzyskać lepszą kontrolę nad kolejnością wykonywania, dodawać argumenty argumentu i obsługiwać kod powrotu (np. W projekcie C ++ byłoby przydatne, gdyby potrzebował czegoś działającego przed lub po globalnych konstruktorach).

Wolę __attribute__((constructor))/((destructor))tam, gdzie to możliwe, jest to proste i eleganckie rozwiązanie, nawet jeśli czuję się jak oszukiwanie. W przypadku koderów bez systemu operacyjnego, takich jak ja, nie zawsze jest to opcja.

Dobre referencje w książce Łączniki i ładowarki .

Michael Ambrus
źródło
jak moduł ładujący może wywoływać te funkcje? funkcje te mogą wykorzystywać globalne i inne funkcje w przestrzeni adresowej procesu, ale moduł ładujący jest procesem z własną przestrzenią adresową, prawda?
user2162550
@ user2162550 Nie, ld-linux.so.2 (zwykły „interpreter”, moduł ładujący biblioteki dynamiczne działający na wszystkich dynamicznie połączonych plikach wykonywalnych) działa w samej przestrzeni adresowej samego pliku wykonywalnego. Ogólnie rzecz biorąc, sam dynamiczny moduł ładujący bibliotekę jest czymś specyficznym dla przestrzeni użytkownika i działa w kontekście wątku, który próbuje uzyskać dostęp do zasobu biblioteki.
Paul Stelian,
Kiedy wywołuję execv () z kodu, który ma __attribute__((constructor))/((destructor))destruktor nie działa. Próbowałem kilka rzeczy, takich jak dodanie wpisu .dtor, jak pokazano powyżej. Ale bez powodzenia. Problem można łatwo powielić, uruchamiając kod za pomocą numactl. Załóżmy na przykład, że kod_testowy zawiera destruktor (dodaj printf do funkcji konstruktora i desctructor, aby debugować problem). Potem biegnij LD_PRELOAD=./test_code numactl -N 0 sleep 1. Zobaczysz, że konstruktor jest wywoływany dwa razy, ale destruktor tylko raz.
B Abali
39

Ta strona zapewnia doskonałe zrozumienie implementacji constructori destructoratrybutów oraz sekcji w ELF, które pozwalają im pracować. Po zapoznaniu się z informacjami tutaj, skompilowałem trochę dodatkowych informacji i (pożyczając przykład sekcji od Michaela Ambrusa powyżej) stworzyłem przykład, aby zilustrować pojęcia i pomóc w nauce. Wyniki te podano poniżej wraz z przykładowym źródłem.

Jak wyjaśniono w tym wątku, atrybuty constructori destructortworzą wpisy w sekcji .ctorsi .dtorspliku obiektowego. Możesz umieścić odniesienia do funkcji w każdej sekcji na jeden z trzech sposobów. (1) za pomocą sectionatrybutu; (2) constructori destructoratrybuty lub (3) z wywołaniem assemblera (zgodnie z linkiem w odpowiedzi Ambrusa).

Zastosowanie constructori destructoratrybuty pozwalają dodatkowo przypisać priorytet konstruktorowi / destruktorowi, aby kontrolować jego kolejność wykonywania przed main()wywołaniem lub po powrocie. Im niższa podana wartość priorytetu, tym wyższy priorytet wykonania (niższe priorytety wykonują przed wyższymi priorytetami przed main () - i kolejne wyższe priorytety po main ()). Podane wartości priorytetów muszą być większe niż100 w przypadku, gdy kompilator rezerwuje wartości priorytetów w zakresie od 0-100 do wdrożenia. A constructorlub destructorokreślone z priorytetem jest wykonywane przed a constructorlub destructorokreślone bez priorytetu.

Z „sekcja” atrybutu lub z inline-montaż, można również odniesienia funkcyjne miejsce w .initi .finisekcji kodu ELF, które zostaną wykonane przed każdym konstruktora i po każdym destructor, odpowiednio. Wszelkie funkcje wywoływane przez odwołanie do funkcji umieszczone w .initsekcji będą wykonywane przed samym odwołaniem do funkcji (jak zwykle).

Próbowałem zilustrować każdą z nich w poniższym przykładzie:

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

wynik:

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

Przykład pomógł utrwalić zachowanie konstruktora / destruktora, miejmy nadzieję, że przyda się także innym.

David C. Rankin
źródło
Gdzie odkryłeś, że „podane przez ciebie wartości priorytetów muszą być większe niż 100”? Ta informacja nie jest obecna w dokumentacji atrybutów funkcji GCC.
Justin,
4
IIRC, było kilka odniesień, PATCH: Argument priorytetu obsługi dla argumentów konstruktora / destruktora ( MAX_RESERVED_INIT_PRIORITY) i że były one takie same jak C ++ ( init_priority) 7.7 C ++ - zmienna specyficzna, funkcja i atrybuty typu . Potem próbowałem go 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
Ach Próbowałem priorytetów <100 z clang i wydawało się, że to działa, ale mój prosty przypadek testowy (jedna jednostka kompilacji) był zbyt prosty .
Justin,
1
Jaki jest priorytet statycznych zmiennych globalnych (statyczne statki)?
dashy
2
Efekt i widoczność statycznego globu będzie zależeć od struktury twojego programu (np. Pojedynczy plik, wiele plików ( jednostki tłumaczeniowe )) oraz w którym deklarowany jest globalny. Patrz: Statyczny (słowo kluczowe) , w szczególności opis globalnej zmiennej statycznej .
David C. Rankin
7

Oto „konkretny” (i prawdopodobnie przydatny ) przykład tego, jak, dlaczego i kiedy używać tych poręcznych, ale nieestetycznych konstrukcji ...

Xcode używa „globalnego” „domyślnego użytkownika”, aby zdecydować, która XCTestObserverklasa wyrzuca swoje serce do oblężonej konsoli.

W tym przykładzie ... kiedy domyślnie ładuję tę bibliotekę psuedo, nazwijmy ją ... libdemure.a, poprzez flagę w moim testowym celu á la ..

OTHER_LDFLAGS = -ldemure

Chcę..

  1. Podczas ładowania (tj. Gdy XCTestładuje mój pakiet testowy), przesłoń XCTestklasę „domyślnego” „obserwatora” ... (za pomocą constructorfunkcji) PS: O ile wiem ... wszystko, co tu zrobiono, mogłoby zostać wykonane z równoważnym skutkiem wewnątrz mojego + (void) load { ... }metoda klasy .

  2. uruchom moje testy .... w tym przypadku, przy mniej logicznej gadatliwości w logach (implementacja na żądanie)

  3. Przywróć XCTestObserverklasę „globalną” do pierwotnego stanu, aby nie zakłócać innych XCTestbiegów, które nie dotarły do ​​modnego wozu (zwanego też linkiem libdemure.a). Wydaje mi się, że historycznie zostało to zrobione w dealloc... ale nie zamierzam zaczynać z tym starym wiedźmą.

Więc...

#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

Bez flagi linkera ... (rój mody policja Cupertino domaga się zemsty , jednak domyślnie Apple dominuje, jak jest pożądane, tutaj )

wprowadź opis zdjęcia tutaj

Z -ldemure.aflagą linkera ... (Zrozumiałe wyniki, zadyszka ... "dzięki constructor/ destructor" ... Tłum wiwatuje ) wprowadź opis zdjęcia tutaj

Alex Gray
źródło
1

Oto kolejny konkretny przykład: biblioteka współdzielona. Główną funkcją biblioteki współdzielonej jest komunikacja z czytnikiem kart inteligentnych. Ale może również otrzymywać „informacje o konfiguracji” w czasie wykonywania przez udp. Udp jest obsługiwany przez wątek, który MUSI zostać uruchomiony w czasie inicjacji.

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

  }

Biblioteka została napisana w ok.

drlolly
źródło
1
Dziwny wybór, jeśli biblioteka jest napisana w C ++, ponieważ zwykłe konstruktory zmiennych globalnych są idiomatycznym sposobem uruchamiania kodu w Main C ++.
Nicholas Wilson,
@NicholasWilson Biblioteka została faktycznie napisana w ok. Nie wiem, jak wpisałem c ++ zamiast c.
drlolly,