X86-64 Assembly中printf 打印 float 和 double的bug的解决

前言

笔者最近闲的无聊,学习一些Assembly,其实笔者以前也想学习,但是没有坚持下来

笔者的电脑是Windows x64,那就学习x86-64的Assembly。

笔者学习了打印hello world,然后打印int,后面在打印float和double遇到巨大的问题。

笔者才疏学浅,搞了老半天才明白。

正文

看代码

cpp 复制代码
extern printf
extern ExitProcess
section .data
    fmt_int db "Integer: %d",10,0
    fmt_float db "Float: %f",10,0
    my_int dd 42
    my_float dd 3.141
section .text
global main
main:
    sub rsp,40
    ; ========== 1. 打印整数 int ==========
    lea rcx,[rel fmt_int]
    mov rdx,[rel my_int]
    xor eax,eax
    call printf
    ; ========== 2. 打印 float(需先转 double)  ==========
    lea rcx,[rel fmt_float] ;rcx存放fmt_float的地址
    movss xmm0,[rel my_float] ;取内存里的值到xmm0
    cvtss2sd xmm0,xmm0 ;转换成8字节
    mov eax,1;AL=1,使用1个xmm寄存器
    call printf
    
    xor ecx,ecx
    call ExitProcess

这个assembly代码意思是显然的,首先打印整数,然后打印float。

rcx是第一个Windows x64的第一个参数寄存器,存放整数 / 指针 / 地址。

rdx是第一个Windows x64的第二个参数寄存器。

还有其他参数寄存器,r8,r9。

还有存放浮点数的寄存器,分别是xmm0,xmm1,xmm2,xmm3。


对于printf这个c函数,它是一个可变参数函数。

cpp 复制代码
lea rcx,[rel fmt_int]

它的参数类型和参数的个数都是不确定的。

int

首先先来看看打印int。

cpp 复制代码
printf("%d",a);

需要传入两个参数,因此,需要两个寄存器rcx和rdx。

那么第一个

cpp 复制代码
lea rcx,[rel fmt_int]

lea 是 Load Effective Address(加载有效地址)的缩写。

说白了,将fmt_int的地址存放到rcx。

cpp 复制代码
 mov rdx,[rel my_int]

mov 是 Move(传送)的缩写,作用是把数据从源操作数复制到目标操作数。

意思是rdx=42。

cpp 复制代码
xor  eax, eax

eax是rax这个64位寄存器的低32位。

上面的意思就是设置eax=0,本质上其实是设置rax的低8位al为0,即,al=0

al用来告诉变参函数printf:用了多少个 XMM 寄存器传浮点参数。

现在al设置为0,其实告诉printf没有设置浮点参数。


float

对float进行考虑

cpp 复制代码
printf("%f",a)

还是需要传入两个参数。

那个第一个参数的地址存放到rcx里面,没什么问题

cpp 复制代码
 lea  rcx, [rel fmt_float] 

因为,%f 按 64 位读取,需要把32位的float变成64位的/。

先把浮点数放到xmm0中。

cpp 复制代码
movss xmm0,[rel my_float]

然后变成64位。

cpp 复制代码
cvtss2sd xmm0,xmm0

因为使用了一个浮点寄存器,因此需要设置al=1,即

cpp 复制代码
mov eax,1

调用,打印。

代码看起来没什么问题。

笔者使用NASM 和Clang进行汇编和链接,笔者使用的终端是nushell。

Index of /pub/nasm/releasebuilds/3.01/win64https://www.nasm.us/pub/nasm/releasebuilds/3.01/win64/llvm/llvm-project: The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.https://github.com/llvm/llvm-project

笔者创建了一个build.nu文件。如下

nasm -f win64 test.asm -o test.obj
clang test.obj -o test.exe
test

运行,结果如下

cpp 复制代码
❯ nu build.nu
Integer: 42
Float: 0.000000

可以发现打印的int没什么问题,打印的float居然是0,这就非常奇怪了。

笔者在这里搞了许久,最后终于成功了。一些参考

printf打印float错误引起的思考-5kdYeW0-ChinaUnix博客http://blog.chinaunix.net/uid-20773046-id-578320.htmlhttps://stackoverflow.com/questions/78806627/nasm-x64-floating-point-part-incorrectly-printed-as-0-000000https://stackoverflow.com/questions/78806627/nasm-x64-floating-point-part-incorrectly-printed-as-0-000000这其实是windows的调用约定。

当调用 printf 这种可变参数函数时,浮点数必须同时出现在 XMM 寄存器和对应的通用寄存器中

说白了,意思就是说,xmm0中的浮点数需要复制到rdx中,即rdx=xmm0

关键的代码如下

cpp 复制代码
movq rdx,xmm0

全部的代码如下

cpp 复制代码
extern printf
extern ExitProcess
section .data
    fmt_int db "Integer: %d",10,0
    fmt_float db "Float: %f",10,0
    my_int dd 42
    my_float dd 3.141
section .text
global main
main:
    sub rsp,40
    ; ========== 1. 打印整数 int ==========
    lea rcx,[rel fmt_int]
    mov rdx,[rel my_int]
    xor eax,eax
    call printf
    ; ========== 2. 打印 float(需先转 double)  ==========
    lea rcx,[rel fmt_float] ;rcx存放fmt_float的地址
    movss xmm0,[rel my_float] ;取内存里的值到xmm0
    cvtss2sd xmm0,xmm0 ;转换成8字节
    movq rdx,xmm0
    mov eax,1;AL=1,使用1个xmm寄存器

    call printf
    
    xor ecx,ecx
    call ExitProcess

再次运行。

❯ nu build.nu
Integer: 42
Float: 3.141000

没问题。

double

那么对于double还是类似的。如下代码

cpp 复制代码
extern printf
extern ExitProcess
section .data
    fmt_double db "Double: %f",10,0
    my_double dq 2.71828
section .text
global main
main:
    sub rsp,40
    lea rcx,[rel fmt_double]
    movsd xmm0,[rel my_double]
    mov eax,1
    call printf
    
    xor ecx,ecx
    call ExitProcess

设置了两个参数,但是没有进行关键的一步,设置rdx=xmm0。

结果打印显然为0,测试一下。

cpp 复制代码
❯ nu build.nu
Double: 0.000000

没问题,添加之后。

cpp 复制代码
movq rdx,xmm0

结果如下

cpp 复制代码
❯ nu build.nu
Double: 2.718280

总结

至于为什么是这样的,笔者也不知道。

相关推荐
sg_knight21 小时前
Bug 定位实战:把报错丢给 Codex,它是怎么修的
chatgpt·llm·bug·openai·agent·ai编程·codex
8年车载测试2 天前
汽车五大域详解:动力、底盘、车身、座舱与智驾域
汽车·bug·jira·uds诊断·ecu刷写测试·canoe工具
ะัี潪ิื4 天前
Redisson Bug回顾
bug
猫咪很可爱6 天前
MySQL 5.7 备份恢复后函数与存储过程丢失:原因分析与正确备份姿势
数据库·mysql·bug·shell
李永奉6 天前
杰理可视化SDK开发-【BUG】AC7006F8芯片开启一拖二和双麦DNS通话算法后,大概率通话时蓝牙自动断连,RAM空间爆了问题处理
单片机·嵌入式硬件·mcu·物联网·电脑·bug
8年车载测试8 天前
车载测试中如何通过 Jira 平台提交 Bug:操作步骤与关键要点
bug·jira·uds诊断·hil测试·uds自动化测试·ecu刷写测试·canoe工具
硅基手札9 天前
SPI技术系列 13 · SPI 调试与排查
linux·运维·stm32·单片机·嵌入式硬件·bug
李永奉9 天前
杰理可视化SDK开发-【BUG】AC7006F8芯片开启一拖二和双麦通话语音算法后,设置触摸音在唤醒语音助手和通话时触摸音没作用问题
单片机·嵌入式硬件·mcu·物联网·电脑·bug
kyle~11 天前
机器人通信 --- FANUC TCP 并发请求-响应错位 Bug 复盘
tcp/ip·机器人·bug
鬼手点金13 天前
Harness 工程:模型之外的竞争力
服务器·人工智能·深度学习·机器学习·bug