__attribute __((constructor))到底如何工作?


347

似乎很清楚,它应该进行设置。

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

Answers:


273
  1. 它在加载共享库时运行,通常在程序启动期间运行。
  2. 这就是所有GCC属性的方式;大概是为了将它们与函数调用区分开来。
  3. GCC特定的语法。
  4. 是的,这适用于C和C ++。
  5. 不,该功能不必是静态的。
  6. 析构函数在共享库卸载时运行,通常在程序退出时运行。

因此,构造函数和析构函数的工作方式是,共享库文件包含特殊部分(ELF上的.ctors和.dtors),这些部分分别包含对标记有构造函数和析构函数属性的函数的引用。在加载/卸载库时,动态加载程序(ld.so或诸如此类)检查是否存在此类节,如果存在,则调用其中引用的函数。

想到这一点,普通的静态链接器中可能存在一些类似的魔术,因此无论用户选择静态链接还是动态链接,都可以在启动/关闭时运行相同的代码。


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 2012年

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代码,in .fini之后运行代码.dtors。如果顺序是相关的,那至少是一种区分init / exit函数的简单但简便的方法。

一个主要的缺点是,每个可加载模块不能轻易拥有_init一个以上的_fini功能,并且可能需要将代码片段化得更多.so。另一个是使用上述链接器方法时,它会替换原始的_init和_fini默认函数(由提供crti.o)。这是通常发生各种初始化的地方(在Linux上,这是初始化全局变量分配的地方)。解决方法在这里描述

请注意,在上面的链接中,_init()不需要层叠到原始文件,因为它仍然存在。但是call,内联汇编中的in是x86助记符,对于许多其他体系结构(例如ARM),从汇编中调用函数看起来会完全不同。即代码不透明。

.init/ .fini.ctors/ .detors机制相似,但不完全相同。.init/中的代码按.fini“原样”运行。也就是说,您可以在.init/中拥有多个功能.fini,但是AFAIK在语法上很难在不破坏许多小.so文件中代码的情况下,以纯C语言完全透明地将其放置在其中。

.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循环的附加功能。但是有了它,就可以更好地控制执行顺序,添加参数并返回代码处理eta(例如,在C ++项目中,如果需要在全局构造函数之前或之后运行的东西很有用)。

我希望__attribute__((constructor))/((destructor))在可能的情况下,即使感觉像作弊,这也是一种简单而优雅的解决方案。对于像我这样的裸机编码员,这并不总是一种选择。

链接器和装载器一书中的一些很好的参考。


加载程序如何调用这些函数?这些函数可以在进程地址空间中使用全局函数和其他函数,但是loader是具有自己地址空间的进程,不是吗?
user2162550

@ user2162550否,ld-linux.so.2(通常的“解释器”,即在所有动态链接的可执行文件上运行的动态库的加载程序)在可执行文件本身的地址空间中运行。通常,动态库加载器本身是特定于用户空间的,在试图访问库资源的线程上下文中运行。
Paul Stelian

当我从具有__attribute__((constructor))/((destructor))析构函数的代码中调用execv()时不会运行。我尝试了一些操作,例如,将.dtor项添加到上面所示的位置。但是没有成功。通过使用numactl运行代码,很容易复制该问题。例如,假设test_code包含析构函数(向构造函数和析构函数添加printf以调试问题)。然后运行LD_PRELOAD=./test_code numactl -N 0 sleep 1。您将看到构造函数被调用两次,而析构函数仅被调用一次。
B阿巴里

39

该页面提供了关于constructorand destructor属性实现以及ELF中允许它们工作的部分的很好的理解。在消化了这里提供的信息之后,我编辑了一些附加信息,并且(借用了上面的Michael Ambrus的本节示例)创建了一个示例来说明概念并帮助我学习。这些结果与示例源一起在下面提供。

如该线程中所述,constructordestructor属性在目标文件的.ctorsand .dtors部分中创建条目。您可以通过以下三种方式之一将对函数的引用放置在任一部分中。(1)使用任一section属性;(2)constructordestructor属性,或(3)具有内联汇编调用(如Ambrus答案中的链接所引用)。

constructordestructor属性的使用允许您另外为构造函数/析构函数分配优先级,以控制其在main()调用之前或返回之后的执行顺序。给定的优先级值越小,执行优先级越高(在main()之前,优先级越高,优先级越高;在main()之后,优先级越高)。您提供的优先级值必须大于,100因为编译器为实现保留了0-100之间的优先级值。一个constructordestructor优先执行规定之前constructordestructor不优先指定。

使用'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_priority7.7 C ++特定的变量,函数和类型属性相同。然后我用99warning: constructor priorities from 0 to 100 are reserved for the implementation [enabled by default] void construct0 () __attribute__ ((constructor (99)));
David C. Rankin

1
啊。我用clang尝试了<100的优先级,它似乎可以工作,但是我的简单测试用例(单个编译单元)太简单了
贾斯汀

1
静态全局变量(静态ctor)的优先级是什么?
2016年

2
静态全局变量的效果和可见取决于程序的结构(例如,单个文件,多个文件(转换单元))以及在其中声明全局变量的方式请参见:静态(关键字),特别是静态全局变量描述。
大卫·兰金

7

这是一个“具体”(并且可能有用)的示例,说明如何,为什么以及何时使用这些方便但难看的结构...

Xcode使用“全局”“用户默认值”来决定哪个XCTestObserver类将其注入陷入困境的控制台中。

在此示例中...当我隐式加载此psuedo-library时,我们libdemure.a通过测试目标ala中的标记将其称为...。

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

如果没有连接标志...(时装警察群库比蒂诺苛刻的报应,但苹果的默认笼罩下,如需要的话,在这里

在此处输入图片说明

使用-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编写的。


1
如果库是用C ++编写的,这是一个奇怪的选择,因为普通的全局变量构造函数是在C ++中运行代码优先的惯用方式。
尼古拉斯·威尔逊

@NicholasWilson该库实际上是用c编写的。不知道如何键入c ++而不是c。
drlolly
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.