Расширения SSE/AVX поддерживают инструкции логического и арифметического сдвига:
pslldq: сдвигает данные в регистре XMM влево на количество байтов, указанное в операнде imm8. В освободившиеся младшие байты помещаются нули.
pslldq xmmdest, imm8
vpslldq: берет значение из второго операнда-регистра XMM или YMM, сдвигает это значение влево на количество байт, указанное в третьем операнде. Затем сохраняет результат в регистре из первого операнда.
vpslldq xmmdest, xmmsrc, imm8 vpslldq ymmdest, ymmsrc, imm8
128-битный вариант инструкция также заполняет нулями биты со 128 по 255 регистра YMM.
psrldq: сдвигает данные в регистре XMM вправо на количество байтов, указанное в операнде imm8. В освободившиеся старшие байты помещаются нули.
psrldq xmmdest, imm8
vpsrldq: сдвигает вправл значение из второго операнда-регистра XMM или YMM на количество байт, указанное в третьем операнде. Затем сохраняет результат в первом операнде.
vpsrldq xmmdest, xmmsrc, imm8 vpsrldq ymmdest, ymmsrc, imm8
Стоит отметить, что эти инструкции сдвигают по байтам (то есть как минимум можно сдвинуть 8 разрядов) весь вектор, который хранится в регистре.
Пример сдвига в программе на Linux:
global _start section .data nums dd 3, 5, 8, 11 section .text _start: movaps xmm0, [nums] ; вектор nums в регистр xmm0 psrldq xmm0, 4 ; сдвигаем регистр xmm0 вправо на 4 байта movd edi, xmm0 ; edi = 5 mov rax, 60 syscall
Здесь в регистр xmm0 помещается вектор nums0, который состоит из 4 чисел dword. Соответственно каждое число занимает 4 байта. Инструкция psrldq xmm0, 4
сдвигает
содерживое регистра xmm0 вправо на 4 байта:
11, 8, 5, 3 >> 4 = 0, 11, 8, 5
В итоге в XMM0 будет вектор 0, 11, 8, 5
Далее первое число в векторе из xmm0 (которым после сдвига является число 5) помещается в регистр edi.
Используя операцию сдвига, нам легко реализовать вывод содержимого регистра xmmN на консоль. Например, определим следующую программу для Linux:
global _start section .data nums dd 254, 5, 8, 11 hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0 len equ $-hexstr128 ; размер строки hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 section .text _start: movaps xmm0, [nums] ; вектор num в регистр xmm0 call print_reg128 mov rax, 60 syscall ; функция печати регистра на консоль ; Параметры: XMM0 - выводимое значение print_reg128: mov rsi, hexmap ; таблица преобразования mov rbx, hexstr128 ; начало строки add rbx, 33 ; переходим на первый байт числа в строке hexstr128 ; в цикле проходим от RCX = 16 до RCX=1 с приращением -1 mov rcx, 16 ; 16 байтов - 32 символа для печати forloop128: movq rax, xmm0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx], al ; сохраняем один символ ascii movq rax, xmm0 shr rax, 4 ; берем второй полубайт из XMM0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx-1], al ; сохраняем один символ ascii sub rbx, 2 ; вычитаем из адреса в RBX 2 для перехода к следующему символу psrldq xmm0, 1 ; сдвиг влево в XMM0 для получения следующего байта sub rcx, 1 ; уменьшаем счетчик RCX на 1 - для печати следующего символа jne forloop128 ; переходим к метке forloop128, если RCX не равно 1 ; собственно печать строки с помощью системного вызова write mov rax, 1 ; номер системной функци mov rdi, 1 ; дескриптор стандартного (консольного) вывода mov rsi, hexstr128 ; адрес строки mov rdx, len ; размер строки syscall ; выполняем системный вызов ret
В данном случае для печати регистра предназначена функция print_reg128. В качестве параметра она принимает значение через регистр xmm0. Для преобразования между 16-ричнымии числами и их символьным представлением применяется таблица-набор hexmap, которая загружается в регистр rsi:
mov rsi, hexmap ; таблица преобразования
Данные будут сохраняться в строку hexstr128, адрес которой загружается в регистр rbx:
mov rbx, hexstr128 ; начало строки add rbx, 33 ; переходим на первый байт числа в строке hexstr128
Причем устанавливаем адрес в rbx на 33 символ, с котором начнем вставку в строку чисел из xmm0.
В регистр rcx помещаем количество байт 128-разрядного регистра, которые нам надо просмотреть - то есть все 16 байт:
mov rcx, 16 ; 16 байт forloop128:
И после метки forloop128 идет циклическая конструкция, в которой перебираем регистр xmm0 по 4 бита и сопоставляем каждые из этих 4 битов с определенной цифрой в hexmap. Строковое отображение цифры помещаем в строку hexstr128. Причем поскольку мы перемещаемся по байтам, то за один виток цикла обрабатываем 2 полубайта. Сначала сохраняем в регистр AL первый полубайт из XMM0:
movq rax, xmm0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx], al ; сохраняем один символ ascii
В al помещаем первый полубайт (первую 16-ричную цифру), находим символьное отображение числа в таблице hexmap и сохраняем символ по адресу в rbx (то есть адрес строки hexstr128).
Обработка второго полубайта аналогична, только символ сохраняем по адресу [rbx-1]
(то есть смещаемся на один байт назад):
movq rax, xmm0 shr rax, 4 ; берем второй полубайт из XMM0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx-1], al ; сохраняем один символ ascii
Когда обработка обоих полубайт первого байта из XMM0 завершилась, сдвигаем xmm0 на 1 байт вправо:
psrldq xmm0, 1 ; сдвиг влево в XMM0 для получения следующего байта
После обработки всех байтов и соответственно завершения цикла выводим строку hexstr128 на консоль с помощью системной функции write. Консольный вывод программы:
root@Eugene:~/asm; nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm; ld hello.o -o hello root@Eugene:~/asm; ./hello 0x0000000B0000000800000005000000FE root@Eugene:~/asm;
Таким образом, регистр XMM0, который содержит вектор из 32-разрядных чисел 254, 5, 8, 11
, в 16-ричном виде соответствует значению 0x0000000B0000000800000005000000FE
Аналогичная программа на Windows:
global _start extern WriteFile ; подключем функцию WriteFile extern GetStdHandle ; подключем функцию GetStdHandle section .data nums dd 254, 5, 8, 11 hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0 len equ $-hexstr128 ; размер строки hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 section .text _start: movaps xmm0, [rel nums] ; вектор num в регистр xmm0 call print_reg128 ret ; функция печати регистра на консоль ; Параметры: XMM0 - выводимое значение print_reg128: sub rsp, 48 ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт) mov rsi, hexmap ; таблица преобразования mov rbx, hexstr128 ; начало строки add rbx, 33 ; переходим на первый байт числа в строке hexstr128 ; в цикле проходим от RCX = 16 до RCX=1 с приращением -1 mov rcx, 16 ; 16 байтов - 32 символа для печати forloop128: movq rax, xmm0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx], al ; сохраняем один символ ascii movq rax, xmm0 shr rax, 4 ; берем второй полубайт из XMM0 and rax, 0xf ; накладываем маску - на число в AL mov al, byte [rsi + rax] ; находим символьное отображение числа в таблице hexmap mov byte [rbx-1], al ; сохраняем один символ ascii sub rbx, 2 ; вычитаем из адреса в RBX 2 для перехода к следующему символу psrldq xmm0, 1 ; сдвиг влево в XMM0 для получения следующего байта sub rcx, 1 ; уменьшаем счетчик RCX на 1 - для печати следующего символа jne forloop128 ; переходим к метке forloop128, если RCX не равно 1 ; собственно печать строки mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли mov rdx, hexstr128 ; Второй параметр WriteFile - адрес строки mov r8d, len ; Третий параметр WriteFile - размер строки xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 48 ret
Аналогичным образом можно было бы выводить данные с помощью библиотечной функции printf
языка Си. Рассмотрим на примере программы для Linux:
global main extern printf section .data nums dd 255, 5, 8, 11 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums] ; вектор num0 в регистр xmm0 movd esi, xmm0 ; помещаем первое число в rsi psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd edx, xmm0 ; помещаем первое число в rdx psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd ecx, xmm0 ; помещаем первое число в rcx psrldq xmm0, 4 ; сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd r8d, xmm0 ; помещаем первое число в r8 mov rdi, format_str call printf add rsp, 8 ret
Также чтобы получить каждое отдельное число вектора в xmm0, применяем операцию сдвига вправо и помещаем крайнее правое число вектора в один из регистров для вывода функцией printf. Пример компиляции и работы программы:
root@Eugene:~/asm; nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm; gcc -static hello.o -o hello root@Eugene:~/asm; ./hello 255, 5, 8, 11 root@Eugene:~/asm;
Вывод чисел с плавающей точкой чуть сложнее. Также рассмотрим на примере программы для Linux:
global main extern printf section .data nums dd 1.2, 2.3, 3.4, 4.5 format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0 section .text main: sub rsp, 8 movaps xmm5, [nums] ; вектор num0 в регистр xmm5 movss xmm0, xmm5 ; помещаем первое число в xmm0 cvtss2sd xmm0, xmm0 ; Преобразуем из float в double psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm1, xmm5 ; помещаем второе число в xmm1 cvtss2sd xmm1, xmm1 ; Преобразуем из float в double psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm2, xmm5 ; помещаем третье число в xmm2 cvtss2sd xmm2, xmm2 ; Преобразуем из float в double psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm3, xmm5 ; помещаем четвертое число в xmm3 cvtss2sd xmm3, xmm3 ; Преобразуем из float в double mov rdi, format_str call printf add rsp, 8 ret
Опять же применяем для вывода функцию printf
. Числа с плавающей точкой передаются в функцию через регистры xmm0-xmm7. Нам надо передать 4 числа, соответственно регистры
xmm0-xmm3 будут заняты, а вектор загружается в регистр xmm5.
Обработчка каждого отдельного числа вектора требует нескольких операций. Сначала помещаем отдельное число с плавающей точкой в целевой регистра
movss xmm0, xmm5
Затем преобразуем значение:
cvtss2sd xmm0, xmm0
Затем для перехода к следующему числу в векторе сдвигаем содержимое xmm5 на 4 байта вправо:
psrldq xmm5, 4
Результат работы программы:
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.2, 2.3, 3.4, 4.5 root@Eugene:~/asm;
Аналогичный пример на Windows:
global main extern printf section .data align 16 nums dd 1.2, 2.3, 3.4, 4.5 format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0 section .text main: sub rsp, 40 movaps xmm5, [rel nums] ; вектор num0 в регистр xmm5 movss xmm1, xmm5 ; помещаем первое число в xmm0 cvtss2sd xmm1, xmm1 ; Преобразуем из float в double movq rdx, xmm1 ; дублируем в rdx psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm2, xmm5 ; помещаем второе число в xmm1 cvtss2sd xmm2, xmm2 ; Преобразуем из float в double movq r8, xmm2 ; дублируем в r8 psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm3, xmm5 ; помещаем третье число в xmm2 cvtss2sd xmm3, xmm3 ; Преобразуем из float в double movq r9, xmm3 ; дублируем в r9 psrldq xmm5, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss xmm4, xmm5 ; помещаем четвертое число в xmm3 cvtss2sd xmm4, xmm4 ; Преобразуем из float в double movq [rsp+32], xmm4 ; помещаем в стек mov rcx, format_str mov rax, 4 call printf add rsp, 40 ret
Если бы мы использовали вектор чисел с плавающей точкой двойной точности (по 8 байт), то преобразования нам естественно были бы не нужны. Пример для Linux:
global main extern printf section .data nums dq 2.3, 4.5 format_str db "%.1f, %.1f", 10, 0 section .text main: sub rsp, 8 movaps xmm5, [nums] ; вектор num0 в регистр xmm5 movsd xmm0, xmm5 ; помещаем первое число в xmm0 psrldq xmm5, 8 ; сдвигаем вправо на 8 байт - к следующему числу в векторе в xmm5 movsd xmm1, xmm5 ; помещаем второе число в xmm1 mov rdi, format_str call printf add rsp, 8 ret
Но также есть ряд инструкций, которые сдвигают по отдельным битам причем не весь регистр в целом, а отдельные элементы в векторе в регистре. То есть каждый элемент вектора сдвигается на определенное количество бит. Это инструкции:
(v)psllw: сдвигает влево слово (16-разрядное целое число)
(v)pslld: сдвигает влево двойное слово (32-разрядное целое число)
(v)psllq: сдвигает влево четверное слово (64-разрядное целое число)
(v)psrlw: сдвигает вправо слово
(v)psrld: сдвигает вправо двойное слово
(v)psrlq: сдвигает вправо четверное слово
(v)psraw: арифметический сдвиг слова вправо
(v)psrad: арифметический сдвиг двойного слова вправо
(v)psraq: арифметический сдвиг четверного слова вправо
Синтаксис инструкций на примере (v)psllw
(остальные инструкции принимают те же операнды)
psllw xmmdest, imm8 psllw xmmdest, xmmsrc/mem128 vpsllw xmmdest, xmmsrc, imm8 vpsllw xmmdest, xmmsrc, mem128 vpsllw ymmdest, ymmsrc, imm8 vpsllw ymmdest, ymmsrc, xmm/mem128
В инструкции с двумя операндами первый операнд представляет сдвигаемое значение, а второй операнд указывает на количество битов для сдвига (это либо 8-битная непосредственная константа, либо регистр XMM, либо в 128-битная переменная).
В инструкции с тремя операндами второй операнд представляет сдвигаемое значение, а третий операнд представляет количество битов для сдвига. Результат сдвига помещается в первый операнд. Исходный регистр (второй операнд) остается неизменным (если, конечно, инструкция не указывает один и тот же регистр для исходного и целевого операндов).
Например, загрузим вектор из 4 чисел и сдвинем каждое число на 1 разряд влево (фактически умножим на 2) в программе на Linux:
global main extern printf section .data nums dd 1, 2, 4, 8 format_str db "%d, %d, %d, %d", 10, 0 section .text main: sub rsp, 8 movaps xmm0, [nums] ; вектор num0 в регистр xmm5 pslld xmm0, 1 ; сдвиг влево каждого числа из xmm0 movd esi, xmm0 ; помещаем первое число в esi psrldq xmm0, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd edx, xmm0 ; помещаем первое число в edx psrldq xmm0, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd ecx, xmm0 ; помещаем первое число в ecx psrldq xmm0, 4 ; сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd r8d, xmm0 ; помещаем первое число в r8d 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 2, 4, 8, 16 root@Eugene:~/asm;