gpt4 book ai didi

linux - 使用NASM以尽可能少的代码打印换行符

转载 作者:IT王子 更新时间:2023-10-29 01:04:32 26 4
gpt4 key购买 nike

我正在学习一些有趣的汇编程序,并且我可能太绿了,无法知道正确的术语并自己找到答案。

我想在程序末尾打印换行符。

下面工作正常。

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

最佳答案

您始终需要将内存中的数据复制到文件描述符中。 没有等效于C stdio fputc的系统调用,该系统调用通过值而不是指针来获取数据。
mov rsi, newline将指针放入寄存器(带有巨大的mov r64, imm64指令)。 sys_write的大小不是1的特殊情况,如果它不是有效的指针,则将其void *buf arg视为char值。

没有其他系统调用可以达到目的。 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可执行文件中也可以使用(例如,如果在默认PIE的发行版中使用gcc -nostdlib而不使用-static链接)。但是只能通过运行时重定位修正,并且代码大小很糟糕。编译器从不使用它,因为它不比LEA快。


  • 但是就像我说的 一样,我们可以完全避免静态寻址:使用push将立即数据放在堆栈中。即使我们需要以零结尾的字符串,这也可以工作,因为 push imm8push imm32都将立即数符号扩展为64位。由于ASCII使用0..255范围的下半部分,因此这等效于零扩展。

    然后,我们只需要将RSP复制到RSI,因为 push会使RSP指向被推送的数据。 mov rsi, rsp为3个字节,因为它需要一个REX前缀。如果您定位的是32位代码或x32 ABI(长模式下的32位指针),则可以使用2字节的 mov esi, esp。但是Linux将堆栈指针放在用户虚拟地址空间的顶部,因此在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

    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字节的静态数据。仅您的前3个 mov指令长5、5和10个字节。 (如果您使用的YASM不能将其优化为 mov rax,1,则 mov eax,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偏移量适合符号扩展的disp8,并且RDX保留 foo的地址。

    然后,您最终可以将 newline: db 10存储在静态存储中。 (根据您已有的指针将其放入 .rodata.data)。

    关于linux - 使用NASM以尽可能少的代码打印换行符,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57001872/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com