为什么C运行时不调用我的exit()函数?

3

C语言标准规定...

...从main函数的初始调用返回,等同于使用main函数返回的值作为参数调用exit函数。

据我所见,通常由C运行时支持代码(crt0.c)通过这样做来实现 - 使用main返回的值调用exit函数。

glibc

result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);

尤利西斯库

exit(main(argc, argv, envp));

然而,当我编写自己的 exit 版本时,它没有被调用:

#include <stdio.h>
#include <stdlib.h>

void exit( int rc )
{
    puts( "ok" );
    fflush( stdout );
}

int main()
{
    return 0;
}

这并没有产生我预期的“ok”输出。显然,我在这里漏掉了什么?

背景:我正在实现一个C标准库,只包括ISO部分,即没有crt0.c。我希望现有的系统运行时会调用我的自己的exit实现,这样“我的”清理工作(例如刷新和关闭流、处理使用atexit注册的函数等)将在链接到我的库的main返回时自动运行。显然,情况并非如此,我只是不明白为什么不是这样。


“标准库”的某些功能通常在库本身、编译器及其支持的对象和库文件之间分配。exitatexit函数通常由编译器及其支持文件处理,而不是标准库。 - Some programmer dude
1
这是因为运行时已经与原始的 exit 实现链接在一起。很可能它被内联到 _start 函数中。 - Ajay Brahmakshatriya
你怎样编写自己的 exit 函数,并同时包含 stdlib.h 库?这是你自己编写的版本,即 stdlib.c 或等价物吗? - Lundin
@Lundin:请注意最后给出的上下文。真正的代码是我自己编写的 exit以及我的 <stdlib.h>。问题中的代码仅用于演示目的。 - DevSolar
1
是的,我看过了,但是如果这个.c文件是你的库实现还是一些随机测试文件并不明显。例如,我不会期望你的std lib包含main()函数。 - Lundin
@Lundin:我的库中没有 main 函数,但是我的测试驱动程序(链接我的库)有,这时我意识到我的 exit 函数(在其中调用了 atexit 注册的函数,其中包括一个用于刷新/关闭打开流的函数)没有被调用。 - DevSolar
1个回答

5
如果我理解正确,您正在尝试实现C标准库中的函数,同时尝试使用C运行时的某些部分(即调用main函数和exit函数)。
通常执行此操作的代码部分是_start函数。这通常是带有Linux加载器的ELF二进制文件的入口点。
这个_start函数在您的编译器使用的C运行时中定义,调用exit的链接已经完成(地址修补到调用点)。很可能它被直接嵌入到_start函数中。
为了让_start函数调用您的exit函数,您需要重新定义_start本身。然后,您必须确保不使用C运行时的_start函数。
我建议像这样处理 -
// Assuming your have included files that declare the puts and fflush functions and the stdout macro. 
int main(int argc, char* argv[]); // This is here just so that there is a declaration before the call
void _start(void) {
    char *argv[] = {"executable"}; // There is a way to get the real arguments, but I think you will have to write some assembly for that. 

    int return_value = main(1, argv);
    exit(return_value);
    // Control should NEVER reach here, because you cannot return from the _start function
}

void exit(int ret) {
    puts("ok"); 
    fflush(stdout); // Assuming your runtime defines these functions and stdout somewhere. 
    // To simulate an exit, we will just spin infinitely - 
    while(1);
}

int main(int argc, char* argv[]) {
    puts("hello world\n");
    return 0;
}

现在你可以将文件编译和链接为 -

gcc test.c -o executable -nostdlib
-nostdlib告诉链接器不要链接到标准运行时库,该库具有_start的实现。
现在,您可以执行可执行文件,并按预期调用“退出”,然后将永远保持循环状态。您可以通过按ctrl+c或以其他方式发送SIGKILL来杀死它。
附录
仅为完整起见,我还尝试编写其余函数的实现。
您可以首先在代码顶部添加以下声明和定义。
#define stdout (1)
int puts(char *s);
long unsigned int strlen(const char *s) {
        int len = 0;
        while (s[len])
                len++;
        return len;
}
int fflush(int s) {
}
void exit(int n);

strlen被定义为预期结果,fflush是一个无效操作,因为我们没有为stdio函数实现缓冲。

现在在一个单独的文件puts.s中编写以下汇编代码(假设x64 linux。如果您的平台不同,请更改系统调用编号和参数)。

        .text
        .globl puts
puts:
        movq    %rdi, %r12
        callq    strlen
        movq    $1, %rdi
        movq    %r12, %rsi
        movq    %rax, %rdx
        movq    $1, %rax
        syscall
        retq

这是一个最简单的puts实现,它调用了strlen函数,然后是write系统调用。
现在,您可以将所有内容编译和链接为-
gcc test.c put.s -o executable -nostdlib

当我运行生成的 可执行文件 时,会得到以下输出 -
hello world
ok

然后进程就僵住了。我可以通过按下ctrl+c来终止它。


@Lundin:换句话说,如果我的 exit 没有被调用,我如何将我的 atexit 函数堆栈的解绑插入到现有 CRT 中,而不必为我想要支持的每个平台实现 CRT?;-) 显然问题在于 crt0.c 与 libc(至少在我测试的 Linux 上)是 静态 链接的,因此在那一点上没有“完全”替换系统 libc,我必须依赖于各自 CRT 背后的人实际链接到我的库才能从 CRT 中获得“完全支持”。该死。 - DevSolar
这是ISO C规范的一部分,不仅仅是函数。顺便说一句,@DevSolar我建议你查看osdev.org上有关制作适当交叉编译器的文章,你真的想将自己的crt0配置到其中。 - Antti Haapala -- Слава Україні
@AnttiHaapala:ISO C对_start没有任何规定。我已经实现了大部分libc的功能。我正在寻找一种方法将我的后main清理插入到现有的 CRT中,而不需要重新实现它。你回答中有价值的部分是观察到exit(可能)静态链接到crt0.o,因此答案实际上是“无法从用户空间完成”。或者我需要使用系统的atexit注册我的 exit,以便链接到crt0.o的系统 exit 调用系统 atexit 处理并调用我的 exit... 这很麻烦。 - DevSolar
@AnttiHaapala:提到OSDev的交叉编译器文章真是有趣,因为那篇文章我*当年就写了。 :-D - DevSolar
@DevSolar 如果我没记错的话,早些时候我无法使用它使交叉编译器正确地链接可执行文件到我的 newlib 端口,所以你也可能漏掉了一些东西 ;) - Antti Haapala -- Слава Україні
显示剩余6条评论

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