Странное поведение Clang ассемблера
Я попытался скомпилировать этот макрос обнаружения переполнения движка Zend:
#define ZEND_SIGNED_MULTIPLY_LONG(a, b, lval, dval, usedval) do { \
long __tmpvar; \
__asm__( \
"mul %0, %2, %3\n" \
"smulh %1, %2, %3\n" \
"sub %1, %1, %0, asr #63\n" \
: "=X"(__tmpvar), "=X"(usedval) \
: "X"(a), "X"(b)); \
if (usedval) (dval) = (double) (a) * (double) (b); \
else (lval) = __tmpvar; \
} while (0)
И получил этот результат в сборке:
; InlineAsm Start
mul x8, x8, x9
smulh x9, x8, x9
sub x9, x9, x8, asr #63
; InlineAsm End
Компилятор использовал только 2 регистра как для ввода, так и для вывода макроса, который, по моему мнению, должен быть не менее 3 и приводить к неверному результату вычисления (например, -1 * -1). Любое предложение?
2 ответа
Код сборки глючит. Из документации GCC по расширенной asm:
Используйте модификатор ограничения "&" (см. "Модификаторы") для всех выходных операндов, которые не должны перекрывать вход. В противном случае GCC может выделить выходной операнд в том же регистре, что и несвязанный входной операнд, при условии, что ассемблерный код потребляет свои входные данные перед созданием выходных данных. Это предположение может быть ложным, если код ассемблера фактически состоит из более чем одной инструкции.
По сути, это говорит о том, что с момента записи в выходной параметр, не помеченный амперсандом, вы больше не можете использовать входные параметры, поскольку они могли быть перезаписаны.
Синтаксис разработан вокруг концепции упаковки одного insn, который читает входные данные перед записью выходных данных.
Когда вы используете несколько insns, вам часто нужно использовать модификатор early-clobber для ограничения ("=&x"
), чтобы компилятор знал, что вы записываете выходные данные или регистр чтения-записи перед чтением всех входных данных. Затем он убедится, что выходной регистр не совпадает с любым из входных регистров.
См. Также вики-тег x86 и мою коллекцию встроенных документов asm и ответов SO внизу этого ответа.
#define ZEND_SIGNED_MULTIPLY_LONG(a, b, lval, dval, usedval) do { \
long __tmpvar; \
__asm__( \
"mul %[tmp], %[a], %[b]\n\t" \
"smulh %[uv], %[a], %[b]\n\t" \
"sub %[uv], %[uv], %[tmp], asr #63\n" \
: [tmp] "=&X"(__tmpvar), [uv] "=&X"(usedval) \
: [a] "X"(a), [b] "X"(b)); \
if (usedval) (dval) = (double) (a) * (double) (b); \
else (lval) = __tmpvar; \
} while (0)
Вам действительно нужны все эти инструкции, чтобы быть внутри встроенного ассема? Вы не можете сделать long tmp = a * b
входной операнд? Тогда, если компилятор нуждается a*b
в другом месте функции, CSE может видеть это.
Вы можете убедить gcc транслировать бит знака с арифметическим сдвигом вправо, используя троичный оператор. Так что, надеюсь, вы можете уговорить компилятор сделать sub
сюда. Тогда он мог бы использовать subs
установить флаги из sub
вместо необходимости отдельного теста Insn на usedval
,
Если вы не можете заставить свой целевой компилятор создать нужный код, то обязательно попробуйте inline asm. Но будьте осторожны, я видел, что clang намного хуже, чем gcc с inline asm. Это приводит к ухудшению качества кода встроенного ассемблера на x86.