¿Cómo funciona exactamente __attribute __ ((constructor))?

347

Parece bastante claro que se supone que debe configurar las cosas.

  1. ¿Cuándo se ejecuta exactamente?
  2. ¿Por qué hay dos paréntesis?
  3. Es __attribute__una función? Una macro? ¿Sintaxis?
  4. ¿Funciona esto en C? C ++?
  5. ¿La función con la que funciona debe ser estática?
  6. ¿Cuándo __attribute__((destructor))corre?

Ejemplo en Objective-C :

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

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

Respuestas:

273
  1. Se ejecuta cuando se carga una biblioteca compartida, generalmente durante el inicio del programa.
  2. Así son todos los atributos de GCC; presumiblemente para distinguirlos de las llamadas a funciones.
  3. Sintaxis específica de GCC.
  4. Sí, esto funciona en C y C ++.
  5. No, la función no necesita ser estática.
  6. El destructor se ejecuta cuando la biblioteca compartida se descarga, generalmente a la salida del programa.

Entonces, la forma en que funcionan los constructores y destructores es que el archivo de objeto compartido contiene secciones especiales (.ctors y .dtors en ELF) que contienen referencias a las funciones marcadas con los atributos de constructor y destructor, respectivamente. Cuando la biblioteca se carga / descarga, el programa de cargador dinámico (ld.so o somesuch) verifica si existen tales secciones y, de ser así, llama a las funciones a las que se hace referencia en ellas.

Ahora que lo pienso, probablemente haya algo de magia similar en el enlazador estático normal para que el mismo código se ejecute al inicio / apagado, independientemente de si el usuario elige el enlace estático o dinámico.

janneb
fuente
49
Los corchetes dobles hacen que sea fácil "macro" ( #define __attribute__(x)). Si tiene varios atributos, por ejemplo, __attribute__((noreturn, weak))sería difícil "eliminar" si solo hubiera un conjunto de paréntesis.
Chris Jester-Young
77
No se hace con .init/.fini. (Puede tener válidamente múltiples constructores y destructores en una sola unidad de traducción, no importa múltiples en una sola biblioteca, ¿cómo funcionaría?) En cambio, en plataformas que usan formato binario ELF (Linux, etc.), se hace referencia a los constructores y destructores en las secciones .ctorsy .dtorsdel encabezado. Es cierto que en los viejos tiempos, las funciones nombradas inityfini se ejecutarían en la carga y descarga de la biblioteca dinámica si existieran, pero eso está en desuso ahora, reemplazado por este mejor mecanismo.
Ephemient
77
@jcayzac No, porque las macros variadic es una extensión gcc, y la razón principal para la macro __attribute__ es si no está usando gcc, ya que también es una extensión de gcc.
Chris Jester-Young
99
@ Las macros variadas de ChrisJester-Young son una característica estándar de C99, no una extensión de GNU.
jcayzac
44
"su uso del tiempo presente (" hacer "en lugar de" hecho "- los dobles parentes aún hacen que sean fáciles de descifrar. Ladró el árbol pedante equivocado.
Jim Balter
64

.init/ .finino está en desuso. Todavía es parte del estándar ELF y me atrevería a decir que será para siempre. El código en .init/ .finies ejecutado por el cargador / runtime-linker cuando el código se carga / descarga. Es decir, en cada código ELF de carga (por ejemplo, una biblioteca compartida) .initse ejecutará. Todavía es posible usar ese mecanismo para lograr casi lo mismo que con __attribute__((constructor))/((destructor)). Es de la vieja escuela pero tiene algunos beneficios.

.ctors/ .dtorsmecanismo, por ejemplo, requiere soporte de system-rtl / loader / linker-script. Esto está lejos de ser seguro para estar disponible en todos los sistemas, por ejemplo, sistemas profundamente integrados donde el código se ejecuta en metal desnudo. Es decir, incluso si __attribute__((constructor))/((destructor))es compatible con GCC, no es seguro que se ejecutará, ya que depende del vinculador organizarlo y del cargador (o, en algunos casos, del código de arranque) ejecutarlo. Para usar .init/ en su .finilugar, la forma más fácil es usar banderas de enlace: -init & -fini (es decir, desde la línea de comandos de GCC, la sintaxis sería -Wl -init my_init -fini my_fini).

En el sistema que admite ambos métodos, un beneficio posible es que el código .initse ejecuta antes .ctorsy el código .finidespués .dtors. Si el orden es relevante, esa es al menos una forma cruda pero fácil de distinguir entre las funciones init / exit.

Una desventaja importante es que no puedes tener más de uno _inity uno_fini función por cada módulo cargable y probablemente tendría que fragmentar el código más .soque motivado. Otra es que cuando se usa el método de enlace descrito anteriormente, uno reemplaza las _finifunciones _init y predeterminadas originales (proporcionadas por crti.o). Aquí es donde generalmente se produce todo tipo de inicialización (en Linux aquí es donde se inicializa la asignación de variable global). Aquí se describe una forma de evitarlo

Observe en el enlace anterior que _init()no se necesita una conexión en cascada con el original, ya que todavía está en su lugar. El callde la línea de montaje es sin embargo x86-mnemotécnica y llamar a una función del conjunto se vería completamente diferente para muchas otras arquitecturas (como ARM, por ejemplo). Es decir, el código no es transparente.

.initLos mecanismos / .finiy .ctors/ .detorsson similares, pero no del todo. El código en .init/ se .finiejecuta "tal cual". Es decir, puede tener varias funciones en .init/.fini , pero AFAIK es sintácticamente difícil ponerlas allí de forma totalmente transparente en C puro sin romper el código en muchos .soarchivos pequeños .

.ctors/ .dtorsestán organizados de manera diferente que .init/.fini . .ctorsLas .dtorssecciones / son solo tablas con punteros a funciones, y el "llamador" es un bucle proporcionado por el sistema que llama a cada función indirectamente. Es decir, la llamada de bucle puede ser específica de la arquitectura, pero como es parte del sistema (si es que existe), no importa.

El siguiente fragmento agrega nuevos punteros de función a la .ctorsmatriz de funciones, principalmente de la misma manera que lo __attribute__((constructor))hace (el método puede coexistir con __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;

También se pueden agregar los punteros de función a una sección autoinventada completamente diferente. Un script enlazador modificado y una función adicional que imita el cargador .ctors/.dtors tal caso, se necesita bucle. Pero con él se puede lograr un mejor control sobre el orden de ejecución, agregar argumentos y manejo de código de retorno eta (en un proyecto de C ++, por ejemplo, sería útil si necesita algo que se ejecute antes o después de los constructores globales).

Preferiría __attribute__((constructor))/((destructor))siempre que sea posible, es una solución simple y elegante, incluso se siente como hacer trampa. Para codificadores de metal desnudo como yo, esto no siempre es una opción.

Alguna buena referencia en el libro Linkers & loaders .

Michael Ambrus
fuente
¿Cómo puede el cargador llamar a esas funciones? esas funciones pueden usar globales y otras funciones en el espacio de direcciones del proceso, pero el cargador es un proceso con su propio espacio de direcciones, ¿no?
user2162550
@ user2162550 No, ld-linux.so.2 (el "intérprete" habitual, el cargador para bibliotecas dinámicas que se ejecuta en todos los ejecutables vinculados dinámicamente) se ejecuta en el espacio de direcciones del propio ejecutable. En general, el cargador dinámico de la biblioteca en sí es algo específico del espacio de usuario, que se ejecuta en el contexto del hilo que intenta acceder a un recurso de la biblioteca.
Paul Stelian
Cuando llamo a execv () desde el código que tiene __attribute__((constructor))/((destructor))el destructor no se ejecuta. Intenté algunas cosas, como agregar una entrada a .dtor como se muestra arriba. Pero no hay éxito. El problema es fácil de duplicar ejecutando el código con numactl. Por ejemplo, suponga que test_code contiene el destructor (agregue un printf a las funciones de constructor y desctructor para depurar el problema). Entonces corre LD_PRELOAD=./test_code numactl -N 0 sleep 1. Verá que el constructor se llama dos veces pero el destructor solo una vez.
B Abali
39

Esta página proporciona una gran comprensión sobre constructorydestructor implementación de atributos y las secciones dentro de dentro de ELF que les permitan trabajar. Después de digerir la información proporcionada aquí, compilé un poco de información adicional y (tomando prestado el ejemplo de la sección de Michael Ambrus anterior) creé un ejemplo para ilustrar los conceptos y ayudar a mi aprendizaje. Esos resultados se proporcionan a continuación junto con la fuente de ejemplo.

Como se explica en este hilo, los atributos constructory destructorcrean entradas en la sección .ctorsy .dtorsdel archivo de objeto. Puede colocar referencias a funciones en cualquier sección de una de tres maneras. (1) usando el sectionatributo; (2) constructory destructoratributos o (3) con una llamada de ensamblaje en línea (como se hace referencia al enlace en la respuesta de Ambrus).

El uso de constructory los destructoratributos le permiten asignar adicionalmente una prioridad al constructor / destructor para controlar su orden de ejecución antes de que main()se llame o después de que regrese. Cuanto menor sea el valor de prioridad dado, mayor será la prioridad de ejecución (las prioridades más bajas se ejecutan antes de las prioridades más altas antes de main () - y después de las prioridades más altas después de main ()). Los valores de prioridad que proporcione deben ser mayores que100 cuando el compilador reserva valores de prioridad entre 0-100 para la implementación. A constructoro destructorespecificado con prioridad se ejecuta antes de a constructoro destructorespecificado sin prioridad.

Con el atributo 'section' o con el ensamblaje en línea, también puede colocar referencias de funciones en la sección de código .inity .finiELF que se ejecutará antes de cualquier constructor y después de cualquier destructor, respectivamente. Cualquier función llamada por la referencia de función colocada en la .initsección, se ejecutará antes de la referencia de función en sí (como de costumbre).

He tratado de ilustrar cada uno de esos en el siguiente ejemplo:

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

salida:

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

El ejemplo ayudó a cimentar el comportamiento del constructor / destructor, con suerte también será útil para otros.

David C. Rankin
fuente
¿Dónde encontraste que "los valores de prioridad que das deben ser mayores que 100"? Esa información no está presente en la documentación de atributos de la función GCC.
Justin
44
IIRC, había un par de referencias, PATCH: argumento de prioridad de soporte para argumentos de constructor / destructor ( MAX_RESERVED_INIT_PRIORITY), y que eran los mismos que C ++ ( init_priority) 7.7 C ++: atributos específicos de variables, funciones y tipos . Luego probé con 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
Ah Intenté prioridades <100 con clang y parecía estar funcionando, pero mi caso de prueba simple (una sola unidad de compilación) era demasiado simple .
Justin
1
¿Cuál es la prioridad de las variables globales estáticas (ctors estáticos)?
guiones el
2
El efecto y la visibilidad de un global estático dependerá de cómo esté estructurado su programa (por ejemplo, un solo archivo, múltiples archivos ( unidades de traducción )) y en el que se declare el global Vea: Estática (palabra clave) , específicamente la descripción de la variable global estática .
David C. Rankin
7

Aquí hay un ejemplo "concreto" (y posiblemente útil ) de cómo, por qué y cuándo usar estas construcciones prácticas pero antiestéticas ...

Xcode utiliza un "valor predeterminado de usuario" "global" para decidir qué XCTestObserverclase arroja su corazón a la consola asediada .

En este ejemplo ... cuando implícitamente cargo esta psuedo-biblioteca, llamémosla ... libdemure.a, a través de una bandera en mi objetivo de prueba á la ..

OTHER_LDFLAGS = -ldemure

Quiero..

  1. En la carga (es decir, cuando XCTestcarga mi paquete de prueba), anule la XCTestclase "predeterminada" "observador" ... (a través de la constructorfunción) PD: Por lo que puedo decir ... cualquier cosa que se haga aquí podría hacerse con un efecto equivalente dentro de mi + (void) load { ... }método de clase

  2. ejecutar mis pruebas ... en este caso, con menos verbosidad en los registros (implementación a pedido)

  3. Regrese la XCTestObserverclase "global" a su estado original ... para no estropear otras XCTestcarreras que no se han subido al carro (alias libdemure.a. Supongo que esto se hizo históricamente en dealloc... pero no voy a comenzar a jugar con esa vieja bruja.

Entonces...

#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

Sin la bandera de enlace ... (La policía de la moda enjambre Cupertino exigiendo represalias , sin embargo, el defecto de Apple prevalece, como se desea, aquí )

ingrese la descripción de la imagen aquí

CON la -ldemure.abandera del vinculador ... (Resultados comprensibles, jadeo ... "gracias constructor/ destructor" ... Multitud aplaude ) ingrese la descripción de la imagen aquí

Alex Gray
fuente
1

Aquí hay otro ejemplo concreto: es para una biblioteca compartida. La función principal de la biblioteca compartida es comunicarse con un lector de tarjetas inteligentes. Pero también puede recibir 'información de configuración' en tiempo de ejecución a través de udp. El udp es manejado por un hilo que DEBE iniciarse en el momento de inicio.

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

  }

La biblioteca fue escrita en c.

drlolly
fuente
1
Una elección extraña si la biblioteca está escrita en C ++, ya que los constructores de variables globales comunes son la forma idiomática de ejecutar código pre-main en C ++.
Nicholas Wilson
@NicholasWilson La biblioteca de hecho fue escrita en c. No sé cómo escribí c ++ en lugar de c.
drlolly