Запуск In-Line Assembly в среде Linux (с использованием GCC/G++)
Итак, у меня есть очень простая программа, написанная на C (файл.c) с частью встроенного кода для сборки. Я хочу преобразовать файл.c в вывод сборки, который я знаю, но не знаю, как скомпилировать этот код для среды Linux.
При использовании gcc или g++ для файлов.cpp я получаю ошибки, не распознающие инструкции asm.
Теперь этот код работает так, как задумано в Visual Studio, кроме меня, меняя скобки для кода asm в круглых скобках. Однако я все еще получаю ошибки. Куча неопределенных ссылок на переменные.
Изменения, которые я внес в рабочий код, заключаются в переносе скобок в круглые скобки, в инструкции к ассемблеру, заключенной в кавычки (может быть неправильно найдено в Интернете)
Короче говоря, я хочу, чтобы приведенный ниже код мог успешно компилироваться в среде 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;
}
2 ответа
Встроенная сборка в GCC переводится буквально в сгенерированный источник сборки; поскольку переменные не существуют в сборке, то, что вы написали, не может работать.
Чтобы заставить его работать, нужно использовать расширенную сборку, которая аннотирует сборку модификаторами, которые GCC будет использовать для перевода сборки при компиляции исходного кода.
__asm__
(
"fld %1\n\t"
"fadd %2\n\t"
"fst %0"
: "=f" (fsum)
: "f" (num1), "f" (num2)
:
);
Встроенный ассемблер GNU C не требует инструкций по перемещению данных в начале / конце 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. Вероятно, в конечном итоге, как неоперативный 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's 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-битных систем. В итоге оба аргумента записываются в регистры, так как он загружается с одинарной точностью при подготовке к сохранению как двойного. По какой-то причине он дублирует значение в стеке FP вместо того, чтобы сохранять как double перед выполнением fadd
,
Так что в этом случае "f"
ограничение делает лучший код, чем "m"
ограничение, и я не вижу простой способ с синтаксисом AT&T, чтобы указать размер операнда одинарной точности для операнда памяти, не нарушая asm для операндов регистра. (fadds %st(1)
не собирается, но fadd (mem)
не собирается ни с лязгом. GNU as по умолчанию использует операнды памяти одинарной точности, по-видимому.) С синтаксисом 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, по-видимому, не думает так поступить. Написание встроенного ассемблера для использования faddp
заставляет GCC делать дополнительные fld %st(1)
перед faddp
вместо того, чтобы убедить его сохранить double
Аргументы для printf, прежде чем делать добавление.
Еще лучше было бы, если бы хранилища одинарной точности были настроены так, чтобы они могли быть аргументами для type-pun printf, вместо того, чтобы снова копировать для этого. Если бы вы писали эту функцию вручную, я бы сохранял результаты в scanf в местах, которые работают как аргументы для printf.