C - функция внешней сборки, возвращающая разные результаты с одним и тем же вводом - PullRequest
0 голосов
/ 04 мая 2018

У меня есть программа на 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 голосов
/ 04 мая 2018

Ошибка в том, что вы делаете это:

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 для этой функции, так как она сама не вызывает никаких функций (жаргонным термином для этого является «конечная процедура») и не нуждается в пустом месте в стеке.

Другая альтернатива, если у вас современный процессор, - использовать более новые инструкции SSE2. Это дает вам нормальные регистры вместо стека операндов, а также означает, что все вычисления фактически выполняются в float вместо 80-битного расширенного, что может быть очень важно - некоторые сложные числовые алгоритмы будут работать со сбоями, если у них больше число с плавающей запятой точность, чем ожидали дизайнеры. Поскольку вы используете 32-битный ELF ABI, однако, возвращаемое значение все еще нужно свернуть в ST (0), и нет никаких прямых инструкций перемещения между регистрами SSE и x87, вам нужно пройти через память. Я не знаю, как писать инструкции SSE2 в синтаксисе Intel, извините.

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
...