【问题标题】:Why does the C runtime not call my exit()?为什么 C 运行时不调用我的 exit()?
【发布时间】:2019-11-13 18:16:12
【问题描述】:

C 标准规定...

...从对main 函数的初始调用返回等效于以主函数返回的值作为参数调用exit 函数。

据我所知,这通常由 C 运行时支持代码 (crt0.c) 通过执行此操作来实现 -- 使用来自 main 的返回值调用 exit

glibc:

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

Ulysses Libc:

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。我希望现有的系统运行时会调用my own exit implementation,因此“我的”清理(如刷新和关闭流、处理用atexit 注册的函数等)将在从链接到我的库的main 返回时自动运行。显然不是这样的,我只是不明白为什么不。

【问题讨论】:

  • “标准库”的某些功能通常在库本身和编译器及其支持对象和库文件之间进行拆分。 exitatexit 函数通常由编译器及其支持文件而不是标准库处理。
  • 这仅仅是因为运行时已经链接到原始的exit 实现。很可能它被内联到 _start 函数中。
  • 如何编写自己的exit 版本并同时包含stdlib.h?这是您自己的版本,即 stdlib.c 还是等效版本?
  • @Lundin:注意最后给出的上下文。 The real code 是我自己的exit我自己的&lt;stdlib.h&gt;。问题中的代码仅用于演示目的。
  • 是的,我读到了,但是这个 .c 文件是您的库实现还是一些随机测试文件并不明显。例如,我不希望您的标准库包含 main()。

标签: c runtime crt


【解决方案1】:

如果我理解正确,您是在尝试使用 C 运行时的某些部分(即调用主函数并退出的部分)时尝试实现 C 标准库中的函数。

通常执行此操作的代码部分是_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

当我运行生成的executable 时,我得到以下输出 -

hello world
ok

然后进程就挂起。我可以按 ctrl+c 杀死它。

【讨论】:

  • 是的,一定是它。 OP 可能必须先编写自己的 crt,然后再做其他事情。查看我现在使用的一个用于 ARM 的随机 crt,他们实际上是在内联 asm 中编写了 exit 函数作为 crt 的一部分。
  • @Lundin:这是我希望避免的——因为 crt 不是 ISO C 指定的一部分,我希望我可以在调用时“插入”到现有框架main 的,没有“侵犯”crt0.c 的平台特性...
  • @DevSolar 你不能只插入它,因为它与 C 运行时的 ISO C 部分的其余部分静态链接。毕竟,这些代码希望始终使用随附的 ISO C 函数运行。
  • @DevSolar 我认为只有exit 函数是一个特殊的雪花,包括atexit 在内的所有其他函数都应该与crt 分开。虽然值得检查memcpymemset 是如何实现的——crt 需要这些来初始化.data.bss,所以它们也可能是特殊情况。
  • @Lundin:好吧,换一种说法——如果我自己的exit 没有被调用,我如何将我的atexit 函数堆栈的展开插入现有的CRT,而不必须为我想支持的每个平台实施 CRT? ;-) 显然问题是 crt0.c 静态 与 libc 链接(至少在我正在测试的 Linux 上),因此此时没有“完全”替换系统 libc 并且我必须依靠各自 CRT 背后的人来实际链接到我的库,以获得 CRT 的“全面支持”。混蛋。
猜你喜欢
  • 1970-01-01
  • 2013-03-03
  • 1970-01-01
  • 2017-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多