C - 外部装配功能使用相同的输入返回不同的结果

时间:2018-05-04 17:24:14

标签: c linux nasm

我在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

最后一个数字是正确的。我的问题是:即使输入相同,为什么这些结果会有所不同?

1 个答案:

答案 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