GNU as, puts 有效但 printf 无效

GNU as, puts works but printf does not

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

# 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 [=10=]
    call _exit

    .section .data
message: .asciz "Hello, World!"

Compilation instructions: gcc test.s -o test

修订版 1:

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

    lea message, %rdi
    call printf

    mov [=11=], %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"

最终修订版(作品):

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

    mov [=12=], %rax
    lea message, %rdi
    call printf

    # flush stdout buffer.
    mov [=12=], %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 [=12=], %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"

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

有人可以解释这种行为以及 printf 的调用方式吗?

提前致谢。


总结:

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

puts 隐式附加一个换行符,标准输出是行缓冲的(默认情况下在终端上)。所以来自 printf 的文本可能只是坐在缓冲区中。你的call to _exit(2) doesn't flush buffers, because it's the exit_group(2) system call, not the exit(3) library function。 (请参阅下面我的代码版本)。

您对 printf(3) 的调用也不完全正确,因为您在调用不带 FP 参数的 var-args 函数之前没有将 %al 归零。 (好消息@RossRidge,我错过了)。 %al 将是非零的(来自 puts() 的 return 值),这大概就是 printf 段错误的原因。我在我的系统上进行了测试,当堆栈未对齐时 printf 似乎并不介意(确实是这样,因为你在调用它之前按了两次,这与 puts 不同)。


此外,您不需要该代码中的任何 push 指令。第一个参数进入 %rdi。前 6 个整数参数进入寄存器,第 7 个和后面进入堆栈。您还忽略了在函数 return 之后弹出堆栈,这只有效,因为您的函数在弄乱堆栈后从不尝试 return。

ABI 确实需要按 16B 对齐堆栈,push 是一种方法,在最近带有堆栈引擎的 Intel CPU 上,这实际上比 sub , %rsp 更有效,而且它需要更少的字节。 (参见 the x86-64 SysV ABI, and other links in the 标签 wiki)。


改进代码:

.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!"

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

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