Як саме працює __attribute __ ((конструктор))?


348

Здається, досить зрозуміло, що він повинен налаштувати речі.

  1. Коли саме він працює?
  2. Чому існують дві дужки?
  3. Це __attribute__функція? Макрос? Синтаксис?
  4. Чи працює це в С? C ++?
  5. Чи функція, з якою вона працює, повинна бути статичною?
  6. Коли __attribute__((destructor))працює?

Приклад в Objective-C :

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

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

Відповіді:


273
  1. Вона працює під час завантаження спільної бібліотеки, як правило, під час запуску програми.
  2. Ось так є всі атрибути GCC; імовірно, щоб відрізнити їх від викликів функцій.
  3. Специфічний для GCC синтаксис.
  4. Так, це працює в C і C ++.
  5. Ні, функція не повинна бути статичною.
  6. Деструктор працює при завантаженні спільної бібліотеки, як правило, при виході з програми.

Отже, спосіб роботи конструкторів та деструкторів полягає в тому, що спільний файл об'єктів містить спеціальні розділи (.ctors і .dtors на ELF), які містять посилання на функції, позначені відповідно атрибутами конструктора та destructor. Коли бібліотека завантажується / вивантажується, програма динамічного завантажувача (ld.so або somesuch) перевіряє, чи існують такі розділи, і якщо так, викликає функції, на які посилається.

Подумайте про це, мабуть, є якась подібна магія у звичайному статичному лінкері, так що той самий код запускається при запуску / відключенні незалежно від того, чи користувач вибирає статичне або динамічне посилання.


49
Подвійні дужки дозволяють легко "макросувати" ( #define __attribute__(x)). Якщо у вас є кілька атрибутів, наприклад, __attribute__((noreturn, weak))важко буде "макрос", якби був лише один набір дужок.
Кріс Єстер-Янг

7
З цим не робиться .init/.fini. (Ви можете дійсно мати декілька конструкторів і деструкторів в одному блоці перекладу, ніколи не пам’ятайте про кілька в одній бібліотеці - як це буде працювати?) Натомість на платформах, що використовують бінарний формат ELF (Linux тощо), посилаються конструктори та деструктори. у заголовках .ctorsта .dtorsрозділах. Щоправда, за старих часів функції, які були названі initта finiзапускалися б при динамічному завантаженні та розвантаженні бібліотеки, якби вони існували, але це вже застаріло, замінено цим кращим механізмом.
ефемія

7
@jcayzac Ні, оскільки варіативні макроси - це розширення gcc, і головна причина макросування __attribute__- якщо ви не використовуєте gcc, оскільки це теж розширення gcc.
Кріс Єстер-Янг

9
@ ChrisJester-Young варіативні макроси - це стандартна функція C99, а не розширення GNU.
jcayzac

4
"Ви використовуєте теперішній час (" make "замість" made "- подвійні паролі все ще дозволяють легко макросувати. Ви гавкали неправильне педантичне дерево.
Джим Балтер

64

.init/ .finiне застаріло. Це все ще є частиною стандарту ELF, і я наважусь сказати, що це буде назавжди. Код в .init/ .finiзапускається завантажувачем / програмою-лінкером, коли код завантажується / вивантажується. Тобто на кожному завантаженні ELF (наприклад, спільна бібліотека) .initбуде запущений код у . Ще можна використовувати цей механізм для досягнення приблизно того ж, що і з __attribute__((constructor))/((destructor)). Це стара школа, але вона має деякі переваги.

.ctors/ .dtorsмеханізм, наприклад, вимагає підтримки system-rtl / loader / linker-script. Це далеко не напевно доступне для всіх систем, наприклад, глибоко вбудованих систем, де код виконується на голому металі. Тобто, навіть якщо __attribute__((constructor))/((destructor))він підтримується GCC, невідомо, що він запуститься, оскільки це залежить від того, що він може його організувати, і завантажувач (або в деяких випадках завантажувальний код) для його запуску. Для використання .init/ .finiнатомість найпростішим способом є використання прапорів-лінкерів: -init & -fini (тобто з командного рядка GCC, це був би синтаксис -Wl -init my_init -fini my_fini).

У системі, що підтримує обидва методи, однією з можливих переваг є те, що код у .initзапускається до, .ctorsа код - .finiпісля .dtors. Якщо замовлення доречне, це хоча б один неочищений, але простий спосіб розрізнити функції init / exit.

Основним недоліком є ​​те, що ви не можете легко мати більше однієї _initта однієї _finiфункції на кожен завантажуваний модуль, і, ймовірно, доведеться фрагментувати код більш .soніж мотивованим. Інша полягає в тому, що, використовуючи описаний вище метод лінкера, один замінює оригінальні функції _init та _finiза замовчуванням (надаються crti.o). Тут зазвичай відбуваються всілякі ініціалізації (в Linux це ініціалізація глобальних змінних). Шлях, описаний тут, описаний

Зауважте за посиланням вище, що каскадування оригіналу _init()не потрібно, оскільки воно все ще існує. Однак callвбудована збірка є x86-мнемонічною, а виклик функції з складання виглядав би зовсім інакше для багатьох інших архітектур (наприклад, ARM). Тобто код не прозорий.

.init/ .finiта .ctors/ .detorsмеханізми схожі, але не зовсім. Код в .init/ .finiпрацює "як є". Тобто ви можете мати кілька функцій в .init/ .fini, але синтаксично важко розмістити їх там, у чистому С, без розбиття коду у багатьох невеликих .soфайлах.

.ctors/ .dtorsорганізовані інакше, ніж .init/ .fini. .ctors/ .dtorsрозділи - це просто таблиці з покажчиками на функції, а "абонент" - це цикл, що надається системою, який викликає кожну функцію опосередковано. Тобто виклик циклу може бути специфічним для архітектури, але оскільки він є частиною системи (якщо вона взагалі існує, тобто), це не має значення.

Наступний фрагмент додає нові функції покажчиків до .ctorsмасиву функцій, головним чином так само, як і __attribute__((constructor))це (метод може співіснувати з __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;

Також можна додати покажчики функцій до зовсім іншого самостійно винайденого розділу. У такому випадку потрібен модифікований сценарій зв’язку та додаткова функція, що імітує завантажувач .ctors/ .dtorsцикл. Але за допомогою цього можна досягти кращого контролю над порядком виконання, додавати вхідний аргумент і повертати код обробки етапу (наприклад, у проекті C ++, було б корисно, якщо вам потрібно щось, що працює до або після глобальних конструкторів).

Я вважаю за краще, __attribute__((constructor))/((destructor))де це можливо, це просте і елегантне рішення, навіть таке відчуття, як обман. Для гофрованих металевих кодерів, як я, це просто не завжди варіант.

Кілька хороших посилань у книзі Повізники та навантажувачі .


як може навантажувач викликати ці функції? ці функції можуть використовувати глобальні та інші функції в адресному просторі процесу, але завантажувач - це процес із власним адресним простором, чи не так?
користувач2162550

@ user2162550 Ні, ld-linux.so.2 (звичайний "інтерпретатор", завантажувач динамічних бібліотек, який працює на всіх динамічно пов'язаних виконуваних файлах) працює в самому адресному просторі самого виконуваного файлу. Взагалі, сам динамічний завантажувач бібліотеки є чимось специфічним для простору користувачів, що працює в контексті потоку, який намагається отримати доступ до бібліотечного ресурсу.
Пол

Коли я викликаю execv () з коду, який має __attribute__((constructor))/((destructor))деструктор, не працює. Я спробував кілька речей, таких як додавання запису до .dtor, як показано вище. Але успіху немає. Дублювати проблему легко, запустивши код з numactl. Наприклад, припустимо, тестовий код містить деструктор (додайте printf до функцій конструктора та дектруктора для налагодження проблеми). Потім бігайте LD_PRELOAD=./test_code numactl -N 0 sleep 1. Ви побачите, що конструктор викликається двічі, а деструктор лише один раз.
Б Абалі

39

На цій сторінці ви знайдете широке розуміння щодо constructorта використання destructorатрибутів та розділів в межах ELF, які дозволяють їм працювати. Перетравивши надану тут інформацію, я зібрав трохи додаткової інформації і (запозичивши приклад розділу у Майкла Амбруса вище) створив приклад, щоб проілюструвати поняття та допомогти моєму навчанню. Ці результати наведено нижче разом із прикладом джерела.

Як пояснюється в цій темі, то constructorі destructorатрибути створення записів в .ctorsі .dtorsсекції об'єктного файлу. Ви можете розмістити посилання на функції в будь-якому розділі одним із трьох способів. (1) використовуючи або sectionатрибут; (2) constructorта destructorатрибути або (3) з викликом вбудованої лінії (як посилається на відповідь Амбруса).

Використання constructorта destructorатрибути дозволяють додатково призначити пріоритет конструктору / деструктору, щоб контролювати його порядок виконання до main()виклику або після його повернення. Чим нижче задане значення пріоритету, тим вище пріоритет виконання (нижчі пріоритети виконуються перед вищими пріоритетами перед головним () - і наступні за більш високими пріоритетами після головного ()). Значення пріоритету, які ви надаєте, повинні бути більшими,100 оскільки компілятор резервує значення пріоритету між 0-100 для реалізації. А constructorабо destructorвказаний з пріоритетним виконанням перед constructorабо destructorвказаним без пріоритету.

За допомогою атрибута 'section' або з вбудованою вбудованою програмою ви також можете розмістити посилання функцій у розділі коду .initта .finiELF, яке буде виконуватися перед будь-яким конструктором та після будь-якого деструктора відповідно. Будь-які функції, викликані посиланням на функцію, розміщеним у .initрозділі, виконуватимуться перед самим посиланням на функцію (як зазвичай).

Я спробував проілюструвати кожен із наведених нижче прикладів:

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

вихід:

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

Приклад допоміг цементувати поведінку конструктора / деструктора, сподіваємось, він буде корисним і для інших.


Де ви виявили, що "значення пріоритету, яке ви даєте, повинно бути більше 100"? Ця інформація відсутня в документації атрибутів функції GCC.
Джастін

4
IIRC, було кілька посилань, PATCH: Аргумент пріоритету підтримки аргументів конструктора / деструктора ( MAX_RESERVED_INIT_PRIORITY), і що вони були такими ж, як C ++ ( init_priority) 7.7 C ++ - специфічні атрибути змінних, функцій та типів . Тоді я спробував це 99: warning: constructor priorities from 0 to 100 are reserved for the implementation [enabled by default] void construct0 () __attribute__ ((constructor (99)));.
Девід К. Ранкін

1
Ага. Я спробував пріоритети <100 з клангом, і, здавалося, це працює, але мій простий тестовий випадок (єдиний блок компіляції) був надто простим .
Джастін

1
Який пріоритет статичних глобальних змінних (статичних ctors)?
тире

2
Ефект і видимість статичного глобального буде залежати від структури вашої програми (наприклад, одного файлу, декількох файлів ( одиниць перекладу )) і в якому глобальний оголошений Див.: Статичний (ключове слово) , зокрема опис глобальної змінної статичної .
Девід К. Ранкін

7

Ось "конкретний" (і, можливо, корисний ) приклад того, як, чому і коли використовувати ці зручні, але непривабливі конструкції ...

Xcode використовує «глобальний» «користувача по замовчуванням» , щоб вирішити , який XCTestObserverклас вивергає це серце до обложеному консолі.

У цьому прикладі ... коли я неявно завантажую цю бібліотеку psuedo, давайте назвемо її ... libdemure.aчерез прапор моєї тестової цілі.

OTHER_LDFLAGS = -ldemure

Я хочу..

  1. При навантаженні (тобто, коли XCTestзавантажуєте мій тестовий пакет), перейдіть до XCTestкласу "спостерігач " за замовчуванням " (за допомогою constructorфункції) PS: Наскільки я можу сказати ... все, що тут зроблено, можна зробити з еквівалентним ефектом всередині мого клас ' + (void) load { ... }метод.

  2. запустити мої тести .... у цьому випадку з меншою кількістю нерівних багатослівностей у журналах (реалізація за запитом)

  3. Поверніть "глобальний" XCTestObserverклас до його первозданного стану .. щоб не зіпсувати інші XCTestпрогони, які не потрапили на смугу (ака. Зв'язана з libdemure.a). Я здогадуюсь, що це історично було зроблено в dealloc.., але я не збираюся починати возитися з тим старим хагом.

Тому...

#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

Без прапора лінкера ... (Мода-поліція роя Купертіно, вимагаючи відплати , але тут дефолт Apple переважає, як хочеться, тут )

введіть тут опис зображення

З -ldemure.aпрапором лінкера ... (зрозумілі результати, ах ... "спасибі constructor/ destructor" ... натовп ура ) введіть тут опис зображення


1

Ось ще один конкретний приклад. Це для спільної бібліотеки. Основна функція спільної бібліотеки - це спілкування з читачем смарт-карт. Але він також може отримувати "інформацію про конфігурацію" під час виконання через udp. Udp обробляється ниткою, яку ОБОВ'ЯЗКОВО запустити під час init.

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

  }

Бібліотека була написана c.


1
Незвичайний вибір, якщо бібліотека написана на C ++, оскільки звичайні глобальні конструктори змінних є ідіоматичним способом запуску коду до основного в C ++.
Ніколас Вілсон

@NicholasWilson Фактично бібліотека була написана в c. Не знаю, як я набрав c ++ замість c.
drlolly
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.