执行初始化和终结函数

28

我刚刚阅读了关于 ELF 文件中 init 和 fini 段落的内容,并尝试了一下:

#include <stdio.h>
int main(){
  puts("main");
  return 0;
}

void init(){
  puts("init");
}
void fini(){
  puts("fini");
}

如果我执行 gcc -Wl,-init,init -Wl,-fini,fini foo.c 并运行结果,"init" 部分不会被打印出来:

$ ./a.out
main
fini

初始化部分没有运行,还是由于某种原因无法打印?

有没有关于init/fini的任何“官方”文档?

man ld说:

 -init=name
     When creating an ELF executable or shared object, call
     NAME when the executable or shared object is loaded, by
     setting DT_INIT to the address of the function.  By
     default, the linker uses "_init" as the function to call.

这是否意味着,将初始化函数命名为_init就足够了?(如果我这样做,gcc会抱怨有多个定义。)


这很奇怪...关于你的第一个问题 - 我的GDB显示init函数根本没有运行。 - MByD
2
很可能在标准库初始化足够使puts工作之前,单元部分就已经运行了。为什么不尝试一些没有依赖关系的东西,比如设置一个全局变量,看看unit函数是否真的被执行了。 - nobody
你所做的方式适用于共享库,而不适用于已经具有默认_init和默认_fini的程序。 - Hibou57
2个回答

21

不要这样做;让你的编译器和链接器根据需要填充部分。

相反,用适当的函数属性标记你的函数,以便编译器和链接器将它们放到正确的部分中。

例如,

static void before_main(void) __attribute__((constructor));
static void after_main(void) __attribute__((destructor));

static void before_main(void)
{
    /* This is run before main() */
}

static void after_main(void)
{
    /* This is run after main() returns (or exit() is called) */
}

您还可以为函数指定优先级(例如:__attribute__((constructor (300)))),取值范围为101到65535之间的整数,其中优先级较低的函数将先执行。

请注意,我在示例中将函数标记为static。也就是说,在文件作用域之外不会看到这些函数。这些函数不需要导出符号即可自动调用。


为了测试,建议将以下内容保存在单独的文件中,例如tructor.c

#include <unistd.h>
#include <string.h>
#include <errno.h>

static int outfd = -1;

static void wrout(const char *const string)
{
    if (string && *string && outfd != -1) {
        const char       *p = string;
        const char *const q = string + strlen(string);

        while (p < q) {
            ssize_t n = write(outfd, p, (size_t)(q - p));
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1 || errno != EINTR)
                break;
        }
    }
}

void before_main(void) __attribute__((constructor (101)));
void before_main(void)
{
    int saved_errno = errno;

    /* This is run before main() */
    outfd = dup(STDERR_FILENO);
    wrout("Before main()\n");

    errno = saved_errno;
}

static void after_main(void) __attribute__((destructor (65535)));
static void after_main(void)
{
    int saved_errno = errno;

    /* This is run after main() returns (or exit() is called) */
    wrout("After main()\n");

    errno = saved_errno;
}

因此,您可以将其编译并链接为任何程序或库的一部分。要将其编译为共享库,请使用例如

gcc -Wall -Wextra -fPIC -shared tructor.c -Wl,-soname,libtructor.so -o libtructor.so

你可以使用它将其插入到任何动态链接的命令或二进制文件中

LD_PRELOAD=./libtructor.so some-command-or-binary

这些函数保持 errno 不变,尽管在实践中这不重要,并使用低级别的 write() 系统调用将消息输出到标准错误。初始标准错误被复制到一个新描述符,因为在许多情况下,在最后一个全局析构函数——也就是我们的析构函数运行之前——标准错误本身会被关闭。

(一些偏执的二进制文件,通常是安全敏感的,会关闭它们不知道的所有描述符,所以在某些情况下可能看不到 After main() 消息。)


这些函数属性到底是如何工作的?编译器是否会为init/fini部分生成单独的代码?但据我所知,该代码是从不同的文件(crti.o)链接而来的。 - michas
@michas 从根本上讲,这些属性在C语言中暴露了与允许在初始化时构造C++全局对象完全相同的机制。无论是C++构造函数还是带有__attribute__((constructor))注释的C函数都只是在“main”之前调用的代码,以便(在前一种情况下)对象在“main”开始时准备好使用。 - Iwillnotexist Idonotexist
@michas:是的,crti.o 提供了 GNU C 库在 ELF _init_fini 部分所需的代码。这取决于使用的特定 ABI(x86、x86-64、ARM 变体等),标记的函数如何被调用。例如,在 x86-64 上,我相信函数地址列在 .init_array.fini_array 部分中(分别是 __frame_dummy_init_array_entry__do_global_dtors_aux_fini_array_entry 符号)。 - Nominal Animal
@NominalAnimal - 我同意应该使用构造函数/析构函数属性,但是你能解释一下为什么-init没有起作用吗?(只是为了满足我的好奇心... :) ) - MByD
@MByD:正如4566976所写,ld完全忽略了-init选项。我刚刚用GCC 4.8.4和binutils 2.24进行了验证。 - Nominal Animal
1
使用 readelf -d 二进制文件 来列出二进制文件中的动态符号。如果链接器选项 -init 产生了任何效果,您会发现上面的列表中的 INIT 符号的地址与 C 库 _init 函数不同(指向命名符号)。在上述版本的 x86-64 上并没有这种情况。ld 选项 -fini 改变了动态符号 FINI 的地址。这意味着从 crti.o 中的 _fini() 被你自己的函数替换掉了。(在 x86-64 上这是可以的,因为 _fini() 在该架构上实际上并没有做什么事情。) - Nominal Animal

8
这并不是ld的错误,而是主可执行文件的glibc启动代码中的错误。对于共享对象,将调用由-init选项设置的函数。 此处是添加-init-fini选项的ld提交。
程序的_init函数没有被动态链接器从文件glibc-2.21/elf/dl-init.c:58中的DT_INIT条目调用,但是从主可执行文件的文件glibc-2.21/csu/elf-init.c:83中的__libc_csu_init调用。
也就是说,程序的DT_INIT中的函数指针被启动忽略了。
如果使用-static编译,则fini也不会被调用。
绝对不应该使用DT_INITDT_FINI,因为它们是旧式的,参见第255行
以下是有效的:
#include <stdio.h>

static void preinit(int argc, char **argv, char **envp) {
    puts(__FUNCTION__);
}

static void init(int argc, char **argv, char **envp) {
    puts(__FUNCTION__);
}

static void fini(void) {
    puts(__FUNCTION__);
}


__attribute__((section(".preinit_array"), used)) static typeof(preinit) *preinit_p = preinit;
__attribute__((section(".init_array"), used)) static typeof(init) *init_p = init;
__attribute__((section(".fini_array"), used)) static typeof(fini) *fini_p = fini;

int main(void) {
    puts(__FUNCTION__);
    return 0;
}

$ gcc -Wall a.c
$ ./a.out
preinit
init
main
fini
$ 

在这种情况下,可能是手册页或ld中的错误。还是有一个好的理由在这种情况下忽略它吗? - michas
@4566976 - 有任何文档资料吗? - MByD
ld 不会抱怨未知的 fini 名称,但 fini 仍然可以工作。 - michas
初始化部分已经运行!您可以通过添加一些汇编代码进行验证。 - michas
@michas 是的,但是 elf 可执行文件中 DT_INIT 中的函数指针被动态链接器和 clib 初始化所忽略。 - 4566976
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接