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

262

Кажется довольно ясным, что предполагается, что все будет сделано.

  • Когда он выполняется,
  • Почему существуют две круглые скобки?
  • Является ли __attribute__ функцией? Макрос? Синтаксис?
  • Это работает в C? С++?
  • Работает ли функция, с которой она работает, должна быть статичной?
  • Когда выполняется __attribute__((destructor))?

Пример в Objective C:

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

__attribute__((destructor))
static void destroy_navigationBarImages() {
  [navigationBarImages release];
}
Теги:
gcc
compiler-construction

5 ответов

242
Лучший ответ
  • Выполняется при загрузке разделяемой библиотеки, как правило, во время запуска программы.
  • Как все атрибуты GCC; предположительно, чтобы отличать их от вызовов функций.
  • Синтаксис, специфичный для GCC.
  • Да, это также работает в C на С++.
  • Нет, функция не обязательно должна быть статичной.
  • Деструктор запускается, когда разделяемая библиотека выгружается, как правило, при выходе из программы.

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

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

  • 42
    Двойные скобки облегчают их «макросъемку» ( #define __attribute__(x) ). Если у вас есть несколько атрибутов, например, __attribute__((noreturn, weak)) , было бы трудно «макросъединить», если бы был только один набор скобок.
  • 6
    Это не сделано с .init/.fini . (Вы можете действительно иметь несколько конструкторов и деструкторов в одном модуле перевода, не обращая внимания на несколько в одной библиотеке - как это будет работать?) Вместо этого на платформах, использующих двоичный формат ELF (Linux и т. Д.), На конструкторы и деструкторы ссылаются в .ctors и .dtors заголовка. Правда, в старые времена функции с именами init и fini запускались при динамической загрузке и выгрузке библиотеки, если бы они существовали, но сейчас это не рекомендуется, заменяя этот более совершенный механизм.
Показать ещё 10 комментариев
48

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

Например, механизм

.ctors/.dtors требует поддержки системой-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, но синтаксически сложно помещать их там полностью прозрачно в чистом 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;

Можно также добавить указатели на функции в совершенно другой самостоятельный раздел. В этом случае необходим модифицированный компоновщик script и дополнительная функция, имитирующая цикл загрузчика .ctors/.dtors. Но с этим можно добиться лучшего контроля над порядком выполнения, добавить в аргумент и вернуть код обработки e.t.a. (Например, в проекте на С++ это было бы полезно, если бы в нем что-то работало до или после глобальных конструкторов).

Я бы предпочел __attribute__((constructor))/((destructor)), где это было возможно, это простое и элегантное решение, даже если оно походит на обман. Для простых кодеров, подобных мне, это не всегда вариант.

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

  • 0
    как загрузчик может вызывать эти функции? эти функции могут использовать глобальные и другие функции в адресном пространстве процесса, но загрузчик - это процесс с собственным адресным пространством, не так ли?
25

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

Как объясняется в этом потоке, атрибуты constructor и destructor создают записи в разделе .ctors и .dtors объектного файла. Вы можете разместить ссылки на функции в любом разделе одним из трех способов. (1), используя либо атрибут section; (2) constructor и destructor или (3) с вызовом inline-assembly (как указано на ссылке в ответе Ambrus).

Использование атрибутов constructor и destructor позволяет дополнительно назначить приоритет конструктору/деструктору для управления его порядком выполнения до вызова main() или после его возврата. Чем ниже значение приоритета, тем выше приоритет выполнения (более низкие приоритеты выполняются перед более высокими приоритетами перед main() - и после более высоких приоритетов после main()). Значения приоритетов, которые вы указываете , должны быть больше, чем 100, поскольку компилятор резервирует значения приоритета между 0-100 для реализации. A constructor или destructor, указанный с приоритетом, выполняется до constructor или destructor, указанных без приоритета.

С атрибутом "section" или с встроенной сборкой вы также можете разместить ссылки на функции в разделе кода .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

Пример помог цементировать поведение конструктора/деструктора, надеюсь, он будет полезен и другим.

  • 0
    Где вы обнаружили, что «значения приоритетов, которые вы задаете, должны быть больше 100»? Эта информация отсутствует в документации по атрибутам функций GCC.
  • 2
    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))); ,
Показать ещё 3 комментария
7

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

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

В этом примере... когда я неявно загружаю эту psuedo-библиотеку, позвоните ей... libdemure.a, через флаг в моей тестовой цели á la..

OTHER_LDFLAGS = -ldemure

Я хочу..

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

  • запустите мои тесты... в этом случае, с меньшей степенью детализации в журналах (реализация по запросу)

  • Верните "глобальный" XCTestObserver класс в его нетронутое состояние.. чтобы не испортить другие трассы XCTest, которые не попали на подножку (также связанный с libdemure.a). Я думаю, это исторически было сделано в dealloc.., но я не собираюсь начинать возиться с этой старой hag.

Итак...

#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 as if we had never been here.
  [USER_DEFS setObject:@"XCTestLog" 
                forKey:@"XCTestObserverClass"];
  [USER_DEFS synchronize];
}

...
@end

Без флага компоновщика... (Мост-полицейский рой Купертино требует возмездия, но по умолчанию Apple по умолчанию превалирует)

Изображение 1262

С флагом компоновщика -ldemure.a... (Понятные результаты, вздох... "спасибо constructor/destructor"... Толпа ура) Изображение 1263

0

Вот еще один конкретный пример. Это для общей библиотеки. Основной функцией общей библиотеки является общение с устройством чтения смарт-карт. Но он также может получать "информацию о конфигурации" во время выполнения над udp. Udp обрабатывается потоком, который MUST запускается во время init.

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

  }

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню