4

I'm learning a bit of assembly for fun and I am probably too green to know the right terminology and find the answer myself.

I want to print a newline at the end of my program.

Below works fine.

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

But I'm hoping to achieve the same result without defining the newline in .data. Is it possible to call sys_write directly with the byte you want, or must it always be done with a reference to some predefined data (which I assume is what mov rsi, newline is doing)?

In short, why can't I replace mov rsi, newline by mov rsi, 10?

4

2 回答 2

6

总是需要内存中的数据将其复制到文件描述符中。 没有与 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

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

  • 5 字节mov esi, imm32 (仅在 Linux 非 PIE 可执行文件中,因此静态地址是链接时常量,并且已知位于虚拟地址空间的低 2GiB 中,因此可用作 32 位零扩展或符号扩展)
  • 7-bytelea rsi, [rel newline]适用于任何地方,如果您不能使用 5-byte mov-immediate,这是唯一的好选择。
  • 10 字节mov rsi, imm64。这甚至在 PIE 可执行文件中也有效(例如,如果您在 PIE 是默认设置的发行版上使用gcc -nostdlibwithout链接-static。)但只能通过运行时重定位修复,并且代码大小很糟糕。编译器从不使用它,因为它并不比 LEA 快。

但就像我说的,我们可以完全避免静态寻址:push用于将即时数据放入堆栈。即使我们需要以零结尾的字符串,这也有效,因为push imm8两者push imm32都将立即数符号扩展为 64 位。由于 ASCII 使用 0..255 范围的低半部分,这相当于零扩展。

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

push但是我们可以用 1-byte + 1-byte复制一个 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

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

由于我们只是进行_exit系统调用,因此我们不需要从10我们推送的堆栈中清理堆栈。

顺便说一句,如果write返回错误,这将崩溃。(例如重定向到/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

所以总代码大小是 0x11 = 17 个字节。与您使用 39 字节代码 + 1 字节静态数据的版本相比。仅您的前 3mov条指令就有 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 +++

如果这是更大计划的一部分:

如果您已经有一个指向寄存器中一些附近静态数据的指针,您可以执行类似 4 字节lea rsi, [rdx + newline-foo](REX.W + opcode + modrm + disp8)的操作,假设newline-foo偏移量适合符号扩展的 disp8 并且 RDX 持有的地址foo

那么你newline: db 10毕竟可以拥有静态存储。(把它.rodata.data,取决于你已经有一个指向的部分)。

于 2019-07-12T07:50:08.733 回答
2

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

mov rsi, newline将地址加载newlinersi.

于 2019-07-12T07:40:02.307 回答