首页 > 解决方案 > C - 外部汇编函数使用相同的输入返回不同的结果

问题描述

我有一个使用 NASM 函数的 C 语言程序。这是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 链接。这是生成文件:

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

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

标签: clinuxnasm

解决方案


错误是你这样做:

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

您不需要发出 a finit,因为 ABI 保证这在进程启动期间已经完成。您不需要为此函数设置 EBP,因为它本身不会调用任何函数(术语是“叶过程”),也不需要堆栈上的任何暂存空间。

如果您有现代 CPU,另一种选择是使用较新的 SSE2 指令。这为您提供了普通寄存器而不是操作数堆栈,也意味着计算实际上都是在float而不是 80 位扩展中完成的,这可能非常重要——如果某些复杂的数值算法的浮点精度高于设计师期望的。但是,因为您使用的是 32 位 ELF ABI,返回值仍然需要在 ST(0) 中结束,并且 SSE 和 x87 寄存器之间没有直接移动指令,您必须通过内存。我不知道如何用 Intel 语法编写 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

推荐阅读