【问题标题】:Startup code of a statically-linked executable issues so many system calls?静态链接的可执行文件的启动代码发出这么多系统调用?
【发布时间】:2011-11-30 06:06:37
【问题描述】:

我正在尝试静态编译一个最小程序并检查发出的系统调用:

$ cat hello.c
#include <stdio.h>

int main (void) {
  write(1, "Hello world!", 12);
  return 0;
}

$ gcc hello.c -static

$ objdump -f a.out
a.out:     file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x00000000004003c0

$ strace ./a.out
execve("./a.out", ["./a.out"], [/* 39 vars */]) = 0
uname({sys="Linux", node="ubuntu", ...}) = 0
brk(0)                                  = 0xa20000
brk(0xa211a0)                           = 0xa211a0
arch_prctl(ARCH_SET_FS, 0xa20880)       = 0
brk(0xa421a0)                           = 0xa421a0
brk(0xa43000)                           = 0xa43000
write(1, "Hello world!", 12Hello world!)            = 12
exit_group(0)                           = ?

我知道,当非静态链接时,ld 会发出启动代码以将libc.sold.so 映射到进程的地址空间,而ld.so 将继续加载任何其他共享库。

但是在这种情况下,除了execvewriteexit_group之外,为什么会发出这么多系统调用?

为什么是uname(2)?为什么这么多调用brk(2) 来获取和设置程序中断,以及调用arch_prctl(2) 来设置进程状态,而这似乎应该在内核空间中完成,在execve 时间?

【问题讨论】:

  • brk 大致是一个低级的malloc 所以它被启动代码调用并不奇怪。如果你真的很好奇为什么要调用它们,为什么不在调用相关函数时使用调试器中断?
  • 省略 -static 以便它使用共享库,然后使用 ltrace -S ./a.out 查看在它最终调用您的 main() 之前完成的全部工作。当我输入 a.out 时:你的 linux 系统是旧系统?
  • @ott--, a.out 是 GCC 的默认输出文件名(当你不通过 -o 时),即使在 EL​​F 系统上也是如此。
  • @bdolan:是的,我想到了可执行格式。

标签: c linux static-linking libc system-calls


【解决方案1】:

需要uname 来检查内核版本是否太旧。

需要两个brks 来设置线程本地存储。需要另外两个来设置动态加载器路径(可执行文件仍然可能调用dlopen,即使它是静态链接的)。我不知道为什么这些是成对出现的。

在系统上arch_prctl 未被调用,set_thread_area 被调用。这将为当前线程设置 TLS。

这些事情可能是懒惰的(即在第一次使用相应的设施时调用)。但也许在性能方面没有意义(只是猜测)。

顺便说一句,gdb-7.x 可以使用catch syscall 命令停止系统调用。

【讨论】:

  • IIRC,内核版本在“auxv”向量中传递。如果是这样,则 uname 是多余的,纯粹是浪费 cpu 时间。
  • @R 不是 auxv 一个动态精灵吗?
  • @R um,你是对的,但是里面没有内核版本。无论如何都不是立即的。也许它隐藏在 AT_SYSINFO 或类似的某个地方,但并非所有内核都通过它。直接拨打uname比较靠谱。
  • 嗯,你是对的。我想如果您只是在测试“足够新”的版本(例如,对于 NPTL),您也许可以使用特定 AT_* 条目的存在,否则可能真的需要 uname...
【解决方案2】:

无耻插件:当针对 musl libc 构建时,该程序静态链接或动态链接的 strace 是:

execve("./a.out", ["./a.out"], [/* 42 vars */]) = 0
write(1, "Hello world!", 12)            = 12
exit_group(0)                           = ?

如果您使用静态链接,或者使用 uClibc 和静态链接,只要您在禁用语言环境和高级 stdio 内容的情况下构建 uClibc,它应该同样最小化。 (出于某种原因,启用了这些功能的 uClibc 会运行大量启动代码来初始化它们,即使在不使用它们的程序中也是如此......)。然而,据我所知,musl 是唯一一个具有动态链接器的程序,能够避免动态链接程序中繁重的启动系统调用开销。

至于 为什么 使用 glibc 的静态链接会导致所有这些 brk 调用,我真的不知道;你必须阅读源代码。我怀疑它正在为malloc、stdio、语言环境以及可能的主线程的线程结构分配空间。作为 n.m.说,arch_prctl 用于设置线程寄存器指向主线程的线程结构。这可以推迟到第一次访问(musl 这样做),但这样做有点痛苦,并且会轻微损害性能。如果您关心大型程序的运行时间而不是许多小程序的启动时间,那么始终在程序加载时初始化线程寄存器可能是有意义的。请注意,内核无法为您设置它,因为它不知道应该设置的地址。

可以对 ELF 格式进行扩展,以允许主线程结构位于 .data 部分,并带有一个 ELF 标头告诉内核它在哪里,但是 libc 和链接器之间需要杂技,并且内核可能会如此丑陋以至于使这种优化变得不可取......它们还会对线程的用户空间实现施加进一步的限制。

【讨论】:

    猜你喜欢
    • 2011-06-07
    • 1970-01-01
    • 2021-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多