从x86-64打印浮点数似乎需要保存%rbp

9
当我使用Ubuntu上的gcc 4.6.1编写一个简单的汇编语言程序,并链接C库,尝试打印一个整数时,它能够正常工作:
        .global main
        .text
main:
        mov     $format, %rdi
        mov     $5, %rsi
        mov     $0, %rax
        call    printf
        ret
format:
        .asciz  "%10d\n"

这将会输出5,和预期一致。

但是,如果我进行小改动,并尝试打印一个浮点数值:

        .global main
        .text
main:
        mov     $format, %rdi
        movsd   x, %xmm0
        mov     $1, %rax
        call    printf
        ret
format:
        .asciz  "%10.4f\n"
x:
        .double 15.5

这个程序出现段错误,没有输出任何信息。仅仅是一个令人悲伤的段错误。

但是我可以通过推入和弹出%rbp来解决这个问题。

        .global main
        .text
main:
        push    %rbp
        mov     $format, %rdi
        movsd   x, %xmm0
        mov     $1, %rax
        call    printf
        pop     %rbp
        ret
format:
        .asciz  "%10.4f\n"
x:
        .double 15.5

现在它可以工作,并打印出15.5000。
我的问题是:为什么推送和弹出%rbp使应用程序工作?根据ABI,%rbp是调用方必须保留的寄存器之一,因此printf不能搞砸它。事实上,在第一个程序中,当只传递一个整数给printf时,printf可以正常工作。所以问题必须在其他地方?

兴趣之余,那个mov%rax的目的是什么? - NPE
2
浮点参数的数量,如果我没记错的话。 - Carl Norum
相关提示:由于C语言变参函数的提升规则,您不能直接使用printf打印float类型的数据,只能使用double(使用"%f")或long double。请参考以下链接了解如何使用printf打印单精度浮点数:如何使用printf打印单精度浮点数 - Peter Cordes
同样相关的是,glibc printf 只在 %al != 0 时关心堆栈对齐,因为这是 gcc 编译可能接受 FP 参数的可变参数函数的方式。printf float in nasm assembly 64-bit 显示当使用未对齐的堆栈和 RAX=0 调用 printf 时,它恰好不会崩溃,并且答案显示了 gcc 的代码(仅适用于非零 AL),该代码使用 movaps 将 xmm0..7 转储到堆栈中(可变参数函数也可以接受 __m128 参数,而不仅仅是 double)。 - Peter Cordes
1个回答

10
我怀疑问题与 %rbp 无关,而是与堆栈对齐有关。引用 ABI 规定:

ABI 要求堆栈帧在 16 字节边界上对齐。具体来说,参数区域的结束位置(%rbp+16)必须是 16 的倍数。这个要求意味着帧大小应该填充到 16 字节的倍数。

当你进入 main() 函数时,堆栈已经对齐。调用 printf() 函数将返回地址推入堆栈,将堆栈指针向下移动了 8 个字节。通过将另外的 8 个字节推入堆栈(这恰好是 %rbp,但也可以是其他值),你可以恢复对齐。
以下是 gcc 生成的代码(也可以在 Godbolt 编译器浏览器 上查看):
.LC1:
        .ascii "%10.4f\12\0"
main:
        leaq    .LC1(%rip), %rdi   # format string address
        subq    $8, %rsp           ### align the stack by 16 before a CALL
        movl    $1, %eax           ### 1 FP arg being passed in a register to a variadic function
        movsd   .LC0(%rip), %xmm0  # load the double itself
        call    printf
        xorl    %eax, %eax         # return 0 from main
        addq    $8, %rsp
        ret

如您所见,它通过在开始时从%rsp中减去8,并在结束时将其加回来来处理对齐要求。

相反,您可以选择对任何寄存器执行虚拟推送/弹出操作,而不是直接操作%rsp; 一些编译器使用虚拟推送对齐堆栈,因为在现代CPU上这实际上可能更便宜且节省代码量


1
我认为你是对的 - 我自己也遇到过类似的问题。它对整数起作用的原因只是运气好而已。未定义的行为等等。OP的第一个示例在我的机器上也需要堆栈调整才能正常工作。我只是使用了 sub $8, %rsp - Carl Norum
有时候推送的数量可能会有所不同,因此堆栈可能会对齐到16字节或不对齐。对于 spspl 的逻辑 AND 总是有效的,例如:and spl,0xf0 - nrz
@NPE 很好的回答。我就是希望是这样的。我从用C语言编写代码并使用gcc -S命令得到了%push rbp的想法。我非常熟悉32位汇编中的push %ebp; mov %esp, %ebp堆栈帧,并且认为gcc推送%rbp是那些旧日遗留下来的东西,如果它真的很重要,我会感到震惊的。谢谢你提醒我关于对齐的问题;我会回去仔细研究ABI文档! - Ray Toal
这是某个平台的确切gcc输出吗?通常你会得到.LC1(带有前导点,因此它是一个GAS本地标签),Linux ELF系统不使用前导下划线。这是来自MacOS X的吗?它使用x86-64 SysV调用约定和符号名称上的_吗?最好匹配Ubuntu的OP代码,这样看起来就不像_main而是main是一个更正/答案的一部分。例如,https://godbolt.org/g/2PKKAP具有gcc4.6.4 -O3的输出,并使用.LC...而没有_,但除了指令顺序外,与您的答案完全相同。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接