【问题标题】:C - external assembly function returning different results with the same inputC - 外部汇编函数使用相同的输入返回不同的结果
【发布时间】:2018-05-04 17:24:14
【问题描述】:

我有一个使用 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 链接。这是 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

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

【问题讨论】:

  • 您是否在 C 中重新实现了hyp(),以检查您的堆栈处理是否正确? (对我来说它看起来有点轻......)
  • 没有固定的指定顺序来评估函数的参数。可能与此有关。
  • @Someprogrammerdude Nasm函数符合cdecl,所以参数应该在ebp + 8上
  • @Attie 堆栈处理在这里应该是正确的,我在 Nasm 中运行了这样的堆栈处理程序,它们运行良好
  • 好的 - 仍然值得在 C 中实现 A)查看它是否有效(帮助确定问题的原因),B)将编译器的输出与您的 ASM 进行比较。

标签: c linux nasm


【解决方案1】:

错误是你这样做:

fstp dword[b]

那会覆盖b 的值,所以下次调用函数时,常量是错误的。在整个程序的输出中,这显示为 rightmost 评估是唯一正确的评估,因为编译器从右到左评估 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,因为它不会自行调用任何函数(术语是“叶过程”),也不需要堆栈上的任何暂存空间。

如果您拥有现代 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

【讨论】:

  • 谢谢!有效。这是大学作业的一部分,他们说所有写的都是汇编必须符合cdecl,所以我需要设置ebp。您能否详细说明为什么我不需要使用finit
  • 不使用堆栈的叶函数可以保持 ebp 不变,即使在 cdecl 中也是如此。只需检查任何体面的编译器的输出即可。
  • 嗯? cdecl,据我所知,与是否需要设置ebp无关。它与论点的位置有关。 (另外,学究式地,Linux 程序不使用 cdecl,它们使用“ELF psABI”。)您不需要使用finit,因为 FPU 在进程启动期间已经初始化。
猜你喜欢
  • 2022-01-16
  • 2012-03-23
  • 2021-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-02
相关资源
最近更新 更多