在ARM64上打印通过scanf获取的浮点数

编程语言 2026-07-12

我正在尝试从控制台读取一个浮点值并把它打印出来。我在一块运行Debian Bullseye的树莓派上运行这段代码。

我从这段代码开始。它把浮点值硬编码在程序中,按预期打印出来:

// print_a_float.s

.global main
.extern printf

.section .data
output_str:
    .asciz "Value is %f.\n"
value:
    .double 1.2345

.section .text
main:
    // Prolog
    stp x29, x30, [sp, -16]!
    mov x29, sp

print:
    ldr x0, =output_str     // Address of output_str into x0
    ldr x1, =value          // Address of value into x1
    ldr d0, [x1]            // Value in address of x1 into d0

    bl printf

    // Cleanup
    mov x0, #0              // Exit code
    ldp x29, x30, [sp], 16  // Restore SP and PC
    ret

有趣的是,使用GDB调试器并逐步执行到printf命令时,显示的并非value为 1.2345,而是d0。

gdb 输出显示变量和值 d0

当我通过scanf从用户那里获取浮点值时,代码就不起作用了。

// print_a_float_2.s

.global main
.extern scanf
.extern printf

.section .data
input_str:
    .asciz "Input a float: "
input_fmt:
    .asciz "%f"
value:
    .double 0
output_str:
    .asciz "You entered %f.\n"

.section .text
main:
    // Prolog
    stp x29, x30, [sp, -16]!
    mov x29, sp

    // Get value from user
    ldr x0, =input_str
    bl printf

    ldr x0, =input_fmt
    ldr x1, =value
    bl scanf

print:
    // Now print value
    ldr x0, =output_str
    ldr x1, =value
    ldr d0, [x1]
    bl printf

    // Cleanup
    mov x0, #0
    ldp x29, x30, [sp], 16
    ret

这次如果在GDB调试器下运行代码直到第二条printf命令,value和 d0的内容就不一样。

gdb 输出显示变量和值寄存器

在这里,value确实包含了用户输入的1.2345,但d0不包含。

请给点建议好吗?

解决方案

首先,我认为你应该避免把“float”这个词当成泛指浮点数的习惯用法。在通常的语境中 float 指的就是C 语言的类型,在这个平台(以及大多数其他平台)上,它是32位的IEEE单精度浮点数。另一方面,double 是64位的双精度。把这两种数据类型区分清楚并按它们各自的类型去访问对象至关重要。你遇到的所有问题都源于把它们混淆。

在你代码的第一版中,期望的值在内存中以 double 的形式存在,你把它加载到64位的 d0 寄存器。这正是 printf 所期望的。注意,因为它是一个变参函数,C编译器会在把参数传给 printf 之前把任何 float 参数提升为 double,所以在所有情况下你都必须提供 double。 (这也意味着对于 printf%f%lf 的格式说明符是相同的。)

请注意,当你让GDB把 valuefloat 打印时,你得到的是存储在那里的值的低32位,被解释为32位的 float。这不是你想要的结果。要看到正确的64位值,你应该执行 p (double) value,它会正确地输出 $1 = 1.2344999999999999

另一方面,对于 scanf,它实际上可以通过指针参数返回一个 float 或者 double 的值,取决于你使用的格式说明符是 %f 还是 %lf。你用的是 %f,这意味着 scanf 会在你提供的地址 value 上存储一个32位的 float(4字节)。当你对 d0 进行64位载入时,你会把这32位的 float 作为低字节,而高字节原本就有的32位零。然后你把它传给 printf,它会把这些位解释为一个 double,其值是没有意义的。

如果你想只使用 double(通常在没有充分理由时这是一个好主意,例如节省内存或提高SIMD吞吐量),那么你需要做的只是把 scanf 的格式说明符改为 %lf。做出这个改动后,对我来说就可以正确工作。

(另一个小修正是,你可能想在 .balign 8 之前执行 value: .double ...,以使其对齐到8 字节。否则由于前面的字符串的原因,可能会出现对齐错误,这可能导致字节数为奇数。Linux/ARM64允许这样做,未对齐的加载和存储会成功,但会带来性能损失。其他操作系统可以配置为在未对齐的内存访问时产生异常。)


现在,如果你真的想尽量在任何地方都使用 float,那么你需要按如下方式修改代码:

  • scanf 说明符保持为 %f
  • value 的大小减至4 字节,例如 value: .float 0.0。或甚至 value: .space 4,因为它的初始值无关紧要且会被覆盖。(同理,你也可以把它放在 .bss 段中,理论上可以略微减小可执行文件的大小。)并在前面加上 .balign 4

(如果你把它保留为8 字节并使用 .double,代码仍然会工作,但另外的4 字节会被浪费。) * 从内存加载输入值时,对32位浮点寄存器进行一次4 字节加载:ldr s0, [x1]。 * 由于 printf 仍然期望 double,且这个不能改,你必须把它从单精度转换为双精度。对这一步的ARM64指令是 fcvt d0, s0。 (如果你试图用 float 调用 printf,C编译器会替你完成这个转换,但这是汇编,你必须自己完成。) * 在用GDB检查 value 时使用 p (float) value


如果你有使用高级语言的经验,那么像这样管理数据类型是在转向汇编时最重大的变化之一。在C 语言中,你可以在源代码中几乎把 floatdouble 视为可互换;编译器会跟踪它们的关系,为每一种发出正确的指令,并发出把它们互相转换的指令。对于不同宽度的有符号和无符号整数也是同样的道理。

但在汇编中,请记住CPU只看到位,没有关于它们的来源或相关类型的任何信息。完全由你,作为程序员,来记住你想如何解释这些位(32位有符号整数、16位无符号整数、64位双精度、指针等),然后选择与这些解释相匹配的指令。 (例如,基于两个整数的比较所做的条件跳转,可能需要是 b.gtb.hi,这取决于你将这些位解释为有符号还是无符号整数。)

如果你搞错了,没有任何检查来帮助你捕捉错误;你只会得到无意义的值,或者更糟。因此,你必须非常小心、仔细地记住你想要的结果。

当然,只要你理解浮点数,结果通常是确定的并且常常是可识别的。将一个32位 float 的位模式零扩展到64位并解释为 double,得到的是一个非常小的子正常数,其指数字段为 0,尾数中最低位为非零,其余位为零。相反,当把一个 binary64 double 位模式 作为一个binary32 float 位模式来使用时,像3.0或 0.75这样的四舍五入数的低32位将全为零。因此,有时你可以把这些现象作为线索,以判断自己是否犯了这类错误。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章