我正在学习一些有趣的汇编语言,可能我太绿了,无法知道正确的术语并自己找到答案。
我想在程序末尾打印换行符。
以下工作正常。
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, newline
替换为mov rsi, 10
?
答案 0 :(得分:4)
您总是需要内存中的数据才能将其复制到文件描述符中。 没有等效于C stdio fputc
的系统调用,它可以通过值而不是指针来获取数据。
mov rsi, newline
将 pointer 放入寄存器(带有巨大的mov r64, imm64
指令)。 sys_write
不是size = 1的特殊情况,并且如果它不是有效的指针,则将其void *buf
arg视为char value 。
没有其他系统调用可以达到目的。 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
链接而没有使用push
,则在发行版中PIE是默认的。)但是只能通过运行时重定位修正,并且代码大小很糟糕。编译器从不使用它,因为它不比LEA快。但是就像我说的那样,我们可以完全避免静态寻址:使用push imm8
将立即数据放在堆栈中。即使我们需要零终止的字符串,这也可以工作,因为push imm32
和push
都将立即数符号扩展为64位。由于ASCII使用0..255范围的下半部分,因此这等效于零扩展。
然后,我们只需要将RSP复制到RSI,因为mov rsi, rsp
使RSP指向被推送的数据。 mov esi, esp
将为3个字节,因为它需要一个REX前缀。如果您的目标是32位代码或x32 ABI(长模式下的32位指针),则可以使用2字节的-EFAULT
。但是Linux将堆栈指针放在用户虚拟地址空间的顶部,因此在x86-64上为0x007ff ...,就在低规范范围的顶部。因此,将堆栈存储器的指针截断为32位不是一个选择。我们会得到push
。
但是我们可以复制1字节pop
+ 1字节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
的64位寄存器。 (假设两个寄存器都不需要REX前缀即可访问。)
mov edi, 0
xor-zeroing是最著名的x86窥孔优化:它节省了3个字节的代码大小,并且实际上比_exit
更有效率。但是,您只要求最小的代码来打印换行符,而无需指定必须以status = 0退出。因此,我们可以省去2个字节。
由于我们只是在进行10
系统调用,因此我们不需要清理我们推送的write
中的堆栈。
顺便说一句,如果/dev/full
返回错误,则会崩溃。 (例如,重定向到./newline >&-
,或用mov al, 60
关闭,或其他任何条件。)这会使RAX =失效,因此0xffff...3c
会给我们RAX = -ENOSYS
。然后,我们将从无效的呼叫号码中获得_start
,从[rax]
的结尾掉下来,然后解码接下来的指令。 (可能是零字节,以objdump -d -Mintel
作为寻址模式进行解码。然后我们将使用SIGSEGV进行故障处理。)
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
链接后反汇编该代码。
mov
因此,总代码大小为0x11 = 17个字节。与您使用39字节代码+ 1字节静态数据的版本进行比较。仅前3条mov rax,1
指令的长度为5、5和10个字节。 (或者,如果您使用YASM不会将其优化为mov eax,1
,则$ strace ./newline
execve("./newline", ["./newline"], 0x7ffd4e98d3f0 /* 54 vars */) = 0
write(1, "\n", 1
) = 1
exit(1) = ?
+++ exited with 1 +++
的长度为7个字节)。
运行它:
lea rsi, [rdx + newline-foo]
如果您已经有一个指向寄存器中某些附近静态数据的指针,则可以假设4个字节newline-foo
(REX.W +操作码+ modrm + disp8),假设{{1} } offset适用于符号扩展的disp8,并且RDX拥有foo
的地址。
那么您最终可以在静态存储中拥有newline: db 10
。 (将其放入.rodata
或.data
,具体取决于您已指向哪个部分。)
答案 1 :(得分:2)
它期望rsi
寄存器中字符串的地址。不是字符或字符串。
mov rsi, newline
将newline
的地址加载到rsi
中。