Инструкции (v)pcmpeqb, (v)pcmpeqw, (v)pcmpeqd, (v)pcmpeqq, (v)pcmpgtb, (v)pcmpgtw, (v)pcmpgtd
и (v)pcmpgtq
сравнивают целые числа в соответствующих дорожках операндов. Результат сравнения сохраняется в первом операнде.
Инструкции pcmpeqX
, сравнивают на равенство соответственно два байта, два слова, два двойных и два четверных слова из соответствуюших дорожек.
pcmpeqb xmmdest, xmmsrc/mem128 ; сравнивает 16 байтовых дорожек pcmpeqw xmmdest, xmmsrc/mem128 ; сравнивает 8 дорожек с числами word pcmpeqd xmmdest, xmmsrc/mem128 ; сравнивает 4 дорожек с числами dword pcmpeqq xmmdest, xmmsrc/mem128 ; сравнивает 2 дорожек с числами qword
Инструкции устанавливают значение всех битов 1, если два значения в одной дорожке равны. Если значения не равны, то устанавливается 0 для всех бит. Например, возьмем следующую программу на Linux:
global main extern printf section .data nums0 dd 1, 2, 4, 8 nums1 dd 1, 2, 3, 8 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums0] movaps xmm1, [nums1] pcmpeqd xmm0, xmm1 ; XMM0 = XMM0 == XMM1 ; XMM0 = -1, -1, 0, -1 ; выводим данные на консоль movd esi, xmm0 psrldq xmm0, 4 movd edx, xmm0 psrldq xmm0, 4 movd ecx, xmm0 psrldq xmm0, 4 movd r8d, xmm0 mov rdi, format_str call printf add rsp, 8 ret
Здесь сравниваются соответствуюшие элементы векторов из XMM0 и XMM1, которые представляют 32-разрядные целые числа. Для упрощения результат сравнения с помощью функции printf языка Си выводится на консоль. Поскольку первые два и последний элементы обоих векторов равны, а третьи элементы не равны,
то в результате получим вектор 0xFFFFFFFF, 0xFFFFFFFF, 0, 0
. Компиляция и консольный вывод программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# gcc -static hello.o -o hello root@Eugene:~/asm# ./hello -1, -1, 0, -1 root@Eugene:~/asm#
Аналогичный пример на Windows:
global main extern printf section .data nums0 dd 1, 2, 4, 8 nums1 dd 1, 2, 3, 8 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 40 movaps xmm0, [rel nums0] movaps xmm1, [rel nums1] pcmpeqd xmm0, xmm1 ; XMM0 = XMM0 == XMM1 ; XMM0 = -1, -1, 0, -1 movd edx, xmm0 ; помещаем первое число в rdx psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd r8d, xmm0 ; помещаем первое число в r8 psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd r9d, xmm0 ; помещаем первое число в r9 psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd [rsp+32], xmm0 ; помещаем первое число в стек mov rcx, format_str call printf add rsp, 40 ret
Инструкции pcmpgtX
проверят, больше ли первый операнд чем второй:
pcmpgtb xmmdest, xmmsrc/mem128 ; сравнивает байты в 16 дорожках pcmpgtw xmmdest, xmmsrc/mem128 ; сравнивает числа word в 8 дорожках pcmpgtd xmmdest, xmmsrc/mem128 ; сравнивает числа dword в 4 дорожках pcmpgtq xmmdest, xmmsrc/mem128 ; сравнивает числа qword в 2 дорожках
Инструкции устанавливают значение всех битов в 1, если значение из первого операнда больше значения из второго. Иначе устанавливается 0 для всех бит. Пример применения на Linux:
global main extern printf section .data nums0 dd 0, 2, 4, 5 nums1 dd 0, 1, 2, 7 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums0] movaps xmm1, [nums1] pcmpgtd xmm0, xmm1 ; XMM0 = XMM0 > XMM1 ; XMM0 = 0, -1, -1, 0 ; выводим данные на консоль movd esi, xmm0 psrldq xmm0, 4 movd edx, xmm0 psrldq xmm0, 4 movd ecx, xmm0 psrldq xmm0, 4 movd r8d, xmm0 mov rdi, format_str call printf add rsp, 8 ret
128-варианты инструкций vpcmpeqX
и vpcmpgtX
работают аналогично SSE-инструкциям, только сравнивают второй и третий операнды и результат помещают в первый операнд.
vpcmpeqb xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает байты в 16 дорожках vpcmpeqw xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа word в 8 дорожках vpcmpeqd xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа dword в 4 дорожках vpcmpeqq xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа qword в 2 дорожках vpcmpgtb xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает байты в 16 дорожках vpcmpgtw xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа word в 8 дорожках vpcmpgtd xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа dword в 4 дорожках vpcmpgtq xmmdest, xmmsrc1, xmmsrc2/mem128 ; сравнивает числа qword в 2 дорожках
256-разрядные варианты инструкций vpcmpeqX
и vpcmpgtX
сравнивают в два раза больше дорожек в 256-битных операндах:
vpcmpeqb ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает байты в 32 дорожках vpcmpeqw ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа word в 16 дорожках vpcmpeqd ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа dword в 8 дорожках vpcmpeqq ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа qword в 4 дорожках vpcmpgtb ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает байты в 32 дорожках vpcmpgtw ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа word в 16 дорожках vpcmpgtd ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа dword в 8 дорожках vpcmpgtq ymmdest, ymmsrc1, ymmsrc2/mem256 ; сравнивает числа qword в 4 дорожках
Сравнение целых чисел со знаком SSE/AVX не влияет на флаги состояния (поскольку они сравнивают несколько значений, и только одно из этих сравнений может быть перемещено во флаги).
Инструкция (v)pmovmskb позволяет проверить результат сравнения. Она извлекает старший бит из всех байтов в регистре XMM или YMM и сохраняет 16 или 32 бита (соответственно) в регистре общего назначения. Эта инструкция устанавливают все старшие биты регистра общего назначения в 0 (помимо тех, которые необходимы для хранения битов маски):
pmovmskb reg, xmm vpmovmskb reg, xmm vpmovmskb reg, ymm
Первым операндом инструкции является 32- или 64-битный регистр общего назначения.
Инструкция (v)pmovmskb
копирует знаковые биты из каждой байтовой дорожки в соответствующую битовую позицию регистра общего назначения. Так, она копирует бит 7
из регистра XMM (бит знака для дорожки 0) в бит 0 регистра назначения; она копирует бит 15 из регистра XMM (бит знака для дорожки 1) в бит 1 регистра назначения,
бит 23 из регистра XMM (бит знака для дорожки 2) в бит 2 регистра назначения и так далее.
128-битные инструкции заполняют только биты с 0 по 15 регистра назначения (обнуляя все остальные биты). 256-битная форма инструкции vpmovmskb
заполняет биты с 0 по 31 целевого регистра (при использовании 64-битного регистра все остальные биты обнуляются)
Можно использовать инструкцию pmovmskb для извлечения одного бита из каждой байтовой дорожки в регистре XMM или YMM после инструкций (v)pcmpeqb
или (v)pcmpgtb
.
Например, получим результат сравнения в программе на Linux:
global main extern printf section .data nums0 dd 0, 2, 4, 5 nums1 dd 0, 1, 2, 7 format_str db "%#x", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums0] movaps xmm1, [nums1] pcmpgtd xmm0, xmm1 ; XMM0 = XMM0 > XMM1 ; XMM0 = 0, -1, -1, 0 pmovmskb esi, xmm0 ; выводим данные на консоль mov rdi, format_str call printf add rsp, 8 ret
Вывод программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# gcc -static hello.o -o hello root@Eugene:~/asm# ./hello 0xff0 root@Eugene:~/asm#
При сравнении регистр xmm0 хранит следующий вектор: 0, -1, -1, 0
. То есть первые 4 байта равны 0, следующие 8 байт равны -1, и последние 4 байта равны 0.
При выполнении инструкции pmovmskb esi, xmm0
страшие биты из первых 4 байтов xmm0 (первое число dword) копируются в первые 4 бита в esi,
страшие биты из вторых 4 байтов xmm0 (второе число dword) копируются во вторые 4 бита в esi и так далее. В итоге в rsi будет хранится значение 0x0ff0
-
каждая шестнадцатеричная цифра будет представлять старшие биты одного 4-байтного числа из xmm0. И таким образом, по числу 0x0ff0
мы можем увидеть, что в xmm0 хранится вектор чисел
0, -1, -1, 0
(или 0, 0xffffffff, 0xffffffff, 0
)
Подобные инструкции удобно применять для сравнения строк. Например, возьмем следующую программу на Linux:
global main extern printf section .data str0 db "Hello Word",0 align 16 str1 db "Hello Word", 0 align 16 mask dw 0xffff equal_str db "String are equal", 10, 0 notequal_str db "String are not equal", 10, 0 section .text main: sub rsp, 8 movdqa xmm0, [str0] movdqa xmm1, [str1] pcmpeqb xmm0, xmm1 ; сравниваем байты (символы) строк vpmovmskb edi, xmm0 ; копируем результат в rdi (поскольку 16 битов сравнения, результат в DI) cmp di, [mask] ; с помощью маски проверяем равны ли строки jz equal ; если результат - 0, строки равны mov rdi, notequal_str ; если строки не равны jmp exit equal: mov rdi, equal_str ; если строки равны exit: call printf add rsp, 8 ret
Здесь сравниваем побайтно 16 первых символов двух строки - str1 и str2 с помощью инструкции pcmpeqb
.
pcmpeqb xmm0, xmm1
После сравнения копируем результат в регистр rdi:
vpmovmskb edi, xmm0
Поскольку из xmm0 в edi копируется 16 бит, то в дальнейшем проверяем младшие 16 бит регистра edi (регистр di) на соответствие маске:
cmp di, [mask]
Маска mask представляет 2 байта, где все биты установлены в 1. То есть маска предполагает ситуацию, когда все 16 байтов регистра xmm0 или все 16 битов регистра di равны 1. И если маска равна числу из di, то две строки равны.
Аналогичный пример на Windows:
global main extern printf section .data str0 db "Hello Word",0 align 16 str1 db "Hello Word", 0 align 16 mask dw 0xffff equal_str db "String are equal", 10, 0 notequal_str db "String are not equal", 10, 0 section .text main: sub rsp, 40 movdqa xmm0, [rel str0] movdqa xmm1, [rel str1] pcmpeqb xmm0, xmm1 ; сравниваем байты (символы) строк vpmovmskb edi, xmm0 ; копируем результат в rdi (поскольку 16 битов сравнения, результат в DI) cmp di, [rel mask] ; с помощью маски проверяем равны ли строки jz equal ; если результат - 0, строки равны mov rcx, notequal_str ; если строки не равны jmp exit equal: mov rcx, equal_str ; если строки равны exit: call printf add rsp, 40 ret
Операции сравнения открывают нам и другую возможность - манипуляции с символами по условию. Например, нам надо перевести все строчные буквы в строке в верхний регистр. С точки зрения таблицы ASCII строчная буква отличается от заглавной тем, что у нее установлен 5-й бит (нумерация битов с нуля). Например:
Код буквы "A": 01000001 Код буквы "a": 01100001
То есть, чтобы перейти от буквы в верхнем регистре к букве в нижнем регистре, нам надо установить бит 5. А для обратного перехода - снять 5-й бит. Однако строка может содержать не только алфавитные символы, но и другие символы - цифры, скобки и т.д., в том числе непечатные символы типа перевода строки (символ \n). Поэтому просто снять или установить для всех символов строки определенный бит не получится. Нам надо манипулировать с битом только тогда, когда символ представляет букву. И для этого нам как раз нужны инструкции сравнения.
Так, определим следующую программу:
global _start section .data msg db "Hello World", 10, 0 msglen equ $-msg ; длина строки align 16 maskgeA times 16 db 96 ; код символа равен букве "a" или больше maskleZ times 16 db 123 ; код символа равен букве "z" или меньше mask20 times 16 db 0b00100000 ; для установки маски, чтобы убрать 5-й бит section .text _start: movdqa xmm0, [msg] ; помещаем строку для преобразования в регистр xmm0 vpcmpgtb xmm1, xmm0, [maskgeA] ; находим символы, код которых равен "a" или больше - результат в xmm1 movdqa xmm2, [maskleZ] vpcmpgtb xmm2, xmm2, xmm0 ; находим символы, код которых равен "z" или меньше - результат в xmm2 vandpd xmm1, xmm1, xmm2 ; находим все строчные символы путем умножения результатов обоих сравнений ; в xmm1 единицы в тех байтах, где в строке строчные буквы andpd xmm1, [mask20] ; получаем маску для перевода из нижнего регистра в верхний vandnpd xmm0, xmm1, xmm0 ; применяем маску к строке - убираем 5-й бит movdqa [msg], xmm0 ; сохраняем строку обратно в переменную msg ; собственно печать строки с помощью системного вызова write mov rax, 1 ; номер системной функции mov rdi, 1 ; дескриптор стандартного (консольного) вывода mov rsi, msg ; адрес строки mov rdx, msglen ; размер строки syscall ; выполняем системный вызов mov rdi, msglen mov rax, 60 syscall ; выход из программы
Итак, наша оригинальная строка - msg. Ее мы будем переводить в верхний регистр, то есть делать все буквы заглавными. И для этого определяем ряд масок. Первая маска - maskgeA
maskgeA times 16 db 96 ; код символа равен букве "a" или больше
Это массив из 16 байтов, каждый из которых равен 96 - это код ascii косой кавычки "`", после которого идет символ "a". Мы будем сравнивать каждый символ строки с этим байтом. Если код символа в строке больше этого байта, то мы скорее всего имеем дело со строчной буквой.
Но после строчных букв в таблице символов еще есть символы. И чтобы ограничить верхний потолок и найти все символы, код которых равен "z" или меньше, определяем вторую маску - maskleZ
maskleZ times 16 db 123
123 - это код символа "{", до которого идет код символа "z".
Третья маска нужна, чтобы сбросить 5-й бит, поэтому каждый ее байт содержит число 32 (число, где установлен только 5-й бит - 0b00100000):
mask20 times 16 db 0b00100000
Исходная строка загружается в регистр xmm0. Сначала мы сравниваем все ее символы с байтами из маски maskgeA:
vpcmpgtb xmm1, xmm0, [maskgeA] ; находим символы, код которых равен "a" или больше
В регистре xmm1 все байтовые дорожки устанавливаются в -1 (0b11111111), если код символа из соответствующей байтовой дорожке в строке больше 96. То есть xmm1 выгдлядит типа следующего:
xmm1 = 0x0000000000FFFFFFFF0000FFFFFFFF00
Далее сравниваем все ее символы с байтами из маски maskleZ:
movdqa xmm2, [maskleZ] vpcmpgtb xmm2, xmm2, xmm0 ; находим символы, код которых равен "z" или меньше - результат в xmm2
В регистре xmm2 все байтовые дорожки устанавливаются в -1 (0b11111111), если байт маски maskleZ - число 123 больше кода символа из соответствующей байтовой дорожке в строке. То есть после выполнения инструкции xmm2 выгдлядит типа следующего:
xmm2 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
По результату можно увидеть, что все символы строки иммеют код меньше 123.
Теперь нам надо найти только строчные буквы. То есть те, для которых в обоих регистрах - %xmm1 и %xmm2 установлены биты (то есть фактически код символа меньше 123, но больше 96). То есть нам надо получить пересечение множеств, и для этого выполняем логическое умножение:
vandpd xmm1, xmm1, xmm2 ; находим все строчные символы путем умножения результатов обоих сравнений
Результат помещается в xmm1, и он выглядит следующим образом:
xmm1 = 0x0000000000FFFFFFFF0000FFFFFFFF00 * xmm2 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF = xmm1 = 0x0000000000FFFFFFFF0000FFFFFFFF00
Фактически остаются те символы, которые были изначально отмечены в xmm1, так как строка не содержит символов, чей код больше 123.. Далее определяем собственно маску для перевода строчный букв в верхний регистр
andpd xmm1, [mask20] ; получаем маску для перевода из нижнего регистра в верхний
В данном случае получаем байты, где установлены 5-е биты:
xmm1 = 0x0000000000FFFFFFFF0000FFFFFFFF00 * mask20 = 0x20202020202020202020202020202020 = xmm1 = 0x00000000002020202000002020202000
Последний этап - применение маски. Для этого инвертируем маску, то есть получаем те биты надо установить. То есть если каждый бит в маске сейчас равен 0b0010000
,
то после инверсии он равен 0b11011111
. Применяя эту маске к каждому символу, мы сбросим 5-й бит. Ассемблер позволяет все эти действия выполнить одной инструкцией (операция NAND):
vandnpd xmm0, xmm1, xmm0
Результат сохраняется в регистр xmm0, а затем обратно в строку msg, которая в конце выводится на экран с помощью системной функции write.
Компиляция и результат работы программы:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# ld hello.o -o hello root@Eugene:~/asm# ./hello HELLO WORLD root@Eugene:~/asm# echo $? 13 root@Eugene:~/asm#
Аналогичный пример на Windows:
global _start extern WriteFile ; подключем функцию WriteFile extern GetStdHandle ; подключем функцию GetStdHandle section .data msg db "Hello World", 10, 0 msglen equ $-msg ; длина строки align 16 maskgeA times 16 db 96 ; код символа равен букве "a" или больше maskleZ times 16 db 123 ; код символа равен букве "z" или меньше mask20 times 16 db 0b00100000 ; для установки маски, чтобы убрать 5-й бит section .text _start: sub rsp, 40 movdqa xmm0, [rel msg] ; помещаем строку для преобразования в регистр xmm0 vpcmpgtb xmm1, xmm0, [rel maskgeA] ; находим символы, код которых равен "a" или больше - результат в xmm1 movdqa xmm2, [rel maskleZ] vpcmpgtb xmm2, xmm2, xmm0 ; находим символы, код которых равен "z" или меньше - результат в xmm2 vandpd xmm1, xmm1, xmm2 ; находим все строчные символы путем умножения результатов обоих сравнений ; в xmm1 единицы в тех байтах, где в строке строчные буквы andpd xmm1, [rel mask20] ; получаем маску для перевода из нижнего регистра в верхний vandnpd xmm0, xmm1, xmm0 ; применяем маску к строке - убираем 5-й бит movdqa [rel msg], xmm0 ; сохраняем строку обратно в переменную msg ; вывод строки на экран mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли mov rdx, msg ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX mov r8d, msglen ; Третий параметр WriteFile - длина строки для записи в регистре R8D xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 40 ret