前言
笔者最近闲的无聊,学习一些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/win64
https://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-000000
https://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
总结
至于为什么是这样的,笔者也不知道。