使用 NASM 以尽可能少的代码打印换行符

Print newline with as little code as possible with NASM

我为了好玩而学习了一些汇编,我可能太新手了,不知道正确的术语并自己找到答案。

我想在我的程序结束时打印一个换行符。

以下工作正常。

section .data
    newline db 10

section  .text
_end:
    mov rax, 1
    mov rdi, 1
    mov rsi, newline
    mov rdx, 1
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

但我希望在不在 .data 中定义换行符的情况下获得相同的结果。是否可以直接用你想要的字节调用 sys_write,或者它必须总是通过引用一些预定义的数据来完成(我假设这是 mov rsi, newline 正在做的)?

简而言之,为什么我不能用 mov rsi, 10 替换 mov rsi, newline

它需要rsi 寄存器中字符串的地址。不是字符或字符串。

mov rsi, newline 加载 newline 的地址到 rsi.

总是需要将内存中的数据复制到file-descriptor。 没有 system-call 等效于 C stdio fputc 的值而不是指针获取数据。

mov rsi, newline 指针 放入寄存器(使用巨大的 mov r64, imm64 指令)。 sys_write 不会 special-case size=1 并将其 void *buf arg 视为 char value 如果它不是有效指针。

没有任何其他系统调用可以解决问题。 pwritewritev 复杂(获取文件偏移量和指针,或获取指针+长度的数组以收集内核中的数据space).


有一个很多你可以为code-size优化这个。https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code

首先,将换行符放入静态存储意味着需要在寄存器中生成一个静态地址。您的选择是:

  • 5 字节 mov esi, imm32(仅在 Linux non-PIE 可执行文件中,因此静态地址是 link-time 常量并且已知位于虚拟地址的低 2GiB 中space 因此可以作为 32 位 zero-extended 或 sign-extended)
  • 7-byte lea rsi, [rel newline] 无处不在,如果你不能使用 5-byte mov-immediate.
  • 唯一好的选择
  • 10 字节 mov rsi, imm64。这甚至在 PIE 可执行文件中也有效(例如,如果你 link 与 gcc -nostdlib 没有 -static,在 PIE 是默认的发行版上。)但只能通过运行时重定位修复,并且 code-size 太可怕了。编译器从不使用它,因为它不比 LEA 快。

但是就像我说的,我们可以完全避免静态寻址:使用push 将即时数据放入堆栈。即使我们需要 zero-terminated 字符串也是如此,因为 push imm8push imm32 都是 sign-extend 64 位的立即数。由于 ASCII 使用 0..255 范围的低半部分,这相当于 zero-extension.

然后我们只需要将RSP复制到RSI,因为push让RSP指向被推送的数据。 mov rsi, rsp 将是 3 个字节,因为它需要一个 REX 前缀。如果您的目标是 32 位代码或 x32 ABI(长模式下的 32 位指针),您可以使用 2 字节 mov esi, esp。但是 Linux 将堆栈指针放在用户虚拟地址 space 的顶部,所以在 x86-64 上是 0x007ff...,就在低规范范围的顶部。因此,将指向堆栈内存的指针截断为 32 位不是一种选择;我们会得到 -EFAULT.

但是我们可以用1字节push + 1字节pop复制一个64位寄存器。 (假设两个寄存器都不需要 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

是最 well-known x86 窥孔优化:它节省了 3 个字节的代码大小,实际上 mov edi, 0 更高效 ].但是您只要求最小的代码来打印换行符,而没有指定它必须以 status = 0 退出。因此我们可以通过省略它来节省 2 个字节。

因为我们只是在进行 _exit 系统调用,所以我们不需要从我们推送的 10 中清理堆栈。

顺便说一句,如果 write returns 出错,这将崩溃。 (例如,重定向到 /dev/full,或以 ./newline >&- 关闭,或任何其他条件。)这将留下 RAX=-something,因此 mov al, 60 将给我们 RAX=0xffff...3c。然后我们从无效的电话号码中得到 -ENOSYS,然后从 _start 的末尾掉下来,并将接下来的内容解码为指令。 (可能是零字节,用 [rax] 作为寻址模式解码。然后我们会用 SIGSEGV 出错。)


objdump -d -Mintel 该代码的反汇编,在使用 nasm -felf64 构建并使用 ld

构建后 link
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

所以总共code-size是0x11 = 17字节。 vs. 你的版本有 39 字节的代码 + 1 字节的静态数据。前 3 个 mov 指令的长度分别为 5、5 和 10 个字节。 (如果您使用 YASM 没有将其优化为 mov eax,1,则 mov rax,1 的长度为 7 个字节)。

运行它:

$ 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 偏移量适合 sign-extended disp8 并且 RDX 保存 foo.

的地址

那么你可以在静态存储中拥有 newline: db 10。 (将其设为 .rodata.data,具体取决于您已经指向哪个部分)。