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

这个程序段错误而不打印任何东西。只是一个可悲的段错误。

但我可以通过 push 和 pop 来解决这个问题%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. 那么问题一定出在其他地方吗?

4

1 回答 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 上更便宜,并且节省了代码大小。

于 2013-04-19T04:40:19.883 回答