查看“Calling Global Constructors”的源代码
←
Calling Global Constructors
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
本教程讨论如何正确调用全局构造函数,例如全局C++对象上的构造函数。 这些应该在你的main函数之前运行,这就是为什么程序入口点通常是一个名为 _start的函数。 此函数负责解析命令行参数,初始化标准库(内存分配、信号等),运行全局构造函数并最终exit(main(argc, argv))。 如果你更改编译器,自制操作系统上的情况可能会有所不同,但是如果你使用的是GNU编译器套件(GCC),遵循System V ABI可能比较好。 在大多数平台上,全局构造函数/析构函数存储在函数指针的有序数组中,调用这些就像遍历数组再运行每个元素一样简单。 但是,编译器并不总是允许访问此列表,一些编译器会认为这算是内部实现细节。 在这种情况下,你将不得不与编译器合作 - 与编译器对着干只会造成麻烦。 == GNU Compiler Collection - System V ABI == System V ABI(用于 <tt>i686-elf-gcc</tt>, <tt>x86_64-elf-gcc</tt>和其它ELF平台)指定使用五个不同的目标文件,它们一起处理程序初始化。 这些传统上称为 <tt>crt0.o</tt>,<tt>crti.o</tt>,<tt>crtbegin.o</tt>,<tt>crtend.o</tt> 和 <tt>crtn.o</tt>。 这些目标文件一起实现两个特殊功能:<tt>_init</tt>运行全局构造函数和其它初始化任务, 以及运行全局析构函数和其他终止任务的 <tt>_fini</tt>。 此方案使编译器可以很好地控制程序初始化,使你的工作变得容易,但你必须与编译器合作,否则会发生不好的事情。 你的交叉编译器将为你提供 <tt>crtbegin.o</tt> 和 <tt>crtend.o</tt>。 这些文件包含编译器希望对你隐藏但对你有用的内部文件。 要访问此信息,你需要提供你自己的<tt>crti.o</tt>和<tt>crtn.o</tt>实现。 幸运的是,这很容易,并且在本教程中进行了详细描述。 第五个文件<tt>crt0.o</tt>包含程序入口点(通常为<tt>_start</tt>),并调用特殊的<tt>_init</tt>函数,该函数运行运行<tt>crti的“程序初始化任务”(由<tt>crti.o</tt>, <tt>crtbegin.o</tt>组成)。 crtend.o和crtn.o组合在一起, 你的exit函数通常会调用这些目标生成的函数。 但是,<tt>crt0</tt>.o超出了本文的讨论范围。 (请注意,包含 <tt>_start</tt> 的目标文件在内核中充当 <tt>crt0.o</tt>。) 为了理解这种明显的复杂性,考虑一个由<tt>foo.o</tt>和<tt>bar.o</tt>组成的程序。由以下代码链接: <source lang="bash">i686-elf-gcc foo.o bar.o -o program</source> 编译器将重写命令行并将其传递给链接器,如下所示: <source lang="bash">i686-elf-ld crt0.o crti.o crtbegin.o foo.o bar.o crtend.o crtn.o</source> 这里的意图是,这些文件在链接过程中一起形成 <tt>_init</tt> 和 <tt>_fini</tt> 函数。 这是通过将<tt>_init</tt>函数存储在<tt>.init</tt>节(section), 以及 <tt>_fini</tt>函数在<tt>.fini</tt>节中来实现的。 然后,每个文件为这些部分贡献一点,链接器将命令行中指定的代码中的片段粘在一起。 <tt>crti.o</tt>提供函数头, <tt>crtbegin.o</tt> 和 <tt>crtend.o</tt> 提供主体, 和<tt>crtn.o</tt>提供脚(返回语句)。 重要的是要理解链接顺序很重要,如果目标没有完全按照这个顺序链接,可能会发生奇怪的事情。 === 使用来自C的全局构造函数 === 作为一个特殊的扩展,GCC允许C程序作为全局构造函数运行函数。 有关详细信息,请参阅编译器文档。 这通常用作: <source lang="c"> __attribute__ ((constructor)) void foo(void) { printf("foo is running and printf is available at this point\n"); } int main(int argc, char* argv[]) { printf("%s: main is running with argc=%i\n", argv[0], argc); } </source> === 在内核中使用crti.o, crtbegin.o, crtend.o和crtn.o === 在内核中,你没有用户空间C库可以使用。 你可能正在使用特殊的内核 “C库”,或者根本没有。 编译器总是提供<tt>crtbegin.o</tt>和<tt>crtend.o</tt>, 但通常C库提供crti.o和crtn.o, 但是在这种情况下不是这样。 内核应该提供自己的<tt>crti.o</tt>和<tt>crtn.o</tt>实现 (即使它在其它方面与用户空间libc版本相同)。 内核用 <tt>-nostdlib</tt> 选项链接 (这与使用<tt>-nodefaultlibs</tt>和<tt>-nostartfiles</tt>选项相同) 这将禁用通常自动添加到链接命令行的“start files”<tt>crt*.o</tt>。 通过使用<tt>-nostartfiles</tt>选项,我们向编译器保证,我们自己负责调用<tt>crtbegin.o</tt> 和 <tt>crtend.o</tt>文件中的“程序初始化任务”。 这意味着我们需要手动添加<tt>crti.o</tt>,<tt>crtbegin.o</tt>,<tt>crtend.o</tt>,和<tt>crtn.o</tt>到命令行。 由于我们自己提供了<tt>crti.o</tt>和<tt>crtn.o</tt>,因此将其添加到内核命令行有点琐碎。 但是,由于 <tt>crtbegin.o</tt> 和 <tt>crtend.o</tt> 安装在特定于编译器的目录中,因此我们需要找出路径。 幸运的是,gcc提供了一个选项来实现这一点。 如果<tt>i686-elf-gcc</tt>是你的交叉编译器,<tt>$CFLAGS</tt>是你通常会提供给编译器的标志,则 <source lang="bash">i686-elf-gcc $CFLAGS -print-file-name=crtbegin.o</source> 将使编译器将正确的 <tt>crtbegin.o</tt> 文件 (与 $CFLAGS选项兼容) 的路径打印到标准输出。 这同样适用于<tt>crtend.o</tt>。 如果你使用的是GNU Make,则可以在Makefile中轻松完成此操作,假设<tt>$(CC)</tt>是你的交叉编译器,而<tt>$(CFLAGS)</tt>是你通常传递给它的标志: <source lang="make"> CRTBEGIN_OBJ:=$(shell $(CC) $(CFLAGS) -print-file-name=crtbegin.o) CRTEND_OBJ:=$(shell $(CC) $(CFLAGS) -print-file-name=crtend.o) </source> 然后,你可以这样使用它们 (适应你的真实构建系统): <source lang="make"> OBJS:=foo.o bar.o CRTI_OBJ=crti.o CRTBEGIN_OBJ:=$(shell $(CC) $(CFLAGS) -print-file-name=crtbegin.o) CRTEND_OBJ:=$(shell $(CC) $(CFLAGS) -print-file-name=crtend.o) CRTN_OBJ=crtn.o OBJ_LINK_LIST:=$(CRTI_OBJ) $(CRTBEGIN_OBJ) $(OBJS) $(CRTEND_OBJ) $(CRTN_OBJ) INTERNAL_OBJS:=$(CRTI_OBJ) $(OBJS) $(CRTN_OBJ) myos.kernel: $(OBJ_LINK_LIST) $(CC) -o myos.kernel $(OBJ_LINK_LIST) -nostdlib -lgcc clean: rm -f myos.kernel $(INTERNAL_OBJS) </source> 重要的是要记住,这些目标文件必须按照这个确切的顺序链接,否则你会遇到奇怪的错误。 然后,你的内核将有一个<tt>_init</tt>和一个<tt>_fini</tt>函数链接在一起,可以从<tt>boot.o</tt>调用它们 (boot.o是你的内核入口点目标文件名称),然后将控制权传递给 <tt>kernel_main</tt> (kernel_main是你的内核主例程)。 请注意,此时内核可能根本没有初始化,你只能从全局构造函数中执行一些琐碎的操作。 此外,<tt>_fini</tt>可能永远不会被调用,因为你的操作系统将保持运行状态,并且当需要关机时,对于马上会处理器重置,做什么工作也没有价值了。 可能值得设置一个 <tt>kernel_early_main</tt> 函数来初始化堆,日志和其它核心内核功能。 然后你的<tt>boot.o</tt>可以调用<tt>kernel_early_main</tt>,然后调用<tt>_init</tt>,最后将控制权传递给真正的<tt>kernel_main</tt>。 这类似于在用户空间中的工作方式,其中 <tt>crt0.o</tt> 调用<tt>_initialize_c_library</tt> (进行C库初始化), 然后<tt>_init</tt>,最后<tt>exit(main(argc, argv))</tt>。 ===在用户空间中使用crti.o,crtbegin.o,crtend.o和crtn.o=== {{Main|Creating a C Library}} 在用户空间中使用这些目标文件非常容易,因为交叉编译器会自动以正确的顺序将它们链接到最终程序中。 编译器将一如既往地提供<tt>crtbegin.o</tt>和<tt>crtend.o</tt>。 然后,你的C库将提供<tt>crt0.o</tt>(程序入口点文件)、<tt>crti.o</tt>和<tt>crtn.o</tt>。 如果你有[[OS Specific Toolchain|操作系统特定工具链]],你可以更改程序入口点的名称 (通常是_start),编译器搜索路径下的<tt>crt{0,i,n}.o</tt> 文件, 通过修改<tt>STARTFILE_SPEC</tt>和<tt>ENDFILE_SPEC</tt>,了解使用了哪些文件(可能带有其它名称)和顺序。 当你开始创建一个用户空间时,创建一个特定于操作系统的工具链可能是值得的,因为它允许你很好地控制所有这些工作的具体方式。 === x86 (32-bit) === 在x86下实现这一点非常简单。 只需在<tt>crti.o</tt>中定义两个函数的头即可, 并定义在<tt>crtn.o</tt>中的脚,让后在你的C库或内核中使用这些目标。 然后,你可以简单地调用 <tt>_init</tt> 来执行初始化任务,并调用 <tt>_fini</tt> 来执行终止任务 (通常从<tt>crt0.o</tt>或<tt>my-kernel-boot-object.o</tt>执行)。 <pre> /* x86 crti.s */ .section .init .global _init .type _init, @function _init: push %ebp movl %esp, %ebp /* GCC会很好地将crtegin.o的.init节的内容放在这里。*/ .section .fini .global _fini .type _fini, @function _fini: push %ebp movl %esp, %ebp /* GCC会很好地将crtbegin.o的.fini节的内容放在这里。*/ </pre> <pre> /* x86 crtn.s */ .section .init /* GCC会很好地将crtend.o的.init节的内容放在这里。*/ popl %ebp ret .section .fini /* GCC会很好地将crtend.o的.fini节的内容放在这里。*/ popl %ebp ret </pre> === x86_64 (64-bit) === x86_64上的系统ABI类似于32位,我们还需要提供函数头和函数脚,编译器将插入其余的 <tt>_init</tt> 以及通过<tt>crtbegin.o</tt>和<tt>crtend.o</tt>提供<tt>_fini</tt>函数 <pre> /* x86_64 crti.s */ .section .init .global _init .type _init, @function _init: push %rbp movq %rsp, %rbp /*GCC会很好地将crtegin.o的.init节的内容放在这里。*/ .section .fini .global _fini .type _fini, @function _fini: push %rbp movq %rsp, %rbp /*GCC会很好地将crtbegin.o的.fini节内容放在这里。*/ </pre> <pre> /* x86_64 crtn.s */ .section .init /*GCC会很好地将crtend.o的.init节内容放在这里。*/ popq %rbp ret .section .fini /*GCC会很好地将crtend.o的.fini节内容放在这里。*/ popq %rbp ret </pre> === ARM (BPABI) === 在这种情况下,情况略有不同。 ABI系统要求使用名为<tt>.init_array</tt>和<tt>.fini_array</tt>的特殊部分, 而不是常见的<tt>.init</tt>和<tt>.fini</tt>节。 这意味着交叉编译器提供的 <tt>crtbegin.o</tt> 和 <tt>crtend.o</tt> 不会在 <tt>.init</tt> 和 <tt>.fini</tt> 部分中插入指令。 结果是,如果你遵循Intel/AMD系统的方法,则你的<tt>_init</tt>和<tt>_fini</tt>函数将不会起任何作用。 你的交叉编译器实际上可能带有默认的 <tt>crti.o</tt>和<tt>crtn.o</tt>目标,但是它们也会受到这个ABI决定的影响,它们的<tt>_init</tt>和<tt>_fini</tt>函数也不会执行任何操作。 解决方案是提供自己的<tt>crti.o</tt>目标文件,这个目标文件在<tt>.init_array</tt>和<tt>.fini_array</tt>节开头插入一个符号, 如同你自己的<tt>crtn.o</tt>,它在节的末尾插入一个符号。 在这种情况下,实际上可以在C中编写 <tt>crti.o</tt> 和 <tt>crtn.o</tt>,因为我们没有编写不完整的函数。 这些文件应该像内核的其它部分一样编译,像普通<tt>crti.o</tt>和<tt>crtn.o</tt>一样使用。 <source lang="c"> /* crti.c for ARM - BPABI - use -std=c99 */ typedef void (*func_ptr)(void); extern func_ptr _init_array_start[0], _init_array_end[0]; extern func_ptr _fini_array_start[0], _fini_array_end[0]; void _init(void) { for ( func_ptr* func = _init_array_start; func != _init_array_end; func++ ) (*func)(); } void _fini(void) { for ( func_ptr* func = _fini_array_start; func != _fini_array_end; func++ ) (*func)(); } func_ptr _init_array_start[0] __attribute__ ((used, section(".init_array"), aligned(sizeof(func_ptr)))) = { }; func_ptr _fini_array_start[0] __attribute__ ((used, section(".fini_array"), aligned(sizeof(func_ptr)))) = { }; </source> <source lang="c"> /* crtn.c for ARM - BPABI - use -std=c99 */ typedef void (*func_ptr)(void); func_ptr _init_array_end[0] __attribute__ ((used, section(".init_array"), aligned(sizeof(func_ptr)))) = { }; func_ptr _fini_array_end[0] __attribute__ ((used, section(".fini_array"), aligned(sizeof(func_ptr)))) = { }; </source> 此外,如果你使用构造函数/析构函数优先级,编译器会将这些优先级附加到节名。 链接器脚本会试图对这些脚本进行排序,因此你必须将以下内容添加到链接器脚本中。 注意,我们必须特别对待<tt>crti.o</tt> and <tt>crtn.o</tt>目标,因为我们需要将符号按正确的顺序排列。 或者,你也可以自己从链接器脚本发出<tt>_init_array_start</tt>,<tt>_init_array_end</tt>,<tt>_ fini_array_start</tt>,<tt>_ fini_array_end</tt>符号。 <pre> /* 包括已排序的初始化函数列表*/ .init_array : { crti.o(.init_array) KEEP (*(SORT(EXCLUDE_FILE(crti.o crtn.o) .init_array.*))) KEEP (*(EXCLUDE_FILE(crti.o crtn.o) .init_array)) crtn.o(.init_array) } /* 包含已排序的终止函数列表。*/ .fini_array : { crti.o(.fini_array) KEEP (*(SORT(EXCLUDE_FILE(crti.o crtn.o) .fini_array.*))) KEEP (*(EXCLUDE_FILE(crti.o crtn.o) .fini_array)) crtn.o(.fini_array) } </pre> === CTOR/DTOR === 执行全局构造函数/析构函数的另一种方法是手动执行.ctors / .dtors 符号 (假设你有自己的ELF加载程序,请参见[[ELF_Tutorial|ELF_教程]])。 将每个ELF文件加载到内存中,并且所有符号都已解析和重新定位后,可以使用.ctors/.dtor手动执行全局构造函数/析构函数 (显然,这同样适用于.init_array和.fini_array)。 要执行此操作,必须首先找到.ctors / .dtors 节头: <source lang="c"> for (i = 0; i < ef->ehdr->e_shnum; i++) { char name[250]; struct elf_shdr *shdr; ret = elf_section_header(ef, i, &shdr); if (ret != ELF_SUCCESS) return ret; ret = elf_section_name_string(ef, shdr, &name); if (ret != BFELF_SUCCESS) return ret; if (strcmp(name, ".ctors") == 0) { ef->ctors = shdr; continue; } if (strcmp(name, ".dtors") == 0) { ef->dtors = shdr; continue; } } </source> 现在你有了.ctors/.dtors节头,你可以使用以下内容解析每个构造函数。 请注意.ctors / .dtors是一个指针表 (ELF32为32bit,ELF64为64bit)。 每个指针都是必须执行的函数。 <source lang="c"> typedef void(*ctor_func)(void); for(i = 0; i < ef->ctors->sh_size / sizeof(void *); i++) { ctor_func func; elf64_addr sym = 0; sym = ((elf64_addr *)(ef->file + ef->ctors->sh_offset))[i]; func = ef->exec + sym; func(); /* elf->文件是存储你使用的ELF文件的字符 *。 Could be binary or shared library */ /*elf->exec是一个char*,它存储ELF文件已加载到的内存位置,并重新放置*/ } </source> 如果在.ctors / .dtors.中你只有一个条目,不要惊讶。 至少在x86_64上,GCC似乎将单个条目添加到一组名为_GLOBAL__SUB_I_XXX和_GLOBAL_SUB_D_XXX的函数中,这两个函数调用的_Z41__static_initialization_and_destruction_0ii实际上为你调用了每个构造函数。 添加更多全局定义的构造函数/析构函数将导致此函数增长,而不是ctors/.dtors。 === 稳定性问题=== 如果不调用GCC提供的构造函数/析构函数,则GCC将生成代码,该代码将在某些条件下使用x86_64发生段错误(segfault)。 以此为例: <source lang="cpp"> class A { public: A() {} }; A g_a; void foo(void) { A *p_a = &g_a; p_a->anything(); // <---- segfault } </source> GCC似乎在使用构造函数/析构函数初始化例程时不仅仅是调用每个全局定义类的构造函数/析构函数。 执行.ctors/.dtors中定义的函数不仅可以初始化所有的构造函数/析构函数,还可以解决这些类型的段错误(上面只是许多已解决的错误中的一个示例)。 据我所知,当全局定义的对象存在时,GCC也可能创建 “.data.rel.ro”,这是GCC需要处理的另一个重定位表。 它被标记为PROGBITS,而不是REL/RELA,这意味着ELF加载程序不会为你重新定位。 相反,执行.ctors中定义的函数将执行_Z41__static_initialization_and_destruction_0ii,它似乎会为我们执行重定位。 有关更多信息,请参见以下内容: [https://gcc.gnu.org/bugzilla/show_bug.cgi?id=68738] == Clang == '''注意:''' 由于Clang试图在很大程度上与GCC兼容,因此这里列出的信息很可能会变化。 如果有你尝试过,请在此处记录调查结果。 据我所知,clang不需要你在传递给链接器的目标中指定正确的crt{begin,end}.o,前提是你在正确的位置传递crt[in].o,能够输出到许多目标, 并不总是有一个可用的crt{begin,end}.o在手边,它似乎是按需编译的。 调用_init似乎也不是一项要求。 对_init的额外调用不会执行两次,因此手动调用仍然是安全的。 总之,为了将GCC的代码修改为clang,从你的链接程序行中去掉crt{Begin,End}.o就可以了。 == 其它编译器/平台 == 如果你的编译器或系统ABI未在此处列出,你将需要手动查阅相应的文档,如果相关信息,请也帮忙在此处记录。 ==另见== === 外部链接 === * [https://gcc.gnu.org/onlinedocs/gccint/Initialization.html 初始化函数的处理方式] 在GCC的文档中。
本页使用的模板:
模板:Main
(
查看源代码
)
返回至“
Calling Global Constructors
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
变体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息