使用 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 如果它不是有效指针。
没有任何其他系统调用可以解决问题。 pwrite
和 writev
都 更 复杂(获取文件偏移量和指针,或获取指针+长度的数组以收集内核中的数据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 imm8
和 push 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
,具体取决于您已经指向哪个部分)。
我为了好玩而学习了一些汇编,我可能太新手了,不知道正确的术语并自己找到答案。
我想在我的程序结束时打印一个换行符。
以下工作正常。
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 如果它不是有效指针。
没有任何其他系统调用可以解决问题。 pwrite
和 writev
都 更 复杂(获取文件偏移量和指针,或获取指针+长度的数组以收集内核中的数据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 imm8
和 push 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
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
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
,具体取决于您已经指向哪个部分)。