Как получить доступ к структуре / переменным C из встроенного asm?
Рассмотрим следующий код:
int bn_div(bn_t *bn1, bn_t *bn2, bn_t *bnr)
{
uint32 q, m; /* Division Result */
uint32 i; /* Loop Counter */
uint32 j; /* Loop Counter */
/* Check Input */
if (bn1 == NULL) return(EFAULT);
if (bn1->dat == NULL) return(EFAULT);
if (bn2 == NULL) return(EFAULT);
if (bn2->dat == NULL) return(EFAULT);
if (bnr == NULL) return(EFAULT);
if (bnr->dat == NULL) return(EFAULT);
#if defined(__i386__) || defined(__amd64__)
__asm__ (".intel_syntax noprefix");
__asm__ ("pushl %eax");
__asm__ ("pushl %edx");
__asm__ ("pushf");
__asm__ ("movl %eax, (bn1->dat[i])");
__asm__ ("xorl %edx, %edx");
__asm__ ("divl (bn2->dat[j])");
__asm__ ("movl (q), %eax");
__asm__ ("movl (m), %edx");
__asm__ ("popf");
__asm__ ("popl %edx");
__asm__ ("popl %eax");
#else
q = bn->dat[i] / bn->dat[j];
m = bn->dat[i] % bn->dat[j];
#endif
/* Return */
return(0);
}
Типы данных uint32 в основном представляют собой unsigned long int или 32-разрядное целое число без знака uint32_t. Тип bnint является либо беззнаковым коротким int (uint16_t), либо uint32_t в зависимости от того, доступны или нет 64-битные типы данных. Если доступно 64-битное значение, то bnint - это uint32, в противном случае - uint16. Это было сделано для того, чтобы захватить перенос / переполнение в других частях кода. Структура bn_t определяется следующим образом:
typedef struct bn_data_t bn_t;
struct bn_data_t
{
uint32 sz1; /* Bit Size */
uint32 sz8; /* Byte Size */
uint32 szw; /* Word Count */
bnint *dat; /* Data Array */
uint32 flags; /* Operational Flags */
};
Функция начинается в строке 300 в моем исходном коде. Поэтому, когда я пытаюсь скомпилировать / сделать это, я получаю следующие ошибки:
system:/home/user/c/m3/bn 1036 $$$ ->make
clang -I. -I/home/user/c/m3/bn/.. -I/home/user/c/m3/bn/../include -std=c99 -pedantic -Wall -Wextra -Wshadow -Wpointer-arith -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnested-externs -Wwrite-strings -Wfloat-equal -Winline -Wunknown-pragmas -Wundef -Wendif-labels -c /home/user/c/m3/bn/bn.c
/home/user/c/m3/bn/bn.c:302:12: warning: unused variable 'q' [-Wunused-variable]
uint32 q, m; /* Division Result */
^
/home/user/c/m3/bn/bn.c:302:15: warning: unused variable 'm' [-Wunused-variable]
uint32 q, m; /* Division Result */
^
/home/user/c/m3/bn/bn.c:303:12: warning: unused variable 'i' [-Wunused-variable]
uint32 i; /* Loop Counter */
^
/home/user/c/m3/bn/bn.c:304:12: warning: unused variable 'j' [-Wunused-variable]
uint32 j; /* Loop Counter */
^
/home/user/c/m3/bn/bn.c:320:14: error: unknown token in expression
__asm__ ("movl %eax, (bn1->dat[i])");
^
<inline asm>:1:18: note: instantiated into assembly here
movl %eax, (bn1->dat[i])
^
/home/user/c/m3/bn/bn.c:322:14: error: unknown token in expression
__asm__ ("divl (bn2->dat[j])");
^
<inline asm>:1:12: note: instantiated into assembly here
divl (bn2->dat[j])
^
4 warnings and 2 errors generated.
*** [bn.o] Error code 1
Stop in /home/user/c/m3/bn.
system:/home/user/c/m3/bn 1037 $$$ ->
Что я знаю:
Я считаю себя достаточно хорошо разбирающимся в ассемблере x86 (как видно из кода, который я написал выше). Однако последний раз, когда я смешивал язык высокого уровня и ассемблер, использовал Borland Pascal около 15-20 лет назад при написании графических драйверов для игр (до Windows 95). Мое знакомство с синтаксисом Intel.
Что я не знаю:
Как я могу получить доступ к членам bn_t (особенно *dat) из asm? Поскольку * dat - указатель на uint32, я обращаюсь к элементам в виде массива (например, bn1->dat[i]).
Как получить доступ к локальным переменным, которые объявлены в стеке?
Я использую push / pop для восстановления забитых регистров до их предыдущих значений, чтобы не нарушать компилятор. Однако нужно ли также включать ключевое слово volatile в локальные переменные?
Или есть лучший способ, о котором я не знаю? Я не хочу помещать это в отдельный вызов функции из-за накладных расходов на вызов, так как эта функция критична к производительности.
Дополнительно:
Прямо сейчас я только начинаю писать эту функцию, поэтому она не завершена. Отсутствуют петли и другие подобные коды поддержки / склеивания. Но основная суть заключается в доступе к локальным переменным / элементам структуры.
РЕДАКТИРОВАТЬ 1:
Синтаксис, который я использую, кажется, единственный, который поддерживает Clang. Я попробовал следующий код и Clang дал мне всевозможные ошибки:
__asm__ ("pushl %%eax",
"pushl %%edx",
"pushf",
"movl (bn1->dat[i]), %%eax",
"xorl %%edx, %%edx",
"divl ($0x0c + bn2 + j)",
"movl %%eax, (q)",
"movl %%edx, (m)",
"popf",
"popl %%edx",
"popl %%eax"
);
Он хочет, чтобы я поместил закрывающую скобку в первой строке, заменив запятую. Я переключился на использование %% вместо%, потому что где-то читал, что для встроенной сборки требуется %% для обозначения регистров процессора, и clang говорил мне, что я использовал недопустимую escape-последовательность.
1 ответ
Если вам нужно только 32b / 32b => 32-битное деление, пусть компилятор использует оба выхода div
, что gcc, clang и icc все отлично работает, как вы можете видеть в проводнике компилятора Godbolt:
uint32_t q = bn1->dat[i] / bn2->dat[j];
uint32_t m = bn1->dat[i] % bn2->dat[j];
Компиляторы неплохо разбираются в CSE div
, Просто убедитесь, что вы не сохраните результат деления где-то, что gcc не может доказать, что это не повлияет на ввод остатка.
например *m = dat[i] / dat[j]
может перекрываться (псевдоним) dat[i]
или же dat[j]
, поэтому gcc должен был бы перезагрузить операнды и повторить div
для %
операция. Смотрите ссылку на Godbolt для плохих / хороших примеров.
Использование встроенного asm для 32-битного / 32-битного = 32-битного div ничего вам не даст, а на самом деле делает код хуже с помощью clang (см. Ссылку на Godbolt).
Если вам нужно 64bit / 32bit = 32bit, вам, вероятно, понадобится asm, если для него нет встроенного компилятора. (У GNU C его нет, AFAICT). Очевидный способ в C (приведение операндов к uint64_t
) генерирует вызов 64-битной /64-битной = 64-битной функции libgcc, которая имеет ветви и несколько div
инструкции. GCC не очень хорошо доказывает, что результат уместится в 32 бита, поэтому один div
инструкция не вызывает #DE
,
Для многих других инструкций вы можете избежать написания встроенных ассемблеров в большинстве случаев с помощью встроенных функций для таких вещей, как popcount. С -mpopcnt
это компилируется в popcnt
инструкция (и учитывает ложную зависимость от выходного операнда, который есть у процессоров Intel.) Без этого она компилируется в вызов функции libgcc.
Всегда предпочитайте встроенные функции или чистый C, который компилируется в хороший asm, поэтому компилятор знает, что делает код. Когда встраивание делает некоторые аргументы известными во время компиляции, чистый C может быть оптимизирован или упрощен, но код, использующий встроенный asm, просто загрузит константы в регистры и выполнит div
во время выполнения. Inline asm также побеждает CSE между аналогичными вычислениями на одних и тех же данных и, конечно, не может автоматически векторизовать.
Правильное использование синтаксиса GNU C
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html объясняет, как указать ассемблеру, какие переменные вы хотите в регистрах, и каковы выходные данные.
Вы можете использовать Intel/MASM-подобный синтаксис и мнемонику, а также имена регистров, отличные от%, если хотите, предпочтительно путем компиляции с -masm=intel
, Синтаксическая ошибка AT&T (fsub
а также fsubr
мнемоника перевернута) может все еще присутствовать в режиме синтаксиса intel; Я забыл.
Большинство программных проектов, использующих встроенный ассемблер GNU C, используют только синтаксис AT&T.
См. Также нижнюю часть этого ответа для получения дополнительной информации о встроенном ассемблере GNU C и вики-тега x86.
asm
Оператор принимает одну строковую аргументацию и 3 набора ограничений. Самый простой способ сделать его многострочным, сделать каждую строку asm отдельной строкой, заканчивающейся на \n
и пусть компилятор неявно объединяет их.
Кроме того, вы сообщаете компилятору, в какие регистры вы хотите что-то добавить. Затем, если переменные уже есть в регистрах, компилятору не нужно их разливать, и вам нужно загрузить и сохранить их. Делая это, вы действительно застрелились бы в ногу. Надеемся, что учебник Бретт Хейл, связанный в комментариях, охватывает все это.
Правильный пример div
с встроенным ассемблером GNU C
Вы можете увидеть вывод asm компилятора для этого на godbolt.
uint32_t q, m; // this is unsigned int on every compiler that supports x86 inline asm with this syntax, but not when writing portable code.
asm ("divl %[bn2dat_j]\n"
: "=a" (q), "=d" (m) // results are in eax, edx registers
: "d" (0), // zero edx for us, please
"a" (bn1->dat[i]), // "a" means EAX / RAX
[bn2dat_j] "mr" (bn2->dat[j]) // register or memory, compiler chooses which is more efficient
: // no register clobbers, and we don't read/write "memory" other than operands
);
"divl %4"
тоже бы сработало, но именованные входы / выходы не меняют имя, когда вы добавляете больше ограничений ввода / вывода.