我正在通过静态编译最小程序并检查已发出的系统调用来进行实验:
$ 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.so
和ld.so
映射到流程的地址空间,ld.so
会继续加载任何其他共享库。
但在这种情况下,除了execve
,write
和exit_group
之外,为什么会发出如此多的系统调用?
为什么要uname(2)
?为什么这么多调用brk(2)
来获取和设置程序中断,以及调用arch_prctl(2)
来设置进程状态,当这看起来像应该在内核空间中完成的那样,在{ {1}}时间?
答案 0 :(得分:10)
uname
来检查内核版本是不是太古老了。
设置线程本地存储需要两个brk
。设置动态加载程序路径需要另外两个(可执行文件仍然可以调用dlopen
,即使它是静态链接的)。我不确定为什么这些成对出现。
在系统arch_prctl
未调用时,set_thread_area
将被调用。这为当前线程设置了TLS。
这些事情可能是懒惰地进行的(即第一次使用相应设施时调用)。但也许在性能方面没有任何意义(只是猜测)。
顺便说一下gdb-7.x
可以使用catch syscall
命令停止系统调用。
答案 1 :(得分:7)
无耻插件:当针对musl libc构建时,该程序静态链接或动态链接的strace是:
execve("./a.out", ["./a.out"], [/* 42 vars */]) = 0
write(1, "Hello world!", 12) = 12
exit_group(0) = ?
如果您使用静态链接,或者使用uClibc和静态链接,只要您使用区域设置和高级stdio内容禁用了uClibc,它应该与dietlibc同样最小。 (出于某种原因,启用了这些功能的uClibc会运行大量启动代码来初始化它们,即使在不使用它们的程序中......)。据我所知,musl是唯一一个拥有动态链接器的人,它能够避免动态链接程序中繁重的启动系统调用开销。
至于为什么静态链接与glibc进行所有brk
次调用,我真的不知道;你必须阅读来源。我怀疑它为malloc
,stdio,locale以及可能是主线程的线程结构的内部数据结构分配空间。作为n.m.说,arch_prctl
用于将线程寄存器设置为指向主线程的线程结构。这个可能被推迟到第一次访问(musl会这样做),但这样做有点痛苦并且会轻微地伤害性能。如果您关心大型程序的运行时间而不是许多小程序的启动时间,那么在程序加载时始终初始化线程寄存器可能是有意义的。请注意,内核无法为您设置它,因为它不知道应该设置的地址。
可能会对ELF格式进行扩展,以允许主线程结构位于.data
部分,ELF头告诉内核它在哪里,但是libc之间需要杂技,链接器和内核可能会如此丑陋,以至于不希望这种优化......它们还会对线程的用户空间实现施加进一步的限制。