您总是需要内存中的数据将其复制到文件描述符中。 没有与 C stdio 等效的系统调用fputc
,它通过值而不是指针获取数据。
mov rsi, newline
将指针放入寄存器(带有巨大的mov r64, imm64
指令)。 如果它不是一个有效的指针,sys_write
则不会特殊情况 size=1 并将其void *buf
arg 视为 char值。
没有任何其他系统调用可以解决问题。 pwrite
并且writev
两者都更复杂(采用文件偏移量和指针,或采用指针+长度数组来收集内核空间中的数据)。
不过,您可以做很多事情来优化代码大小。 请参阅https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code
首先,将换行符放入静态存储意味着您需要在寄存器中生成一个静态地址。您的选择是:
- 5 字节
mov esi, imm32
(仅在 Linux 非 PIE 可执行文件中,因此静态地址是链接时常量,并且已知位于虚拟地址空间的低 2GiB 中,因此可用作 32 位零扩展或符号扩展)
- 7-byte
lea rsi, [rel newline]
适用于任何地方,如果您不能使用 5-byte mov-immediate,这是唯一的好选择。
- 10 字节
mov rsi, imm64
。这甚至在 PIE 可执行文件中也有效(例如,如果您在 PIE 是默认设置的发行版上使用gcc -nostdlib
without链接-static
。)但只能通过运行时重定位修复,并且代码大小很糟糕。编译器从不使用它,因为它并不比 LEA 快。
但就像我说的,我们可以完全避免静态寻址:push
用于将即时数据放入堆栈。即使我们需要以零结尾的字符串,这也有效,因为push imm8
两者push imm32
都将立即数符号扩展为 64 位。由于 ASCII 使用 0..255 范围的低半部分,这相当于零扩展。
然后我们只需要将 RSP 复制到 RSI,因为push
让 RSP 指向被推送的数据。 mov rsi, rsp
将是 3 个字节,因为它需要一个 REX 前缀。如果您的目标是 32 位代码或 x32 ABI(长模式下的 32 位指针),您可以使用 2-byte mov esi, esp
。但是 Linux 将堆栈指针放在用户虚拟地址空间的顶部,所以在 x86-64 上是 0x007ff ...,就在低规范范围的顶部。因此,将指向堆栈内存的指针截断为 32 位不是一种选择。我们会得到-EFAULT
。
push
但是我们可以用 1-byte + 1-byte复制一个 64 位寄存器pop
。(假设两个寄存器都不需要 REX 前缀来访问。)
default rel ; We don't use any explicit addressing modes, but no reason to leave this out.
_start:
push 10 ; \n
push rsp
pop rsi ; 2 bytes total vs. 3 for mov rsi,rsp
push 1 ; _NR_write call number
pop rax ; 3 bytes, vs. 5 for mov edi, 1
mov edx, eax ; length = call number by coincidence
mov edi, eax ; fd = length = call number also coincidence
syscall ; write(1, "\n", 1)
mov al, 60 ; assuming write didn't return -errno, replace the low byte and keep the high zeros
;xor edi, edi ; leave rdi = 1 from write
syscall ; _exit(1)
.size: db $ - _start
xor-zeroing是最著名的 x86 窥孔优化:它节省了 3 个字节的代码大小,实际上比mov edi, 0
. 但是您只要求打印换行符的最小代码,而没有指定它必须以 status = 0 退出。所以我们可以通过省略它来节省 2 个字节。
由于我们只是进行_exit
系统调用,因此我们不需要从10
我们推送的堆栈中清理堆栈。
顺便说一句,如果write
返回错误,这将崩溃。(例如重定向到/dev/full
,或关闭./newline >&-
,或任何其他条件。)这将留下 RAX=-something,所以mov al, 60
会给我们 RAX= 0xffff...3c
。然后我们会-ENOSYS
从无效的电话号码中获取,并从结尾处掉下来并将_start
接下来的内容解码为指令。(可能[rax]
是作为寻址模式解码的零字节。然后我们会用 SIGSEGV 出错。)
objdump -d -Mintel
在构建nasm -felf64
和链接之后,对该代码的反汇编ld
0000000000401000 <_start>:
401000: 6a 0a push 0xa
401002: 54 push rsp
401003: 5e pop rsi
401004: 6a 01 push 0x1
401006: 58 pop rax
401007: 89 c2 mov edx,eax
401009: 89 c7 mov edi,eax
40100b: 0f 05 syscall
40100d: b0 3c mov al,0x3c
40100f: 0f 05 syscall
0000000000401011 <_start.size>:
401011: 11 .byte 0x11
所以总代码大小是 0x11 = 17 个字节。与您使用 39 字节代码 + 1 字节静态数据的版本相比。仅您的前 3mov
条指令就有 5、5 和 10 个字节长。mov rax,1
(如果您使用未将其优化为的 YASM,则长度为 7 个字节mov eax,1
)。
运行它:
$ strace ./newline
execve("./newline", ["./newline"], 0x7ffd4e98d3f0 /* 54 vars */) = 0
write(1, "\n", 1
) = 1
exit(1) = ?
+++ exited with 1 +++
如果这是更大计划的一部分:
如果您已经有一个指向寄存器中一些附近静态数据的指针,您可以执行类似 4 字节lea rsi, [rdx + newline-foo]
(REX.W + opcode + modrm + disp8)的操作,假设newline-foo
偏移量适合符号扩展的 disp8 并且 RDX 持有的地址foo
。
那么你newline: db 10
毕竟可以拥有静态存储。(把它.rodata
或.data
,取决于你已经有一个指向的部分)。