Расширения SSE/AVX поддерживают инструкции логического и арифметического сдвига:
pslldq: сдвигает данные в регистре XMM влево на количество байтов, указанное в операнде imm8. В освободившиеся младшие байты помещаются нули.
pslldq imm8, xmmdest
vpslldq: сдвигает значение второго операнда-регистра XMM или YMM влево на количество байт из первого операнда. Результат помещается в регистр из третьего операнда.
vpslldq imm8, xmmsrc, xmmdest vpslldq imm8, ymmsrc, ymmdest
128-битный вариант инструкция также заполняет нулями биты со 128 по 255 регистра YMM.
psrldq: сдвигает данные в регистре XMM вправо на количество байтов, указанное в операнде imm8. В освободившиеся старшие байты помещаются нули.
psrldq imm8, xmmdest
vpsrldq: сдвигает значение второго операнда-регистра XMM или YMM вправо на количество байт из первого операнда. Результат помещается в регистр из третьего операнда.
vpsrldq imm8, xmmsrc, xmmdest vpsrldq imm8, ymmsrc, ymmdest
Стоит отметить, что эти инструкции сдвигают по байтам (то есть как минимум можно сдвинуть 8 разрядов) весь вектор, который хранится в регистре.
Пример:
.globl _start .data nums0: .long 3, 5, 8, 11 .text _start: movaps nums0, %xmm0 # вектор num0 в регистр %xmm0 psrldq $4, %xmm0 # сдвигаем регистр %xmm0 вправо на 4 байта movd %xmm0, %edi # %edi = 5 movq $60, %rax syscall
Здесь в регистр %xmm0 помещается вектор nums0, который состоит из 4 чисел .long. Соответственно каждое число занимает 4 байта. Инструкция psrldq $4, %xmm0
сдвигает
содерживое регистра xmm0 вправо на 4 байта:
11, 8, 5, 3 >> 4 = 0, 11, 8, 5
В итоге в XMM0 будет вектор 0, 11, 8, 5
Далее первое число в векторе из xmm0 (которым после сдвига является число 5) помещается в регистр %edi.
Используя операцию сдвига, нам легко реализовать вывод содержимого регистра xmmN на консоль:
.globl _start .data nums0: .long 254, 5, 8, 11 hexstr128: .ascii "0x123456789ABCDEFG123456789ABCDEFG\n" len=.-hexstr128 # размер строки hexmap: .byte 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 .text _start: movaps nums0, %xmm0 # вектор num0 в регистр %xmm0 call print_reg128 movq $60, %rax syscall # функция печати регистра на консоль # Параметры: XMM0 - выводимое значение print_reg128: movq $hexmap, %rsi # таблица преобразования movq $hexstr128, %rbx # начало строки addq $33, %rbx # переходим на первый байт числа # в цикле проходим от RCX = 16 до RCX=1 с приращением -1 movq $16, %rcx # 16 байтов - 32 символа для печати forloop128: movq %xmm0, %rax andq $0xf, %rax # накладываем маску - на число в AL movb (%rsi, %rax), %al # находим символьное отображение числа в таблице hexmap movb %al, (%rbx) # сохраняем один символ ascii movq %xmm0, %rax shrq $4, %rax # берем второй полубайт из XMM0 andq $0xf, %rax # накладываем маску - на число в AL movb (%rsi, %rax), %al # находим символьное отображение числа в таблице hexmap movb %al, -1(%rbx) # сохраняем один символ ascii subq $2, %rbx # вычитаем из адреса в RBX единицу для перехода к следующему символу psrldq $1, %xmm0 # сдвиг влево в XMM0 для получения следующего байта subq $1, %rcx # уменьшаем счетчик RCX на 1 - для печати следующего символа jne forloop128 # переходим к метке forloop128, если RCX не равно 1 # собственно печать строки с помощью системного вызова write movq $1, %rax # номер системной функци movq $1, %rdi # дескриптор стандартного (консольного) вывода movq $hexstr128, %rsi # адрес строки movq $len, %rdx # размер строки syscall # выполняем системный вызов ret
В данном случае для печати регистра предназначена функция print_reg128. В качестве параметра она принимает значение через регистр %xmm0. Для преобразования между 16-ричнымии числами и их символьным представлением применяется таблица-набор hexmap, которая загружается в регистр %rsi:
movq $hexmap, %rsi # таблица преобразования
Данные будут сохраняться в строку hexstr128, адрес которой загружается в регистр %rbx:
movq $hexstr128, %rbx # начало строки addq $33, %rbx # переходим на первый байт числа
Причем устанавливаем адрес в %rbx на 33 символ, с котором начнем вставку в строку чисел из %xmm0.
В регистр %rcx помещаем количество байт 128-разрядного регистра, которые нам надо просмотреть - то есть все 16 байт:
movq $16, %rcx # 16 байт forloop128:
И после метки forloop128 идет циклическая конструкция, в которой перебираем регистр xmm0 по 4 бита и сопоставляем каждые из этих 4 битов с определенной цифрой в hexmap. Строковое отображение цифры помещаем в строку hexstr128. Причем поскольку мы перемещаемся по байтам, то за один виток цикла обрабатываем 2 полубайта. Сначала сохраняем в регистр AL первый полубайт из XMM0:
movq %xmm0, %rax andq $0xf, %rax # накладываем маску - на число в AL movb (%rsi, %rax), %al # находим символьное отображение числа в таблице hexmap movb %al, (%rbx) # сохраняем один символ ascii
В %al помещаем первый полубайт (первую 16-ричную цифру), находим символьное отображение числа в таблице hexmap и сохраняем символ по адресу в %rbx (то есть адрес строки hexstr128).
Обработка второго полубайта аналогична, только символ сохраняем по адресу -1(%rbx)
(то есть смещаемся на один байт назад):
movq %xmm0, %rax shrq $4, %rax # берем второй полубайт из XMM0 andq $0xf, %rax # накладываем маску - на число в AL movb (%rsi, %rax), %al # находим символьное отображение числа в таблице hexmap movb %al, -1(%rbx) # сохраняем один символ ascii
Когда обработка обоих полубайт первого байта из XMM0 завершилась, сдвигаем xmm0 на 1 байт вправо:
psrldq $1, %xmm0 # сдвиг влево в XMM0 для получения следующего байта
После обработки всех байтов и соответственно завершения цикла выводим строку hexstr128 на консоль с помощью системной функции write. Консольный вывод программы:
root@Eugene:~/asm# as hello.s -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
Аналогичным образом можно было бы выводить данные с помощью библиотечной функции printf
языка Си:
.globl main .data nums0: .long 255, 5, 8, 11 format_str: .asciz "%d, %d, %d, %d\n" .text main: subq $8, %rsp movaps nums0, %xmm0 # вектор num0 в регистр xmm0 movd %xmm0, %esi # помещаем первое число в %rsi psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %edx # помещаем первое число в %rdx psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %ecx # помещаем первое число в %rcx psrldq $4, %xmm0 # сдвигаем вправо на 4 бита - к следующему числу в векторе в xmm0 movd %xmm0, %r8d # помещаем первое число в %r8 movq $format_str, %rdi call printf addq $8, %rsp ret
Также чтобы получить каждое отдельное число вектора в %xmm0, применяем операцию сдвига вправо и помещаем крайнее правое число вектора в один из регистров для вывода функцией printf. Пример компиляции и работы программы:
root@Eugene:~/asm# gcc -static hello.s -o hello root@Eugene:~/asm# ./hello 255, 5, 8, 11 root@Eugene:~/asm#
Вывод чисел с плавающей точкой чуть сложнее:
.globl main .data nums: .single 1.2, 2.3, 3.4, 4.5 format_str: .asciz "%.1f, %.1f, %.1f, %.1f\n" .text main: subq $8, %rsp movaps nums, %xmm5 # вектор nums в регистр xmm5 movss %xmm5, %xmm0 # помещаем первое число в %xmm0 cvtss2sd %xmm0, %xmm0 # Преобразуем из float в double psrldq $4, %xmm5 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss %xmm5, %xmm1 # помещаем второе число в %xmm1 cvtss2sd %xmm1, %xmm1 # Преобразуем из float в double psrldq $4, %xmm5 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss %xmm5, %xmm2 # помещаем третье число в %xmm2 cvtss2sd %xmm2, %xmm2 # Преобразуем из float в double psrldq $4, %xmm5 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm5 movss %xmm5, %xmm3 # помещаем четвертое число в %xmm3 cvtss2sd %xmm3, %xmm3 # Преобразуем из float в double movq $format_str, %rdi call printf addq $8, %rsp ret
Опять же применяем для вывода функцию printf
. Числа с плавающей точкой передаются в функцию через регистры xmm0-xmm7. Нам надо передать 4 числа, соответственно регистры
xmm0-xmm3 будут заняты, а вектор загружается в регистр xmm5.
Обработчка каждого отдельного числа вектора требует нескольких операций. Сначала помещаем отдельное число с плавающей точкой в целевой регистра
movss %xmm5, %xmm0
Затем преобразуем значение .single (.float) в тип .double:
cvtss2sd %xmm0, %xmm0
Затем для перехода к следующему числу в векторе сдвигаем содержимое xmm5 на 4 байта вправо:
psrldq $4, %xmm5
Результат работы программы:
root@Eugene:~/asm# gcc -static hello.s -o hello root@Eugene:~/asm# ./hello 1.2, 2.3, 3.4, 4.5 root@Eugene:~/asm#
Если бы мы использовали вектор чисел .double, то преобразования нам естественно были бы не нужны:
.globl main .data nums: .double 2.3, 4.5 format_str: .asciz "%.1f, %.1f\n" .text main: subq $8, %rsp movaps nums, %xmm5 # вектор nums в регистр xmm5 movsd %xmm5, %xmm0 # помещаем первое число в %xmm0 psrldq $8, %xmm5 # сдвигаем вправо на 8 бfqт - к следующему числу в векторе в xmm5 movsd %xmm5, %xmm1 # помещаем второе число в %xmm1 movq $format_str, %rdi call printf addq $8, %rsp 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 imm8, xmmdest psllw xmmsrc/mem128, xmmdest vpsllw imm8, xmmsrc, xmmdest vpsllw mem128, xmmsrc, xmmdest vpsllw imm8, ymmsrc, ymmdest vpsllw xmm/mem128, ymmsrc, ymmdest
В инструкции с двумя операндами первый операнд указывает на количество битов для сдвига (это либо 8-битная непосредственная константа, либо регистр XMM, либо в 128-битная переменная), а второй операнд представляет сдвигаемое значение. .
В инструкции с тремя операндами первый операнд представляет количество битов для сдвига, а второй - сдвигаемое значение. Результат сдвига помещается в третий операнд. Исходный регистр (второй операнд) остается неизменным (если, конечно, инструкция не указывает один и тот же регистр для исходного и целевого операндов).
Например, загрузим вектор из 4 чисел и сдвинем каждое число на 1 разряд влево (фактически умножим на 2):
.globl main .data nums: .long 1, 2, 4, 8 format_str: .asciz "%d, %d, %d, %d\n" .text main: subq $8, %rsp movaps nums, %xmm0 # вектор nums в регистр xmm0 pslld $1, %xmm0 # сдвиг влево каждого числа из xmm0 movd %xmm0, %esi # помещаем первое число в %esi psrldq $4, %xmm0 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd %xmm0, %edx # помещаем первое число в %edx psrldq $4, %xmm0 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd %xmm0, %ecx # помещаем первое число в %ecx psrldq $4, %xmm0 # сдвигаем вправо на 4 байта - к следующему числу в векторе в xmm0 movd %xmm0, %r8d # помещаем первое число в %r8d movq $format_str, %rdi call printf addq $8, %rsp ret
Компиляция и результат работы программы:
root@Eugene:~/asm# gcc -static hello.s -o hello root@Eugene:~/asm# ./hello 2, 4, 8, 16 root@Eugene:~/asm#