【问题标题】:Printing floating point numbers from x86-64 seems to require %rbp to be saved从 x86-64 打印浮点数似乎需要保存 %rbp
【发布时间】:2013-04-12 09:46:30
【问题描述】:

当我在 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 的目的是什么?
  • 浮点参数计数,IIRC。
  • 相关:您不能直接打印带有printffloat,只能打印double(带有"%f")或long double,因为可变参数函数的C 提升规则:@987654321 @.
  • 也相关; glibc printf 仅在 %al != 0 时关心堆栈对齐,因为这就是 gcc 编译可能接受 FP args 的可变参数函数的方式。 printf float in nasm assembly 64-bit 表明 printf 在使用未对齐的堆栈和 RAX=0 调用时不会崩溃,答案显示 gcc 的代码(仅对非零 AL 运行)将 xmm0..7 转储到带有 @ 的堆栈987654342@(可变参数函数也可以接受__m128 args,而不仅仅是double。)

标签: assembly floating-point x86-64


【解决方案1】:

我怀疑这个问题与%rbp 没有任何关系,而是与堆栈对齐有关。引用 ABI:

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

当您输入main() 时,堆栈对齐。调用printf() 将返回地址压入堆栈,将堆栈指针移动8 个字节。您可以通过将另外 8 个字节压入堆栈来恢复对齐(恰好是 %rbp,但也可以很容易地成为其他东西)。

这是gcc 生成的代码(也是on the Godbolt compiler explorer):

.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 并在末尾添加回来来处理对齐要求。

您可以改为对您喜欢的任何寄存器进行虚拟推送/弹出,而不是直接操作%rspsome compilers do use a dummy push to align the stack 因为 this can actually be cheaper 在现代 CPU 上,并且节省了代码大小。

【讨论】:

  • 我认为你是对的 - 我自己也遇到过类似的问题。它适用于整数的原因只是抽签的运气。未定义的行为等等。如果没有堆栈调整,OP 的第一个示例也无法在我的机器上运行。不过,我只是使用了sub $8, %rsp
  • 有时完成的推送次数可能取决于,因此堆栈可能对齐到 16 个字节或不对齐。 spspl 的逻辑 AND 始终有效,如下所示:and spl,0xf0
  • @NPE 不错的答案。我希望是那样的。我通过用 C 编写代码并执行 gcc -S 得到了 %push rbp 的想法。我非常了解 32 位程序集 push %ebp; mov %esp, %ebp 堆栈帧,并且认为 gcc 对 %rbp 的推送是过去的遗物,如果它真的很重要,我会感到震惊。感谢您提供有关对齐的说明;我会回去更仔细地研究 ABI 文档!
  • 对于某些平台,这是否是准确的 gcc 输出?通常你会得到.LC1(带有一个前导点,所以它是一个 GAS 本地标签),并且 Linux ELF 系统不使用前导下划线。是来自 MacOS X,它使用 x86-64 SysV 调用约定和符号名称上的_?匹配 Ubuntu 的 OP 代码可能会更好,所以它看起来不像 _main 而不是 main 是更正/答案的一部分。例如godbolt.org/g/2PKKAP 具有来自 gcc4.6.4 -O3 的输出,并使用 .LC... 而没有 _,但除了指令顺序之外与您的答案相同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-09
  • 2015-06-09
  • 2020-06-07
  • 2014-01-20
相关资源
最近更新 更多