Запуск встроенной сборки в среде Linux (с использованием GCC/g++)

Итак, у меня есть очень простая программа, написанная в C (.c файле) с частью сборки в сборе. Я хочу преобразовать файл .c в сборку, который я знаю, но не знаю, как скомпилировать этот код для среды Linux.

При использовании gcc или g++ для .cpp файлов я получаю ошибки, не распознающие инструкции asm.

Теперь этот код работает так же, как и в Visual Studio, кроме меня, меняя скобки для кода asm в скобки. Однако я все еще получаю ошибки. Связка undefined ссылок на переменные.

Изменения, которые я сделал из рабочего кода, меняют скобки в круглые скобки, ввод инструкции сборки в кавычки (найденный в Интернете, может быть неправильным).

Короче говоря, я хочу, чтобы код, приведенный ниже, мог быть скомпилирован успешно в среде Linux, используя команду gcc. Я не знаю синтаксиса, но код работает, просто не для linux/.

#include <stdio.h>
int main()

{

float num1, num2, sum, product;
float sum, product;
float f1, f2, f3, fsum, fmul;

printf("Enter two floating point numbers: \n");
scanf("%f %f", &num1, &num2);


__asm__ 
(
    "FLD num1;"
    "FADD num2;"
    "FST fsum;"
);

printf("The sum of %f and %f " "is" " %f\n", num1, num2, fsum);
printf("The hex equivalent of the numbers and sum is %x + %x = %x\n", num1, num2, fsum);

return 0;
}

Ответы

Ответ 1

Встраиваемая сборка в GCC переводится буквально в сгенерированный источник сборки; так как переменные не существуют в сборке, никоим образом не может работать то, что вы написали.

Способ заставить работать: расширенная сборка, которая комментирует сборку с помощью модификаторов, которые GCC будет использовать для перевода сборки, когда источник скомпилирован.

__asm__
(
  "fld %1\n\t"
  "fadd %2\n\t"
  "fst %0"
  : "=f" (fsum)
  : "f" (num1), "f" (num2)
  :
);

Ответ 2

GNU C inline asm предназначен для того, чтобы не требовать инструкций перемещения данных в начале/конце инструкции asm. Каждый раз, когда вы пишете mov или fld или что-то в качестве первой инструкции в inline asm, вы побеждаете цель системы ограничений. Вы должны просто попросить компилятор разместить данные, в которых вы хотели, в первую очередь.

Кроме того, запись нового кода x87 в 2016 году обычно является пустой тратой времени. Это странно и сильно отличается от обычного способа выполнения математики FP (скалярные или векторные инструкции в xmm-регистрах). Вероятно, вы получите лучшие результаты, переведя древний код asm в чистый C, если он был настроен вручную для самых разных микроархитектур или не использует команды SSE. Если вы все еще хотите написать код x87, обратитесь к руководству в x86.

Если вы пытаетесь изучить asm с помощью встроенного asm GNU C, просто не делайте этого. Выберите любой другой способ изучения asm, например. записывая целые функции и вызывая их из C. См. также нижнюю часть этого ответа для коллекции ссылок на запись хорошего GNU C inline asm.


специальные правила для операндов с плавающей запятой x87, потому что стек регистра x87 не является произвольным доступом. Это делает inline-asm еще более сложным в использовании, чем это уже происходит для "нормального" материала. Также лучше получить оптимальный код.


В нашем случае мы знаем, что нам нужен один входной операнд в верхней части стека FP, и мы получим наш результат. Просить компилятор сделать это для нас означает, что нам не нужны инструкции за пределами fadd.

  asm (
    "fadd %[num2]\n\t"
    : "=t" (fsum)                                  // t is the top of the register stack
    : [num1] "%0" (num1), [num2] "f" (num2)         // 0 means same reg as arg 0, and the % means they're commutative.  gcc doesn't allow an input and output to both use "t" for somre reason.  For integer regs, naming the same reg for an input and an output works, instead of using "0".
    : // "st(1)"  // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
      // This is optimal for this context, but in other cases faddp would be better
      // we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
  );

Обратитесь к документам для модификаторов ограничений для объяснения %0.

До fadd: num2 находится %st(0). num1 находится либо в памяти, либо в другом регистре стека FP. Компилятор выбирает, который и заполняет имя регистра или эффективный адрес.

Это, надо надеяться, заставит компилятор вытащить стек потом правильное количество раз. (Обратите внимание, что fst %0 был довольно глупым, когда ограничение вывода должно было быть регистром стека FP. Скорее всего, это будет no-op вроде fst %st(0) или что-то в этом роде.)

Я не вижу простого способа оптимизировать это, чтобы использовать faddp, если оба значения FP уже находятся в регистре %st. например faddp %st1 был бы идеальным, если бы num1 находился в %st1 раньше, но не был необходим в регистре FP.


Здесь полная версия, которая фактически компилируется и работает даже в режиме 64 бит, так как я написал для вас функцию обертки типа. Это необходимо для любого ABI, который передает некоторые аргументы FP в регистры FP для функций varargs.

#include <stdio.h>
#include <stdint.h>

uint32_t pun(float x) {
  union fp_pun {
    float single;
    uint32_t u32;
  } xu = {x};
  return xu.u32;
}


int main()
{
  float num1, num2, fsum;

  printf("Enter two floating point numbers: \n");
  scanf("%f %f", &num1, &num2);

  asm (
    "fadd %[num2]\n\t"
    : "=t" (fsum)
    : [num1] "%0" (num1), [num2] "f" (num2)  // 0 means same reg as arg 0, and the % means it commutative with the next operand.  gcc doesn't allow an input and output to both use "t" for some reason.  For integer regs, naming the same reg for an input and an output works, instead of using "0".
    : // "st(1)"  // we *don't* pop the num2 input, unlike the FYL2XP1 example in the gcc manual
      // This is optimal for this context, but in other cases faddp would be better
      // we don't need an early-clobber "=&t" to prevent num2 from sharing a reg with the output, because we already have a "0" constraint
  );

  printf("The sum of %f and %f is %f\n", num1, num2, fsum);
  // Use a union for type-punning.  The %a hex-formatted-float only works for double, not single
  printf("The hex equivalent of the numbers and sum is %#x + %#x = %#x\n",
         pun(num1), pun(num2), pun(fsum));

  return 0;
}

Посмотрите, как он компилируется в Godbolt Compiler Explorer.

Выньте -m32, чтобы посмотреть, насколько глупо получать данные в регистры x87 только для одного добавления, в обычном коде, использующем SSE для математики FP. (особенно, поскольку они также должны быть преобразованы в двойную точность для printf после scanf дает нам одинарную точность.)

gcc заканчивается тем, что делает довольно неэффективный внешний вид x87-кода для 32-битного. Он заканчивается тем, что имеет оба аргумента в regs, поскольку он загружал его с одной точностью при подготовке к хранению в виде двойной. По какой-то причине он дублирует значение в стеке FP вместо сохранения как double перед выполнением fadd.

Итак, в этом случае ограничение "f" делает лучший код чем ограничение "m", и я не вижу простого способа с синтаксисом AT & T указывать размер операнда одной точности для операнда памяти без нарушения asm для регистровых операндов. (fadds %st(1) не собирается, но fadd (mem) не собирается ни с clang. По умолчанию GNU по умолчанию является операндом памяти с одинарной точностью.) С синтаксисом Intel модифицированный размер операнда прикрепляется к операнду памяти, и будет там, если компилятор выбирает операнд памяти, иначе нет.

Во всяком случае, эта последовательность будет лучше, чем то, что gcc испускает, потому что она избегает fld %st(1):

    call    __isoc99_scanf
    flds    -16(%ebp)
    subl    $12, %esp      # make even more space for args for printf beyond what was left after scanf
    fstl    (%esp)         # (double)num1

    flds    -12(%ebp)
    fstl    8(%esp)        # (double)num2

    faddp  %st(1)          # pops both inputs, leaving only fsum in %st(0)
    fsts    -28(%ebp)      # store the single-precision copy
    fstpl   16(%esp)       # (double)fsum
    pushl   $.LC3
    call    printf

Но gcc не думает делать это так, видимо. Написание встроенного asm для использования faddp делает gcc do extra fld %st(1) перед faddp вместо того, чтобы убеждать его в хранении аргументов double для printf перед выполнением добавления.

Еще лучше было бы, если бы хранилища с одной точностью были настроены так, чтобы они могли быть аргументами для print-print-типа, вместо того, чтобы снова копироваться для этого. Если вы записываете функцию вручную, у меня будут результаты сканирования scanf в местах, которые работают как args для printf.