Запуск встроенной сборки в среде 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.