Архитектура x86-64 предоставляет множество инструкций для копирования данных, которые копируют данные между регистрами (SSE/AVX), получают данные из регистров общего назначения или переменных или сохраняют данные обратно в переменные и другие регистры. И здесь мы можем копировать либо единичные значения, либо вектор значений.
Расширения SSE предоставляют инструкции movd (для копирования данных типа dword) и movq:
movd xmm, reg32/mem32 movq xmm, reg64/mem64
Инструкция movd копирует значение из 32-разрядного регистра общего назначения или переменной в младшие 32 бита регистра XMM. А инструкция movq копирует значение из 64-разрядного регистра общего назначения или переменной в младшие 64 бита регистра XMM.
Также эти инструкции поддерживают обратную операцию - сохранение данных из регистров XMM в 32- или 64-разрядный регистр общего назначения или переменную:
movd reg32/mem32, xmm movq reg64/mem64, xmm
Инструкция movq также позволяет копировать данные из младших 64 бит одного регистра XMM в другой регистр XMM:
movq xmm, xmm
Расширение AVX поддержиет аналогичные инструкции, только их название предваряется символом v:
vmovd xmm, reg32/mem32 vmovd reg32/mem32, xmm vmovq xmm, reg64/mem64 vmovq reg64/mem64, xmm
Поскольку в этих операциях участвуют 32- и 64-разрядные операнды, то для переменных необязательно выравнивание в памяти по 16/32/64 битам (хотя при наличии подобного выравнивания инструкции могут выполняться быстрее).
Пример копирования:
.data num dword 22 .code main proc mov eax, 7 ; в XMM movq xmm0, rax ; в XMM0 число 7 movd xmm1, num ; в XMM1 число 22 ; из XMM movq rax, xmm1 ; RAX = 22 movd num, xmm0 ; num = 7 add eax, num ; eax = 22 + 7 = 29 ret main endp end
Отдельно есть инструкции, которые выполняют копирование выровненных данных
movaps: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real4, dword и oword
movapd: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real8, qword и oword
movdqa: копирование восьмеричного слова (oword). Допускает только переменные типа oword
Эти инструкции копируют 16 байтов данных между переменными и регистрами XMM или между двумя регистрами XMM. Расширения AVX представляют аналогичные инструкции с префиксом v, которые копируют 16 или 32 байта между переменными и регистрами XMM или YMM или между двумя регистрами XMM или YMM (при копировании в регистры XMM обнуляются старшие биты соответствующего регистра YMM):
vmovaps: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real4, dword и (при использовании регистров YMM) ymmword ptr
vmovapd: копирование числа с плавающей точкой одинарной точности. Допускает переменные типов real8, qword и (при использовании регистров YMM) ymmword ptr
vmovdqa: копирование восьмеричного слова (oword). При использовании регистров YMM допускает только переменные типа ymmword ptr
Инструкции принимают следующие формы:
movaps xmm, mem128 vmovaps xmm, mem128 vmovaps ymm, mem256 movaps mem128, xmm vmovaps mem128, xmm vmovaps mem256, ymm movaps xmm, xmm vmovaps xmm, xmm vmovaps ymm, ymm movapd xmm, mem128 vmovapd xmm, mem128 vmovapd ymm, mem256 movapd mem128, xmm vmovapd mem128, xmm vmovapd mem256, ymm movapd xmm, xmm vmovapd xmm, xmm vmovapd ymm, ymm movdqa xmm, mem128 vmovdqa xmm, mem128 vmovdqa ymm, mem256 movdqa mem128, xmm vmovdqa mem128, xmm vmovdqa mem256, ymm movdqa xmm, xmm vmovdqa xmm, xmm vmovdqa ymm, ymm
При копировании переменные должны быть выровнены по 16-байтовой или 32-байтовой границе (соответственно), иначе процессор сгенерирует ошибку невыровненного доступа.
Операнд mem128 должен быть выровнен по 16-байтовой границе и в зависимости от инструкции должен представлять:
для инструкции (v)movaps
- вектор из четырех 32-разрядных чисел
для инструкции (v)movapd
- вектор из двух 64-разрядных чисел
для инструкции (v)movdqa
- 16-байтовое значение (16 байтов, 8 word, 4 dword или 2 qword)
Операнд mem256 должен быть выровнен по 32-байтовой границе и в зависимости от инструкции должен представлять:
для инструкции vmovaps
- вектор из восьми 32-разрядных чисел
для инструкции vmovapd
- вектор из четырех 64-разрядных чисел
для инструкции vmovdqa
- 32-байтовое значение (32 байта, 16 word, 8 dword или 4 qword)
Подобные инструкции удобно применять для копирования данных из одного вектора в другой:
.data qnums qword 121, 122 qnums_copy qword 2 dup(?) .code main proc ; в XMM movapd xmm0, qnums ; из XMM movapd qnums_copy, xmm0 mov rax, qnums_copy ; rax = 121 ret main endp end
В данном случае определены два вектора, они состоят из 2-х чисел qword.
С помощью инструкции movapd
вектор qnums загружается в регистр XMM0. Затем эти данные выгружаются обратно в переменную qnums_copy.
Как выше говорилось, для определенных инструкций мы можем передать определенное количество данных определенного типа. Так, в примере выше мы передаем инструкции movapd
вектор из 2 чисел qword. Что будет, если мы передадим вектор из большего количества чисел? Например:
.data qnums qword 121, 122, 123, 124 ................................ movapd xmm0, qnums
В этом случае в XMM0 будут загружены только 2 первых числа, что вообщем-то логично, так как 128-разрядный регистр XMM1 вмешает только два 64-разрядных числа.
Аналогично можно копировать строки:
includelib kernel32.lib ; подключаем библиотеку kernel32.lib ; подключаем функции WriteFile и GetStdHandle extrn WriteFile: PROC extrn GetStdHandle: PROC len equ 16 .data source byte "Hello METANIT",10 align 16 dest byte len dup(?) .code main proc movdqa xmm0, oword ptr source ; копируем из source в %ymm0 movdqa oword ptr dest, xmm0 ; копируем из %ymm0 в dest ; вывод строки dest на консоль sub rsp, 40 ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт плюс 8 байт для выравнивания данных mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли lea rdx, dest ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX mov r8d, len ; Третий параметр WriteFile - длина строки для записи в регистре R8D xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword ptr [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 40 ret main endp end
В данном случае копируем строку source в регистр xmm0, а затем из регистра xmm0 - в переменную dest. Затем выводим строку на консоль с помощью функции WriteLine.
Но стоит не забывать главную вещь - эти инструкции требуют выравнивания. Например, в примере выше выравнивание устанавливалось по умолчанию - обе переменных автоматически было выровнены
по 16 байтам (как того требуют инструкции movaps
и movapd
), поскольку перед первой переменной ничего нет, а она сама занимает 4 * 32 = 128 байт, то есть размер кратный 16. Соответственно вторая переменная также была выравнена по 16 байтам.
Но возьмем другой пример:
.data temp1 byte 1 qnums qword 121, 122 temp2 byte 31 qnums_copy qword 2 dup(?) .code main proc ; в XMM movapd xmm0, qnums ; из XMM movapd qnums_copy, xmm0 mov rax, qnums_copy ; для теста получим в RAX первый элемент вектора qnums_copy ret main endp end
Перед вектором qnums идет однобайтная переменная, поэтому он не выровнен по 16 байт, соответственно также не выровнен и вектор qnums_copy. И здесь как раз можно использовать выравнивание:
.data temp1 byte 1 align 16 qnums qword 121, 122 temp2 byte 31 align 16 qnums_copy qword 2 dup(?)
Из минусов - программа занимает больше места. Однако если порядок переменных не играет значения, то лучше переупорядочить, чтобы избежать избыточных выравниваний:
.data qnums qword 121, 122 qnums_copy qword 2 dup(?) temp1 byte 1 temp2 byte 31
Однако подобное выравнивание имеет недостаток - мы не можем так задать выравнивание по 32 байтам, если нам нужна, например, 256-разрядная переменная, которую надо поместить в один из 256-разрядных регистров YMM, как в следующем случае:
.data temps qword 1, 2, 3 align 32 ; Ошибка: 32 - некорректное значение nums qword 11, 22, 33, 44 nums_copy qword 4 dup(?) .code main proc vmovapd ymm0, nums vmovapd nums_copy, ymm0 mov rax, nums_copy ret main endp end
Здесь мы столкнемся с ошибкой, так как 32 - некорректное значение для установки выравнивания. С аналогичной проблемой мы столкнемся, если захотим выравнить данные по 64 байтам, например, для последущего копирования в 516-разрядный регистр ZMM.
Чтобы решить проблему, нам надо определить сегмент с установкой выравнивания. Сегмент определяется следующим образом:
имя_сегмента segment readonly alignment инструкции имя_сегмента ends
Сначала идет имя сегмента, после которого указывается оператор segment
. После слова segment
указывается параметр выравнивания,
который может принимать значения: byte (выравнивае по 1 байту), word (по 2 байтам), dword (по 4 байтам), para (выравнивание по параграфу - 16 байтам), page (по странице - 256 байтам),
align(n)
(выравнивание по n байт). Если сегмент надо сделать доступным только для чтения (например, если его переменные не планируется изменять), то после слова segment
указывается слово readonly
.
После выравнивания размещаются инструкции или определения данных. Завершается сегмент также именем сегмента с оператором ends
.
Например, перепишем предыдущтй пример, используя сегменты:
dataSeg segment align(32) temps qword 1, 2, 3 align 32 nums qword 11, 22, 33, 44 nums_copy qword 4 dup(?) dataSeg ends .code main proc vmovapd ymm0, nums vmovapd nums_copy, ymm0 mov rax, nums_copy ret main endp end
Здесь сегмент называется dataSeg. Он вырованен по 32 байтам (align(32)
), что гарантирует, что первая переменная будет выровнена по 32 байтам. Кроме того, внутри сегмента мы
можем использовать директиву align 32
, чтобы также выровнять переменную nums по 32 байтам. Но опять же в данном случае, если порядок данных в сегменте не важен, то мы могли бы
перегруппировать данные, чтобы избежать ненужного выравнивания:
dataSeg segment align(32) nums dword 12, 22, 33, 44, 55, 66, 77, 88 nums_copy dword 8 dup(?) temps dword 1, 2, 3, 4 dataSeg ends
Если нельзя гарантировать, что переменные-операнды выровнены по 16- или 32-байтовой адресной границе, то для копирования данных между регистрами XMM или YMM и памятью можно использовать ряд других инструкций:
(v)movups
: копирование невыровненных 4-байтных чисел
(v)movupd
: копирование невыровненных 8-байтных чисел
(v)movdqu
: копирование невыровненного 16-байтного числа
Хотя считается, что эти инструкции работают в целом медленнее, чем их аналоги для выровненных данных.