我在C中有一个使用NASM功能的程序。这是C程序的代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
extern float hyp(float a); // supposed to calculate 1/(2 - a) + 6
void test(float (*f)(float)){
printf("%f %f %f\n", f(2.1), f(2.1), f(2.1));
}
void main(int argc, char** argv){
for(int i = 1; i < argc; i++){
if(!strcmp(argv[i], "calculate")){
test(hyp);
}
}
}
这是NASM功能:
section .data
a dd 1.0
b dd 2.0
c dd 6.0
section .text
global hyp
hyp:
push ebp
mov ebp, esp
finit
fld dword[b]
fsub dword[ebp + 8]
fstp dword[b]
fld dword[a]
fdiv dword[b]
fadd dword[c]
mov esp, ebp
pop ebp
ret
这些程序在Linux中与gcc和nasm链接。这是Makefile:
all: project clean
main.o: main.c
gcc -c main.c -o main.o -m32 -std=c99
hyp.o: hyp.asm
nasm -f elf32 -o hyp.o hyp.asm -D UNIX
project: main.o hyp.o
gcc -o project main.o hyp.o -m32 -lm
clean:
rm -rf *.o
程序运行时,输出:
5.767442 5.545455 -4.000010
最后一个数字是正确的。我的问题是:即使输入相同,为什么这些结果会有所不同?
答案 0 :(得分:2)
错误是你这样做:
fstp dword[b]
这会覆盖b
的值,所以下次调用该函数时,常量是错误的。在整个程序的输出中,这显示为最右边的评估是唯一正确的评估,因为编译器从右到左评估了printf
的参数。 (允许在它想要的任何顺序中评估多参数函数的参数。)
您应该使用.rodata
部分作为常量;然后程序会崩溃而不是覆盖常量。
您可以使用fdivr
代替fdiv
来避免存储和重新加载中间值。
hyp:
fld DWORD PTR [b]
fsub DWORD PTR [esp+4]
fdivr DWORD PTR [a]
fadd DWORD PTR [c]
ret
或者,做一个Forth程序员会做的事情,并在其他所有事情之前加载常量1,所以当它需要时它在ST(1)中。这允许您使用fld1
而不是将1.0放入内存。
hyp:
fld1
fld DWORD PTR [b]
fsub DWORD PTR [esp+4]
fdivp
fadd DWORD PTR [c]
ret
您不需要发出finit
,因为ABI保证在流程启动期间已经完成此操作。您不需要为此函数设置EBP,因为它不会使任何函数调用本身(行话术语是&#34; leaf procedure&#34;),也不需要堆栈上的任何临时空间。
另一种选择,如果你有一个现代CPU,是使用较新的SSE2指令。这给你正常的寄存器而不是操作数堆栈,也意味着计算都是在float
而不是80位扩展中实际完成的,这可能非常重要 - 一些复杂的数值算法如果有更多浮动就会出现故障精度高于设计师的预期。但是,因为您使用的是32位ELF ABI,返回值仍需要在ST(0)中结束,并且SSE和x87寄存器之间没有直接移动指令,您必须去通过记忆。我不知道如何用英特尔语法编写SSE2指令,抱歉。
hyp:
subl $4, %esp
movss b, %xmm1
subss 8(%esp), %xmm1
movss a, %xmm0
divss %xmm1, %xmm0
addss c, %xmm0
movss %xmm0, (%esp)
flds (%esp)
addl $4, %esp
ret
在64位ELF ABI中,XMM0中的浮点返回值(默认情况下参数也在寄存器中传递),这只是
hyp:
movss b(%rip), %xmm1
subss %xmm0, %xmm1
movss a(%rip), %xmm0
divss %xmm1, %xmm0
addss c(%rip), %xmm0
ret