Como exatamente __atributo __ ((construtor)) funciona?


347

Parece bastante claro que é suposto configurar as coisas.

  1. Quando exatamente ele funciona?
  2. Por que existem dois parênteses?
  3. É __attribute__uma função? Uma macro? Sintaxe?
  4. Isso funciona em C? C ++?
  5. A função com a qual trabalha precisa ser estática?
  6. Quando __attribute__((destructor))funciona?

Exemplo no Objetivo-C :

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

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

Respostas:


273
  1. É executado quando uma biblioteca compartilhada é carregada, normalmente durante a inicialização do programa.
  2. É assim que todos os atributos do GCC são; presumivelmente para distingui-los das chamadas de função.
  3. Sintaxe específica do GCC.
  4. Sim, isso funciona em C e C ++.
  5. Não, a função não precisa ser estática.
  6. O destruidor é executado quando a biblioteca compartilhada é descarregada, normalmente na saída do programa.

Portanto, a maneira como os construtores e destruidores trabalham é que o arquivo de objeto compartilhado contenha seções especiais (.ctors e .dtors no ELF) que contêm referências às funções marcadas com os atributos construtor e destruidor, respectivamente. Quando a biblioteca é carregada / descarregada, o programa carregador dinâmico (ld.so ou algo assim) verifica se tais seções existem e, se houver, chama as funções nela mencionadas.

Pense bem, provavelmente existe alguma mágica semelhante no vinculador estático normal, para que o mesmo código seja executado na inicialização / desligamento, independentemente se o usuário escolher um link estático ou dinâmico.


49
Os colchetes duplos facilitam a "saída macro" ( #define __attribute__(x)). Se você tiver vários atributos, por exemplo, __attribute__((noreturn, weak))seria difícil "macro out" se houvesse apenas um conjunto de colchetes.
Chris Jester-Young

7
Ainda não acabou .init/.fini. (Você pode validamente ter vários construtores e destruidores em uma única unidade de tradução, não se preocupe com vários em uma única biblioteca - como isso funcionaria?) Em vez disso, em plataformas que usam o formato binário ELF (Linux, etc.), os construtores e destruidores são referenciados nas seções .ctorse .dtorsdo cabeçalho. É verdade que, antigamente, as funções nomeadas inite finiseriam executadas com carregamento e descarregamento dinâmico de biblioteca, se existissem, mas isso foi preterido agora, substituído por esse mecanismo melhor.
ephemient

7
@jcayzac Não, porque as macros variadas são uma extensão do gcc, e o principal motivo da exclusão __attribute__é se você não estiver usando o gcc, pois também é uma extensão do gcc.
Chris Jester-Young

9
As macros variadas do ChrisJester-Young são um recurso C99 padrão, não uma extensão GNU.
Jcayzac

4
"o uso do presente do indicativo (" make" em vez de 'feito' - os parênteses duplas ainda torná-los fáceis de fora macro Você latiu a árvore pedante errado..
Jim Balter

64

.init/ .fininão está obsoleto. Ainda faz parte do padrão ELF e ouso dizer que será para sempre. O código em .init/ .finié executado pelo carregador / runtime-linker quando o código é carregado / descarregado. Ou seja, em cada código de carregamento ELF (por exemplo, uma biblioteca compartilhada) .initserá executado. Ainda é possível usar esse mecanismo para obter a mesma coisa que com __attribute__((constructor))/((destructor)) . É à moda antiga, mas tem alguns benefícios.

.ctors/ .dtorsmecanismo, por exemplo, requer suporte do system-rtl / loader / linker-script. Isso está longe de estar disponível em todos os sistemas, por exemplo, sistemas profundamente embutidos nos quais o código é executado no bare metal. Ou seja, mesmo que o __attribute__((constructor))/((destructor))GCC seja suportado, não é certo que ele funcione, pois cabe ao vinculador organizá-lo e ao carregador (ou, em alguns casos, código de inicialização) executá-lo. Para usar .init/ em .finivez disso, a maneira mais fácil é usar sinalizadores de vinculador: -init & -fini (ou seja, na linha de comando do GCC, a sintaxe seria -Wl -init my_init -fini my_fini).

No sistema que suporta os dois métodos, um benefício possível é que o código in .inité executado antes .ctorse o código .finidepois .dtors. Se a ordem for relevante, é pelo menos uma maneira simples, porém fácil de distinguir entre as funções de inicialização / saída.

Uma grande desvantagem é que você não pode facilmente ter mais de uma _inite uma _finifunção por cada módulo carregável e provavelmente teria que fragmentar o código em mais do .soque motivado. Outra é que, ao usar o método vinculador descrito acima, um substitui as _finifunções _init e padrão originais (fornecidas por crti.o). É aqui que geralmente ocorrem todos os tipos de inicialização (no Linux é onde a atribuição global de variáveis ​​é inicializada). Uma maneira de contornar isso é descrita aqui

Observe no link acima que uma cascata no original _init()não é necessária, pois ainda está em vigor. ocall entanto, na montagem embutida é x86-mnemônico e chamar uma função de montagem pareceria completamente diferente para muitas outras arquiteturas (como o ARM, por exemplo). Ou seja, o código não é transparente.

.init/ .finie .ctors/ .detorsmecanismos são semelhantes, mas não exatamente. Código em .init/ .finiexecutado "como está". Ou seja, você pode ter várias funções em .init/ .fini, mas é AFAIK sintaticamente difícil colocá-las totalmente transparentes em C puro, sem quebrar o código em muitos .soarquivos pequenos .

.ctors/ .dtorssão organizados de forma diferente de .init/ .fini. .ctors/.dtors seções são apenas tabelas com ponteiros para funções, e o "chamador" é um loop fornecido pelo sistema que chama cada função indiretamente. Ou seja, o chamador de loop pode ser específico da arquitetura, mas como faz parte do sistema (se é que existe), isso não importa.

O fragmento a seguir adiciona novos ponteiros de .ctorsfunção à matriz de funções, principalmente da mesma maneira que __attribute__((constructor))faz (o método pode coexistir __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;

Pode-se também adicionar os ponteiros de função a uma seção auto-inventada completamente diferente. Nesse caso, é necessário um script vinculador modificado e uma função adicional que imite o carregador .ctors/ .dtorsloop. Mas, com ele, é possível obter um melhor controle sobre a ordem de execução, adicionar argumentos e retornar código eta (em um projeto C ++, por exemplo, seria útil se você precisasse de algo em execução antes ou depois dos construtores globais).

Eu preferiria sempre __attribute__((constructor))/((destructor))que possível, é uma solução simples e elegante, mesmo que pareça trapaça. Para codificadores bare-metal como eu, isso nem sempre é uma opção.

Alguma boa referência no livro Linkers & loaders .


como o carregador pode chamar essas funções? essas funções podem usar globais e outras funções no espaço de endereço do processo, mas loader é um processo com seu próprio espaço de endereço, não é?
user2162550

@ user2162550 Não, o ld-linux.so.2 (o "intérprete" usual, o carregador de bibliotecas dinâmicas que roda em todos os executáveis ​​vinculados dinamicamente) é executado no próprio espaço de endereço do próprio executável. Em geral, o próprio carregador de biblioteca dinâmica é algo específico para o espaço do usuário, executado no contexto do encadeamento que tenta acessar um recurso da biblioteca.
Paul Stelian

Quando eu chamo execv () do código que possui __attribute__((constructor))/((destructor))o destruidor não é executado. Tentei algumas coisas, como adicionar uma entrada ao .dtor, como mostrado acima. Mas sem sucesso. É fácil duplicar o problema executando o código com numactl. Por exemplo, suponha que test_code contenha o destruidor (adicione um printf às funções construtor e desctrutor para depurar o problema). Então corra LD_PRELOAD=./test_code numactl -N 0 sleep 1. Você verá que o construtor é chamado duas vezes, mas destruidor apenas uma vez.
B Abali

39

Esta página fornece grande compreensão sobre o constructore destructorimplementação atributo e as seções dentro dentro ELF que lhes permitem trabalhar. Depois de digerir as informações fornecidas aqui, eu compilei um pouco de informações adicionais e (emprestando o exemplo da seção de Michael Ambrus acima) criei um exemplo para ilustrar os conceitos e ajudar meu aprendizado. Esses resultados são fornecidos abaixo, juntamente com a fonte de exemplo.

Conforme explicado neste encadeamento, os atributos constructore destructorcriam entradas na seção .ctorse .dtorsdo arquivo de objeto. Você pode colocar referências a funções em qualquer seção de uma das três maneiras. (1) usando o sectionatributo; (2) constructore destructoratributos ou (3) com uma chamada de montagem em linha (conforme referenciado no link na resposta da Ambrus).

O uso de constructore destructoratributos permitem atribuir adicionalmente uma prioridade ao construtor / destruidor para controlar sua ordem de execução antes de main()ser chamado ou depois que ele retornar. Quanto mais baixo for o valor da prioridade, mais alta será a prioridade de execução (as prioridades mais baixas serão executadas antes das prioridades mais altas antes de main () - e subseqüentes às prioridades mais altas depois de main ()). Os valores de prioridade que você fornecer devem ser maiores do100 que o compilador reserva valores de prioridade entre 0 e 100 para implementação. Um constructorou destructorespecificado com prioridade é executado antes de um constructorou destructorespecificado sem prioridade.

Com o atributo 'section' ou com assembly em linha, você também pode colocar referências de função na seção de código .inite .finiELF que será executada antes de qualquer construtor e após qualquer destruidor, respectivamente. Quaisquer funções chamadas pela referência de função colocada na .initseção serão executadas antes da própria referência de função (como de costume).

Eu tentei ilustrar cada uma delas no exemplo abaixo:

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

resultado:

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

O exemplo ajudou a cimentar o comportamento construtor / destruidor, espero que seja útil para outros também.


Onde você descobriu que "os valores de prioridade que você fornece devem ser maiores que 100"? Essas informações não estão presentes na documentação dos atributos da função GCC.
Justin

4
No IIRC, havia algumas referências, PATCH: Argumento de prioridade de suporte para argumentos de construtor / destruidor ( MAX_RESERVED_INIT_PRIORITY), e que eram os mesmos que C ++ ( init_priority) 7.7 C ++ - Atributos específicos de variável, função e tipo . Então eu tentei com 99: warning: constructor priorities from 0 to 100 are reserved for the implementation [enabled by default] void construct0 () __attribute__ ((constructor (99)));.
David C. Rankin

11
Ah Tentei prioridades <100 com clang e parecia estar funcionando, mas meu caso de teste simples (uma única unidade de compilação) era muito simples .
Justin

11
Qual é a prioridade das variáveis ​​globais estáticas (estáticos)?
dashesy 8/08/16

2
O efeito e a visibilidade de um global estático dependerão de como o seu programa está estruturado (por exemplo, arquivo único, vários arquivos ( unidades de tradução )) e no qual o global é declarado Consulte: Estático (palavra-chave) , especificamente a descrição da variável global Estática .
David C. Rankin

7

Aqui está um exemplo "concreto" (e possivelmente útil ) de como, por que e quando usar essas construções úteis, porém desagradáveis ...

O Xcode usa um "global" "padrão do usuário" para decidir qual XCTestObserverclasse vomita seu coração no console sitiado .

Neste exemplo ... quando eu carrego implicitamente essa biblioteca psuedo, vamos chamá-la ... libdemure.a, através de uma bandeira no meu destino de teste à la ..

OTHER_LDFLAGS = -ldemure

Eu quero..

  1. No carregamento (ou seja, quando XCTestcarrega meu pacote de teste), substitua a XCTestclasse "padrão" "observador" ... (através da constructorfunção) PS: Tanto quanto eu sei ... tudo o que é feito aqui pode ser feito com efeito equivalente dentro do meu + (void) load { ... }método de classe ' .

  2. executar meus testes .... neste caso, com menos verbosidade inana nos logs (implementação mediante solicitação)

  3. Retorne a XCTestObserverclasse "global" ao seu estado primitivo .. para não estragar outras XCTestcorridas que ainda não entraram no movimento (também conhecido como .linked libdemure.a). Acho que isso foi feito historicamente em dealloc... mas não vou começar a mexer com aquela velha bruxa.

Assim...

#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

Sem a bandeira do vinculador ... (A polícia da moda enxame Cupertino exigindo retribuição , mas o padrão da Apple prevalece, conforme desejado, aqui )

insira a descrição da imagem aqui

COM a -ldemure.abandeira do vinculador ... (Resultados compreensíveis, suspiro ... "obrigado constructor/ destructor" ... Aplausos da multidão ) insira a descrição da imagem aqui


1

Aqui está outro exemplo concreto: é para uma biblioteca compartilhada. A principal função da biblioteca compartilhada é se comunicar com um leitor de cartão inteligente. Mas também pode receber 'informações de configuração' em tempo de execução pelo udp. O udp é tratado por um thread que DEVE ser iniciado no momento do init.

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

  }

A biblioteca foi escrita em c.


11
Uma escolha estranha se a biblioteca for escrita em C ++, pois os construtores de variáveis ​​globais comuns são a maneira idiomática de executar o código principal no C ++.
Nicholas Wilson

@NicholasWilson A biblioteca foi de fato escrita em c. Não sei como eu digitei c ++ em vez de c.
drlolly
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.