使用NASM以最少的代码打印换行符

时间:2019-07-12 07:11:43

标签: 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

但是我希望在不定义.data换行符的情况下获得相同的结果。是否可以直接使用所需的字节调用sys_write,还是必须始终通过引用一些预定义的数据(我假设mov rsi, newline在做什么)来完成?

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

2 个答案:

答案 0 :(得分:4)

总是需要内存中的数据才能将其复制到文件描述符中。 没有等效于C stdio fputc的系统调用,它可以通过值而不是指针来获取数据。

mov rsi, newline pointer 放入寄存器(带有巨大的mov r64, imm64指令)。 sys_write不是size = 1的特殊情况,并且如果它不是有效的指针,则将其void *buf arg视为char value

没有其他系统调用可以达到目的。 pwritewritev都比较复杂(采用文件偏移量和指针,或者采用指针+长度的数组来收集内核空间中的数据)。


您可以执行很多来优化代码大小。请参见https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code

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

  • 5个字节的mov esi, imm32(仅在Linux非PIE可执行文件中,因此静态地址是链接时间常数,并且已知位于虚拟地址空间的低2GiB中,因此可以作为32位零位地址使用-扩展或符号扩展)
  • 7字节的lea rsi, [rel newline]随处可见,如果您不能使用5字节的mov-immediate,这是一个不错的选择。
  • 10个字节的mov rsi, imm64。即使在PIE可执行文件中也可以使用(例如,如果您将gcc -nostdlib-static链接而没有使用push,则在发行版中PIE是默认的。)但是只能通过运行时重定位修正,并且代码大小很糟糕。编译器从不使用它,因为它不比LEA快。

但是就像我说的那样,我们可以完全避免静态寻址:使用push imm8将立即数据放在堆栈中。即使我们需要零终止的字符串,这也可以工作,因为push imm32push都将立即数符号扩展为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, newlinenewline的地址加载到rsi中。