assembly GNU as,puts可以工作,但printf不行

b09cbbtk  于 5个月前  发布在  其他
关注(0)|答案(1)|浏览(63)

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

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

字符串
编译说明: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!"


最终版本(工程):

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


我很难理解为什么对put的调用成功了,但对printf的调用却导致了Segmentation错误。
有人能解释一下这种行为以及如何调用printf吗?
提前感谢。

摘要

  1. printf从%rdi中获取打印字符串,并在%rax的低位DWORD中获取附加参数的数量。
  2. printf的结果是看不到的,直到一个换行符被放入标准输出,或fflush(0)被调用。
q7solyqu

q7solyqu1#

puts隐式追加一个换行符,并且stdout是行缓冲的(在终端上默认)。因此来自printf的文本可能只是坐在缓冲区中。您的call to _exit(2)不会刷新缓冲区,因为它是exit_group(2) system call,而不是exit(3) library function。(请参阅下面我的代码版本)。
printf(3)的调用也不完全正确,因为在调用不带FP参数的var-args函数之前没有将%al置零。(很好的捕捉@RossRidge,我错过了). xor %eax,%eax is the best way to do that. %al将是非零的(来自puts()的返回值),这大概就是为什么printf segfaults.我在我的系统上测试过,而且printf似乎并不介意堆栈是否未对齐(它确实不介意,因为在调用它之前需要压入两次,不像puts)。
(更新:新版本的glibc * 即使AL=0,也会在printf中出现RSP未对齐的segfault,因为gcc更多地使用SSE来一次加载或存储16个字节,当然也利用了ABI保证的对齐。
另外,在这段代码中你不需要任何push指令。第一个参数放在%rdi中。前6个整数参数放在寄存器中,第7个和以后的放在堆栈中。你还忽略了在函数返回后弹出堆栈,这只会起作用,因为你的函数在弄乱堆栈后从不尝试返回。
ABI确实需要将堆栈对齐16 B,push是实现这一点的一种方法,在最新的英特尔CPU上,它实际上比sub $8, %rsp更高效,并且占用的字节更少。(参见the x86-64 SysV ABI,以及x86标签wiki中的其他链接)。
改进代码:

.text
.global main
main:
    lea     message(%rip), %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  in a non-PIE executable, since the pointer is known to be pointing to static storage which will be in the low 2GiB
    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 from main is equivalent to exit(0)
    ret

    .section .rodata     # constants should go in .rodata
message: .asciz "Hello, World!"

字符串
lea message(%rip), %rdi很便宜,执行两次比使用%rbx的两条mov指令少。但是由于我们需要将堆栈调整8B以严格遵循ABI的16 B对齐保证,我们不妨通过保存调用保留寄存器来实现。mov reg,reg非常便宜和小巧,所以利用调用保留的reg是很自然的。
现代发行版现在默认使PIE可执行文件,因此即使是静态存储,指针也是64位的。您需要RIP相对莱亚,并且需要64位的操作数大小来复制它们。参见 * How to load address of function or label into register * 与非PIE中的mov $message, %edi。永远没有理由在32位绝对寻址模式下使用lea message, %rdi,只有RIP相对莱亚或mov-immediate。

相关问题