Инструкция MUL выполняет умножение. Она имеет следующую форму:
MUL Xd, Xn, Xm MUL Wd, Wn, Wm
Инструкция фактически вычисляет выражение Xd = Xn ∗ Xm
(для 32-разрядных регистров Wd = Wn ∗ Wm
). Пример применения инструкции MUL:
.global _start _start: mov x0, #5 mov x1, #2 mul x0, x0, x1 // X0 = X0 * X1 = 5 * 2 = 10 mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
Но при умножении следует учитывать ряд ограничений:
Регистры являются 64-битными, и если результат умножения больше 64 бит (например, 128-битное число), то в регистр Xd помещаются только младшие 64 разрядов числа. А старшие 64 разряда отбрасываются
В отличие от операций ADD
или SUB
для инструкции MUL нет S-версии, которая бы устанавливала флаги при переполнении.
Поэтому при умножении нельзя отследить переполенение.
У инструкции MUL нет отдельных версий, которые умножают числа со знаком и без знака.
Оба умножаемых числа должны помещаться в регистры, нельзя использовать произвольное число вне регистра, как для инструкций сложения и вычитания.
Если перемножаются два 32-битных W-регистра, то результат помещается только в W-регистр, соответственно опять же мы можем столкнуться с потерей точности при умножении, а старшие разряды будут отброшены.
Для преодоления этих ограничений архитектура ARM64 предоставляет еще 4 инструкции, которые выполняют умножение:
SMULH: при использовании в паре с инструкцией MUL получает старшие 64 разрядов результата умножения 64-битных чисел со знаком
SMULH Xd, Xn, Xm
UMULH: при использовании в паре с инструкцией MUL получает старшие 64 разрядов результата умножения беззнаковых 64-битных чисел
UMULH Xd, Xn, Xm
SMULL: выполняет умножение 32-битных чисел со знаком и возвращает в регистр Xd 64-битный результат
SMULL Xd, Wn, Wm
UMULL: выполняет умножение 32-битных чисел без знака и возвращает в регистр Xd 64-битный результат
UMULL Xd, Wn, Wm
Умножение 32-битных регистров без знака:
mov w1, #22 mov w2, #4 umull x0, w1, w2 // X0 = W1 * W2 = 22 * 4 = 88
Умножение 62-битных регистров без знака с переносом старших разрядов:
// в X2 #0x8000000000000002 mov x2, #0x00000002 movk x2, #0x8000, LSL #48 // начиная с 48-го бита mov x3, #0x0010 // умножаем X2*X3, результат в X0 и X1 mul x0, x2, x3 // В X0 младшие 64 бита - 0x0000000000000020 или 32 umulh x1, x2, x3 // В X1 старшие 64 бита - 0x0000000000000008
Умножение 62-битных регистров со знаком с переносом старших разрядов:
// в X2 #0x8000000000000002 mov x2, #0x00000002 movk x2, #0x8000, LSL #48 // начиная с 48-го бита mov x3, #0x0010 // X2 * X3 = X1:X0 mul x0, x2, x3 // В X0 младшие 64 бита - 0x0000000000000020 или 32 smulh x1, x2, x3 // В X1 старшие 64 бита - 0xFFFFFFFFFFFFFFF8
Обратите внимание на разницу между умножением без знаком и со знаком - разница в том, как интерпретируются старшие 64 разряда. Почему так происходит? Дело в том, что старший бит при умножении со знаком интепретируется как индикатор знака: если он равен 0, то число положительное, если равен 1 - то число отрицательное. Посмотрим внимательно на перемножаемые числа в регистрах.
Число в регистре X2 начинается на 8 или в двоичной системе 10008. Инструкция SMULH трактует старший бит числа как индикатор отрицательного числа. Поэтому число, которое получается в результате произведения, то же будет отрицательным (учитывая, что число в регистре X3 положительное).
В дополнение ассемблер предоставляет три инструкции, которые выполняют умножение двух чисел с последующим дополнительным умножением результата на -1:
MNEG: выполняет умножение значений 64-битных регистров и результат умножает на -1
MNEG Xd, Xn, Xm
Что фактически эквивалентно Xd = -(Xn * Xm)
SMNEGL: выполняет умножение значений 32-битных регистров со знаком и результат умножает на -1
SMNEGL Xd, Wn, Wm
UMNEGL: выполняет умножение значений 32-битных регистров без знака и результат умножает на -1
UMNEGL Xd, Wn, Wm
Пример использования инструкций:
mov x1, #0x2 mov x2, #0x3 mneg x0, x1, x2 // x0=-1*(x1 * x2) = 0xFFFFFFFFFFFFFFFA
В данном случае мы имеем следующую ситацию
-(0x0000000000000002 * 0x0000000000000003) = - (0x0000000000000006) = 0xFFFFFFFFFFFFFFFA
результат умножения числа 0x0000000000000006 на -1 (дополнение до двух) равно 0xFFFFFFFFFFFFFFFA.
Инструкция SMNEGL учитывает старший разряд числа, который трактуется как указатель знака числа:
mov w1, #0x1 mov w2, #0xfffffff3 // w2 = -13 umnegl x0, w1, w2 // 0xFFFFFFFF0000000D mov w1, #0x1 mov w2, #0xfffffff3 // w2 = -13 smnegl x0, w1, w2 // 0x000000000000000D = 13
В данном случае число в регистре W2 в качестве старшего разряда имеет значение 1. Соответственно инструкция SMNEGL трактует это число как отрицательное. Поэтому при умножении на -1 результат будет положительный (учитывая, что число в регистре W1 положительное).