Как именно работает __attribute __ ((конструктор))?

350

Кажется, довольно ясно, что он должен все настроить.

  1. Когда именно он запускается?
  2. Почему есть две скобки?
  3. Это __attribute__функция? Макрос? Синтаксис?
  4. Это работает в C? 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];
}
Casebash
источник

Ответы:

274
  1. Он запускается при загрузке разделяемой библиотеки, обычно при запуске программы.
  2. Вот как все атрибуты GCC; предположительно, чтобы отличить их от вызовов функций.
  3. GCC-специфичный синтаксис.
  4. Да, это работает на C и C ++.
  5. Нет, функция не должна быть статичной.
  6. Деструктор запускается, когда разделяемая библиотека выгружается, обычно при выходе из программы.

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

Если подумать, в обычном статическом компоновщике, возможно, есть нечто подобное, так что один и тот же код запускается при запуске / завершении работы независимо от того, выбирает ли пользователь статическое или динамическое связывание.

janneb
источник
49
Двойные скобки облегчают их «макросъемку» ( #define __attribute__(x)). Если у вас есть несколько атрибутов, например, __attribute__((noreturn, weak))было бы трудно "вычеркнуть" макрос, если бы был только один набор скобок.
Крис Шут-Янг
7
Это не сделано с .init/.fini. (Вы можете действительно иметь несколько конструкторов и деструкторов в одном модуле перевода, не обращая внимания на несколько в одной библиотеке - как это будет работать?) Вместо этого на платформах, использующих двоичный формат ELF (Linux и т. Д.), На конструкторы и деструкторы ссылаются в .ctorsи .dtorsсекциях заголовка. Правда, в прежние времена функции с именами initи finiвыполнялись при динамической загрузке и выгрузке библиотеки, если бы они существовали, но сейчас это устарело, заменив этот более совершенный механизм.
Эфимент
7
@jcayzac Нет, потому что variadic-макросы - это расширение gcc, и основная причина макропроцессирования __attribute__- это то, что вы не используете gcc, поскольку это тоже расширение gcc.
Крис Джестер-Янг
9
@ ChrisJester-Young variadic макросы являются стандартной функцией C99, а не расширением GNU.
Jcayzac
4
«Вы используете настоящее время (« сделайте »вместо« сделанного »- двойные скобки по- прежнему облегчают их макросъемку. Вы закололи не то педантичное дерево.
Джим Балтер,
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).

В системе, поддерживающей оба метода, одним из возможных преимуществ является то, что код in .initзапускается до, .ctorsа код - .finiпосле .dtors. Если порядок важен, это как минимум один грубый, но простой способ различить функции инициализации / выхода.

Основным недостатком является то, что вы не можете легко иметь более одной _initи одной _finiфункции на каждый загружаемый модуль и, вероятно, придется фрагментировать код более .soчем мотивированным. Другое состоит в том, что при использовании метода компоновщика, описанного выше, один заменяет оригинальный _init и _finiфункции по умолчанию (предоставленные crti.o). Это где все виды инициализации обычно происходят (в Linux это где глобальное назначение переменных инициализируется). Обход, который описан здесь

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

.init/ .finiи .ctors/ .detorsмеханизмы похожи, но не совсем. Код в .init/ .finiработает "как есть". Т.е. у вас может быть несколько функций в .init/ .fini, но синтаксически сложно AFAIK поместить их там полностью прозрачно в чистом C, не разбивая код на множество маленьких .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))где это возможно, это простое и элегантное решение, даже если оно кажется обманом. Для таких программистов, как я, это не всегда вариант.

Несколько хороших ссылок в книге « Линкеры и загрузчики» .

Майкл Амбрус
источник
как загрузчик может вызвать эти функции? эти функции могут использовать глобальные и другие функции в адресном пространстве процесса, но загрузчик - это процесс с собственным адресным пространством, не так ли?
user2162550
@ user2162550 Нет, ld-linux.so.2 (обычный «интерпретатор», загрузчик динамических библиотек, который запускается на всех динамически связанных исполняемых файлах) работает в самом адресном пространстве самого исполняемого файла. В общем случае динамический загрузчик библиотеки - это нечто специфическое для пользовательского пространства, работающее в контексте потока, который пытается получить доступ к ресурсу библиотеки.
Пол Стелиан
Когда я вызываю execv () из кода, который имеет __attribute__((constructor))/((destructor))деструктор, не запускается. Я пробовал несколько вещей, таких как добавление записи в .dtor, как показано выше. Но безуспешно. Эту проблему легко продублировать, запустив код с помощью numactl. Например, предположим, что test_code содержит деструктор (добавьте printf к функциям конструктора и desctructor для устранения проблемы). Тогда беги 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()вызывается или после того как он возвращается. Чем ниже заданное значение приоритета, тем выше приоритет выполнения (более низкие приоритеты выполняются до более высоких приоритетов перед main () - и после более высоких приоритетов после main ()). Заданные вами значения приоритета должны быть больше, чем100 когда компилятор резервирует значения приоритета от 0 до 100 для реализации. A constructorили destructorуказанный с приоритетом выполняется до constructorили destructorуказанный без приоритета.

С помощью атрибута «раздела» или рядных сборки, вы можете также ссылки на функцию места в .initи .finiсекции ELF коды , который будет выполняться до любого конструктора и после любого деструктора, соответственно. Любые функции, вызываемые ссылкой на функцию, размещенную в .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 с помощью clang, и это, похоже, работало, но мой простой контрольный пример (один блок компиляции) был слишком простым .
Джастин
1
Каков приоритет статических глобальных переменных (статических ctors)?
дашесы
2
Эффект и видимость статического глобала будет зависеть от того, как структурирована ваша программа (например, один файл, несколько файлов ( единицы перевода )), и в котором объявлено глобальное значение. См .: Статический (ключевое слово) , в частности, описание глобальной переменной Статическое .
Дэвид С. Ранкин,
7

Вот «конкретный» (и, возможно, полезный ) пример того, как, почему и когда использовать эти удобные, но неприглядные конструкции ...

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

В этом примере ... когда я неявно загружаю эту псевдо-библиотеку, давайте назовем ее ... 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 обрабатывается потоком, который ДОЛЖЕН быть запущен во время инициализации.

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

  }

Библиотека была написана в c.

drlolly
источник
1
Странный выбор, если библиотека написана на C ++, поскольку обычные конструкторы глобальных переменных являются идиоматическим способом запуска предварительного кода в C ++.
Николас Уилсон
@NicholasWilson Библиотека была фактически написана в c. Не знаю, как я набрал c ++ вместо c.
Drlolly