GNU as, puts 有效,但 printf 无效

Dmi*_*try 0 64-bit x86 assembly gnu-assembler crt

这是我现在正在玩的代码:

# file-name: test.s
# 64-bit GNU as source code.
    .global main

    .section .text
main:
    lea message, %rdi
    push %rdi
    call puts

    lea message, %rdi
    push %rdi
    call printf

    push $0
    call _exit

    .section .data
message: .asciz "Hello, World!"
Run Code Online (Sandbox Code Playgroud)

编译说明:gcc test.s -o test

修订版 1:

    .global main
    .section .text
main:
    lea message, %rdi
    call puts

    lea message, %rdi
    call printf

    mov $0, %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"
Run Code Online (Sandbox Code Playgroud)

最终修订版(作品):

    .global main
    .section .text
main:
    lea message, %rdi
    call puts

    mov $0, %rax
    lea message, %rdi
    call printf

    # flush stdout buffer.
    mov $0, %rdi
    call fflush

    # put newline to offset PS1 prompt when the program ends.  
    # - ironically, doing this makes the flush above redundant and can be removed.
    # - The call to  fflush is retained for display and 
    #      to keep the block self contained.  
    mov $'\n', %rdi
    call putchar

    mov $0, %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"
Run Code Online (Sandbox Code Playgroud)

我很难理解为什么对 puts 的调用会成功,但对 printf 的调用会导致分段错误。

有人可以解释这种行为以及如何调用 printf 吗?

提前致谢。


总结

  1. printf 从 %rdi 获取打印字符串以及 %rax 的下 DWORD 中附加参数的数量。
  2. 在将换行符放入 stdout 或调用 fflush(0) 之前,无法看到 printf 结果。

Pet*_*des 5

puts隐式附加换行符,并且 stdout 是行缓冲的(默认情况下在终端上)。所以来自的文本printf可能只是坐在缓冲区中。您对 的调用_exit(2)不会刷新缓冲区,因为它是exit_group(2)系统调用,而不是exit(3)库函数。(请参阅下面我的代码版本)。

您对 的调用printf(3)也不完全正确,因为%al在调用没有 FP 参数的 var-args 函数之前您没有归零。(很好的捕获@RossRidge,我错过了)。 xor %eax,%eax是最好的方法%al将是非零(来自puts()的返回值),这大概是 printf 段错误的原因。我在我的系统上进行了测试,当堆栈未对齐时,printf 似乎并不介意(这是因为您在调用它之前推送了两次,与 puts 不同)。


此外,您不需要push该代码中的任何说明。第一个 arg 进入%rdi。前 6 个整数 args 进入寄存器,第 7 个和以后进入堆栈。您还忽略了在函数返回后弹出堆栈,这只是因为您的函数在弄乱堆栈后从不尝试返回。

ABI 确实需要将堆栈对齐 16B,而 apush是一种方法,它实际上比sub $8, %rsp最近的带有堆栈引擎的 Intel CPU更有效,并且占用的字节更少。(请参阅x86-64 SysV ABI标签维基中的其他链接)。


改进的代码:

.text
.global main
main:
    lea     message, %rdi     # or  mov $message, %edi  if you don't need the code to be position-independent: default code model has all labels in the low 2G, so you can use shorter 32bit instructions
    push    %rbx              # align the stack for another call
    mov     %rdi, %rbx        # save for later
    call   puts

    xor     %eax,%eax         # %al = 0 = number of FP args for var-args functions
    mov     %rbx, %rdi        # or mov %ebx, %edi  will normally be safe, since the pointer is known to be pointing to static storage, which will be in the low 2G
    call   printf

    # optionally putchar a '\n', or include it in the string you pass to printf

    #xor    %edi,%edi    # exit with 0 status
    #call  exit          # exit(3) does an fflush and other cleanup

    pop     %rbx         # restore caller's rbx, and restore the stack

    xor     %eax,%eax    # return 0
    ret

    .section .rodata     # constants should go in .rodata
message: .asciz "Hello, World!"
Run Code Online (Sandbox Code Playgroud)

lea message, %rdi很便宜,并且执行两次比使用 的两条mov指令更少的指令%rbx。但是由于我们需要将堆栈调整 8B 以严格遵循 ABI 的 16B 对齐保证,我们不妨通过保存调用保留寄存器来实现。 mov reg,reg非常便宜且体积小,因此利用调用保留的 reg 是很自然的。

使用mov %edi, %ebx类似的东西可以在机器代码编码中保存 REX 前缀。如果您不确定/不明白为什么只复制低 32 位,将高 32b 清零,然后使用 64 位寄存器是安全的。一旦您了解了发生了什么,您就会知道何时可以使用 32 位操作数大小来保存机器代码字节。