blu*_*isk 3 linux assembly x86-64 nasm code-size
我正在学习一些有趣的汇编程序,并且我可能太绿了,无法知道正确的术语并自己找到答案。
我想在程序末尾打印换行符。
下面工作正常。
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
Run Code Online (Sandbox Code Playgroud)
但是我希望在不定义.data换行符的情况下实现相同的结果。是否可以sys_write直接使用所需的字节进行调用,还是必须始终通过引用一些预定义的数据来完成调用(我认为这mov rsi, newline是在做什么)?
简而言之,为什么我不能替换mov rsi, newline为mov rsi, 10?
您始终需要将内存中的数据复制到文件描述符中。 没有与C stdio等效的系统调用fputc,它按值而不是指针获取数据。
mov rsi, newline将指针放入寄存器(带有巨大mov r64, imm64指令)。 sys_write不特殊情况下size = 1,如果不是有效的指针,则将其void *bufarg视为char 值。
没有其他系统调用可以达到目的。 pwrite和writev均更复杂(采取文件偏移,以及一个指针,或服用指针+长度的阵列,以收集在内核空间中的数据)。
但是,您可以做很多事情来优化代码大小。 参见https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code
首先,将换行符放在静态存储中意味着您需要在寄存器中生成一个静态地址。您的选择如下:
mov esi, imm32 (仅在Linux非PIE可执行文件中,因此静态地址是链接时间常量,并且已知在虚拟地址空间的低2GiB中,因此可以作为32位零扩展或符号扩展来工作)lea rsi, [rel newline]适用于任何地方,如果您不能使用5字节mov-immediate,那么这是唯一的好选择。mov rsi, imm64。(如果您有链接如这个工程即使在PIE可执行文件gcc -nostdlib没有-static,在哪里发行PIE是默认的)。但只有通过运行时重定位修正,而代码大小是可怕的。编译器从不使用它,因为它不比LEA快。但是就像我说的,我们可以完全避免静态寻址:push用于将立即数据放入堆栈。即使我们需要以零结尾的字符串,这push imm8也push imm32可以工作,因为和都将立即数符号扩展为64位。由于ASCII使用0..255范围的下半部分,因此这等效于零扩展。
然后,我们只需要将RSP复制到RSI,因为pushRSP指向已推送的数据。 mov rsi, rsp将为3个字节,因为它需要一个REX前缀。如果您定位的是32位代码或x32 ABI(长模式下的32位指针),则可以使用2个字节mov esi, esp。但是Linux将堆栈指针放在用户虚拟地址空间的顶部,因此在x86-64上为0x007ff ...,就在低规范范围的顶部。因此,将堆栈存储器的指针截断为32位不是一个选择。我们会得到-EFAULT。
但是我们可以复制1字节push+ 1字节的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
Run Code Online (Sandbox Code Playgroud)
xor-zeroing是最著名的x86窥孔优化:它节省了3个字节的代码大小,并且实际上比效率更高mov edi, 0。但是您只要求最小的代码来打印换行符,而无需指定必须以status = 0退出。所以我们可以省去2个字节。
因为我们只是在进行_exit系统调用,所以我们不需要清理10推送的堆栈。
顺便说一句,如果write返回错误,它将崩溃。(例如,重定向到/dev/full,或使用./newline >&-或以其他任何条件关闭)。这将使RAX = 变为某些东西,因此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
Run Code Online (Sandbox Code Playgroud)
因此,总代码大小为0x11 = 17个字节。与您的版本(带有39个字节的代码+ 1个字节的静态数据)相比。仅您的前3 mov条指令的长度为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 +++
Run Code Online (Sandbox Code Playgroud)
如果您已经有一个指向寄存器中附近静态数据的指针,则可以执行4字节lea rsi, [rdx + newline-foo](REX.W +操作码+ modrm + disp8)操作,假设该newline-foo偏移量适合符号扩展的disp8并且RDX保持的地址foo。
然后,您最终可以拥有newline: db 10静态存储。(将其放入.rodata或.data,具体取决于您已经指向哪个部分)。
| 归档时间: |
|
| 查看次数: |
104 次 |
| 最近记录: |