wxz*_*wxz 6 c assembly linker compilation
我正在尝试生成最小的 C 程序,以查看通过运行它执行了多少指令。我禁用了库的使用并禁用了 vdso。然而,我的 C 程序(gdb 说是 7 条汇编指令)根据 perf stat 最终执行了 17k 条指令。
这是正常数量的指令只是为了设置程序吗?根据 gdb,来自 ld-linux-x86-64.so.2 的代码被映射到程序地址空间。鉴于我禁用了 vdso 并且不包含任何库,此文件是否需要运行该程序?这可能是 17k 指令的原因吗?
我的 C 程序foo5.c
int main(){
char* str = "Hello World";
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我如何编译:
gcc -nostdlib -nodefaultlibs stubstart.S -o foo5 foo5.c
Run Code Online (Sandbox Code Playgroud)
stubstart.S
.globl _start
_start:call main;
movl $1, %eax;
xorl %ebx, %ebx;
int $0x80
Run Code Online (Sandbox Code Playgroud)
性能统计输出:
Performance counter stats for './foo5':
0.60 msec task-clock:u # 0.015 CPUs utilized
0 context-switches:u # 0.000 K/sec
0 cpu-migrations:u # 0.000 K/sec
11 page-faults:u # 0.018 M/sec
46,646 cycles:u # 0.077 GHz
17,224 instructions:u # 0.37 insn per cycle
5,145 branches:u # 8.513 M/sec
435 branch-misses:u # 8.45% of all branches
Run Code Online (Sandbox Code Playgroud)
gdb程序布局:
`/home/foo5', file type elf64-x86-64.
Entry point: 0x5555555542b1
0x0000555555554238 - 0x0000555555554254 is .interp
0x0000555555554254 - 0x0000555555554278 is .note.gnu.build-id
0x0000555555554278 - 0x0000555555554294 is .gnu.hash
0x0000555555554298 - 0x00005555555542b0 is .dynsym
0x00005555555542b0 - 0x00005555555542b1 is .dynstr
0x00005555555542b1 - 0x00005555555542d5 is .text
0x00005555555542d5 - 0x00005555555542e1 is .rodata
0x00005555555542e4 - 0x00005555555542f8 is .eh_frame_hdr
0x00005555555542f8 - 0x0000555555554330 is .eh_frame
0x0000555555754f20 - 0x0000555555755000 is .dynamic
0x00007ffff7dd51c8 - 0x00007ffff7dd51ec is .note.gnu.build-id in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd51f0 - 0x00007ffff7dd52c4 is .hash in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd52c8 - 0x00007ffff7dd53c0 is .gnu.hash in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd53c0 - 0x00007ffff7dd56f0 is .dynsym in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd56f0 - 0x00007ffff7dd5914 is .dynstr in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5914 - 0x00007ffff7dd5958 is .gnu.version in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5958 - 0x00007ffff7dd59fc is .gnu.version_d in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5a00 - 0x00007ffff7dd5dd8 is .rela.dyn in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5dd8 - 0x00007ffff7dd5e80 is .rela.plt in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5e80 - 0x00007ffff7dd5f00 is .plt in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5f00 - 0x00007ffff7dd5f08 is .plt.got in /lib64/ld-linux-x86-64.so.2
0x00007ffff7dd5f10 - 0x00007ffff7df4b20 is .text in /lib64/ld-linux-x86-64.so.2
0x00007ffff7df4b20 - 0x00007ffff7df9140 is .rodata in /lib64/ld-linux-x86-64.so.2
0x00007ffff7df9140 - 0x00007ffff7df9141 is .stapsdt.base in /lib64/ld-linux-x86-64.so.2
0x00007ffff7df9144 - 0x00007ffff7df97b0 is .eh_frame_hdr in /lib64/ld-linux-x86-64.so.2
0x00007ffff7df97b0 - 0x00007ffff7dfbc24 is .eh_frame in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffc680 - 0x00007ffff7ffce64 is .data.rel.ro in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffce68 - 0x00007ffff7ffcfd8 is .dynamic in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffcfd8 - 0x00007ffff7ffcfe8 is .got in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffd000 - 0x00007ffff7ffd050 is .got.plt in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffd060 - 0x00007ffff7ffdfd8 is .data in /lib64/ld-linux-x86-64.so.2
0x00007ffff7ffdfe0 - 0x00007ffff7ffe170 is .bss in /lib64/ld-linux-x86-64.so.2
Run Code Online (Sandbox Code Playgroud)
更新:
最后,jester 关于创建标准可执行文件而不是 PIE 以通过向 gcc 添加 -no-pie 标志来删除 ld.so 的评论将 perf 指令 stat 减少到 12。然后 old_timer 的 -O2 建议进一步将其减少到 7!谢谢大家。
更新 2:使用 -static 的选定答案还将指令数从 17k 减少到 12。优秀的答案。
此外,评论者链接的这篇文章是相关且有趣的。
TL:DR:-static不是默认值,使用它来制作一个只运行你的_start.
-no-pie -nostdlib 也将生成一个静态可执行文件,因为它是非 PIE 并且没有要链接的动态库。
还有一种情况是-static-pie,内核会将您的可执行文件加载到随机基地址,但不会首先运行 ld.so(我认为),但这不是您使用-static.
为了清楚起见,我们讨论的是动态指令计数(有多少在用户空间中实际执行perf stat -e instructions:u),而不是静态计数(有多少作为可执行文件的一部分位于磁盘/内存中)。静态计数只对循环内的指令计数一次,并且仍然对从未执行的指令计数。
或者至少这就是我要回答的。这使得其他部分中的元数据和不执行的代码变得无关紧要。
根据 gdb,来自 ld-linux-x86-64.so.2 的代码被映射到程序地址空间。鉴于我禁用了 vdso 并且不包含任何库,此文件是否需要运行该程序?
您仍然构建了一个与位置无关的可执行文件 (PIE)。这是一个带有入口点的 ELF 共享对象,因此它仍然是动态链接的。所以 ld.so ELF 解释器在它上面运行。它没有什么可做的,因为您实际上并没有使用任何共享库,但是 17k 用户空间指令听起来是正确的。我在我的 Arch Linux 系统 (glibc 2.31) 上得到了 32606 或 7 条程序指令。
ld.so启动为二进制文件的“解释器”,/bin/sh其启动方式与启动解释以#!/bin/sh. (尽管 Linux 的 ELF 程序加载器仍然会根据可执行文件的程序头将程序段映射到内存中,因此 ld.so 不必通过系统调用手动执行此操作。)
您可以通过在第一条用户空间指令之前运行 undergdb ./foo5和 usingstarti而不是runto stop来看到这一点。你会看到你在ld.so's _start。
Reading symbols from ./foo5...
(No debugging symbols found in ./foo5)
Cannot access memory at address 0x1024 ### note this isn't a real address,
### just an offset relative to the base address / start of the file.
### That's another clue this is a PIE
(gdb) starti
Program stopped.
0x00007ffff7fd3100 in _start () from /lib64/ld-linux-x86-64.so.2
Run Code Online (Sandbox Code Playgroud)
您还可以运行strace ./foo5以查看它所做的系统调用,以表明发生了很多事情:
$ strace ./foo5
execve("./foo5", ["./foo5"], 0x7ffc12394d90 /* 50 vars */) = 0
brk(NULL) = 0x55741b4b7000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffca69312b0) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1d4fc4b000
arch_prctl(ARCH_SET_FS, 0x7f1d4fc4ba80) = 0
mprotect(0x557419622000, 4096, PROT_READ) = 0
strace: [ Process PID=303809 runs in 32 bit mode. ]
exit(0) = ?
Run Code Online (Sandbox Code Playgroud)
(注意“以 32 位模式运行”;它没有,但 strace 检测到您使用的是 32 位int $0x80ABI 而不是syscallld.so 使用的普通ABI。)
-static-nostdlib用于暗示-static,在 GCC 中配置为默认不制作 PIE。但是出于安全原因,现代发行版确实配置了 GCC 来制作 PIE。看到x86-64 Linux 中不再允许使用 32 位绝对地址了吗?
$ file foo5
foo5: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1ac0a9af247fefebde100695805e5b73f06e891c, not stripped
Run Code Online (Sandbox Code Playgroud)
使用-static,OTOH构建后:
$ file foo5
foo5: ELF 64-bit LSB executable ...
$ perf stat --all-user ./foo5
Performance counter stats for './foo5':
0.03 msec task-clock # 0.151 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
1 page-faults # 0.030 M/sec
1,930 cycles # 0.058 GHz
12 instructions # 0.01 insn per cycle
4 branches # 0.121 M/sec
0 branch-misses # 0.00% of all branches
0.000219151 seconds time elapsed
0.000284000 seconds user
0.000000000 seconds sys
Run Code Online (Sandbox Code Playgroud)
(奇怪的是,:u当你使用时,perf 不会为事件打印--all-user。我的系统有/proc/sys/kernel/perf_event_paranoid= 0,所以如果我不使用它,它也会计算内核中执行的指令。这在每次运行之间差异很大,但总共大约 60k对于这个静态可执行文件。)
我只计算了 11 条执行的用户空间指令,但显然我的 i7-6700k 为该事件计算了 12 条。(硬件支持屏蔽用户、内核或任何事件计数器的两者。这就是 perf 使用的。)
GDB 也确认成功:
Reading symbols from ./foo5...
(No debugging symbols found in ./foo5)
Cannot access memory at address 0x401024
(gdb) starti
Starting program: /tmp/foo5
Program stopped.
0x0000000000401000 in _start ()
(gdb)
Run Code Online (Sandbox Code Playgroud)
并且从layout reg显示的反汇编窗口:
? >0x401000 <_start> call 0x40100e <main>
? 0x401005 <_start+5> mov eax,0x1
? 0x40100a <_start+10> xor ebx,ebx
? 0x40100c <_start+12> int 0x80
? 0x40100e <main> push rbp
? 0x40100f <main+1> mov rbp,rsp
? 0x401012 <main+4> lea rax,[rip+0xfe7] # 0x402000
? 0x401019 <main+11> mov QWORD PTR [rbp-0x8],rax
? 0x40101d <main+15> mov eax,0x0
? 0x401022 <main+20> pop rbp
? 0x401023 <main+21> ret
Run Code Online (Sandbox Code Playgroud)
您可以编译以-O2将您优化main为一个xor eax,eax/ ret,或者根本不调用它,因此只需要执行 3 个用户空间指令。
或者要在仍然使用 C 的同时优化您的用户空间指令数,请参阅@mosvy关于_start用 C编写的答案,以及_exit(2)可以内联到其中的内联汇编。)
请注意,您的 _start 无法将 argc 和 argv 传递给 main,尽管它确实在函数调用之前正确对齐了 16 字节的 RSP。(因为 x86-64 SysV ABI 保证进程进入时堆栈对齐)。你可以用 mov load 和 LEA 来做到这一点。请注意,由于您没有初始化 libc,即使您静态链接 libc,您也无法调用其函数。
请参阅如何在没有 Glibc 的情况下在 C 中使用内联汇编获取参数值?对于一些黑客。(基本上_start 是asm()在全局范围内的声明中编写的独立 asm ,或者我的回答是对调用约定的完全破解。)