Архитектура x86-64 предоставляет множество инструкций для копирования данных, которые копируют данные между регистрами (SSE/AVX), получают данные из регистров общего назначения или переменных или сохраняют данные обратно в переменные и другие регистры.
Расширения SSE предоставляют инструкции movd (для копирования 32-разрядных чисел) и movq (для копирования 64-разрядных чисел):
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 vmovq xmm, reg64/mem64 vmovd reg32/mem32, xmm vmovq reg64/mem64, xmm
Данные инструкции применяются как для копирования целых чисел, так и чисел с плавающей точкой. Пример копирования на Linux:
global _start section .data num dw 22 section .text _start: mov rax, 7 ; в XMM movq xmm0, rax ; в XMM0 число 7 movd xmm1, [num] ; в XMM1 число 22 ; из XMM movq rdi, xmm1 ; RDI = 22 movd [num], xmm0 ; num = 7 ; для проверки устанавливаем код возврата add rdi, [num] ; RDI = RDI + num = 22 + 7 = 29 mov rax, 60 syscall
Аналогичный пример на Windows:
global _start section .data num dw 22 section .text _start: mov rax, 7 ; в XMM movq xmm0, rax ; в XMM0 число 7 movd xmm1, [rel num] ; в XMM1 число 22 ; из XMM movq rax, xmm1 ; RAX = 22 movd [rel num], xmm0 ; num = 7 ; для проверки устанавливаем код возврата add rax, [rel num] ; RAX = RAX + num = 22 + 7 = 29 ret
Подобным образом копируются и числа с плавающей точкой. Пример на Linux:
global _start section .data num0 dq 2.13 num1 dw 3.14 section .text _start: movq xmm0, [num0] ; XMM0 = 2.13 movd xmm1, [num1] ; XMM1 = 3.14 mov rax, 60 syscall
Для копирования отдельных чисел с плавающей точкой также есть пара инструкций:
movss: копирует одно 32-разрядное число с плавающей точкой
movsd: копирует одно 64-разрядное число с плавающей точкой
Их общий синтаксис:
movss xmmn, mem32 movss mem32, xmmn movss xmmsrc, xmmdest movsd xmmn, mem64 movsd mem64, xmmn movsd xmmsrc, xmmdest
Можно копировать как в регистр xmm из переменной, либо в переменную из регистра xmm.
Для максимальной производительности переменные, используемые в инструкции movss
, должны располагаться выравненны в памяти по двойному слову (4 бита), а операнды инструкции movsd
— по адресу памяти, выровненному по четверному слову.
Для копирования вектора или набора данных предусмотрен ряд инструкций, которые могут копировать либо выровненные, либо не выровненные данные.
Для копирования выровненных данных расширения SSE предоставляют следующий набор инструкций
movaps: копирование набора 32-разрядных чисел
movapd: копирование набора 64-разрядных чисел
movdqa: копирование восьмеричного слова
Эти инструкции копируют 16 байтов данных между переменными и регистрами XMM или между двумя регистрами XMM. Расширения AVX представляют аналогичные инструкции с префиксом v, которые копируют 16 или 32 байта между переменными и регистрами XMM или YMM или между двумя регистрами XMM или YMM (при копировании в регистры XMM обнуляются старшие биты соответствующего регистра YMM):
vmovaps: копирование набора 32-разрядных чисел
vmovapd: копирование набора 64-разрядных чисел
vmovdqa: копирование восьмеричного слова
Инструкции принимают следующие формы:
movaps mem128, xmm vmovaps mem128, xmm vmovaps mem256, ymm movaps xmm, mem128 vmovaps xmm, mem128 vmovaps ymm, mem256 movaps xmm, xmm vmovaps xmm, xmm vmovaps ymm, ymm movapd mem128, xmm vmovapd mem128, xmm vmovapd mem256, ymm movapd xmm, mem128 vmovapd xmm, mem128 vmovapd ymm, mem256 movapd xmm, xmm vmovapd xmm, xmm vmovapd ymm, ymm movdqa mem128, xmm vmovdqa mem128, xmm vmovdqa mem256, ymm movdqa xmm, mem128 vmovdqa xmm, mem128 vmovdqa ymm, mem256 movdqa xmm, xmm vmovdqa xmm, xmm vmovdqa ymm, ymm
При копировании переменные должны быть выровнены по 16-байтовой или 32-байтовой границе (соответственно), иначе процессор сгенерирует ошибку невыровненного доступа. Собственно буква a в названии инструкций как раз представляет сокращение от "aligned" (выровненный)
Подобные инструкции позволяют нам легко скопировать большой набор данных из одного набора в другой:
global _start section .data align 16 ; гарантируем выравнивание source dd 12, 13, 14, 15 ; откуда копируем section .bss align 16 ; гарантируем выравнивание dest resd 4 ; куда копируем section .text _start: movaps xmm0, [source] ; копируем из source в xmm0 movaps [dest], xmm0 ; копируем из xmm0 в dest mov edi, [dest] ; RDI = 12 mov rax, 60 syscall
В данном случае определены два вектора - source и dest. Стоит отметить, что оба вектора выровнены по 16 байтам. Вектор source копируется в регистр xmm0. Затем вектор чисел из регистра xmm0 копируется в dest. Таким образом, с помощью двух инструкций мы скопировали набор из 4 32-разрядных чисел типа. При использовании расширений AVX2/AVX512 и соответственно регистров YMM/ZMM количество чисел увеличивается.
Аналогичный пример на Windows:
global _start section .data align 16 ; гарантируем выравнивание source dd 12, 13, 14, 15 ; откуда копируем section .bss align 16 ; гарантируем выравнивание dest resd 1 ; куда копируем section .text _start: movaps xmm0, [rel source] ; копируем из source в xmm0 movaps [rel dest], xmm0 ; копируем из xmm0 в dest mov eax, [rel dest] ; RAX = 12 ret
Как выше говорилось, для определенных инструкций мы можем передать определенное количество данных определенного типа. Так, в примере выше мы передаем инструкции movaps
вектор из 4 32-разрядных чисел. Что будет, если мы передадим вектор из большего количества чисел? Например:
section .data align 16 ; гарантируем выравнивание source dd 12, 13, 14, 15, 16, 17, 18 ............................ movaps xmm0, [source] ; копируем из source в xmm0
В этом случае в XMM0 будут загружены только 4 первых числа, что вообщем-то логично, так как 128-разрядный регистр XMM0 вмешает только четыре 32-разрядных числа.
Аналогично можно копировать и строки. Пример на Linux:
global _start section .data align 16 source db "Hello METANIT.COM", 10, 0 len equ $ - source section .bss align 16 ; гарантируем выравнивание dest resb len ; куда копируем section .text _start: vmovapd ymm0, [source] ; копируем из source в ymm0 vmovapd [dest], ymm0 ; копируем из ymm0 в dest ; выводим строку dest на консоль mov rdi, 1 mov rsi, dest mov rdx, len mov rax, 1 syscall mov rax, 60 syscall
Здесь копируем строку source через регистр 256-разрядный регистр ymm0 в переменную dest.
Аналогичная программа для Windows:
global _start extern GetStdHandle extern WriteFile section .data align 16 source db "Hello METANIT.COM", 10, 0 len equ $ - source section .bss align 16 ; гарантируем выравнивание dest resb len ; куда копируем section .text _start: sub rsp, 40 ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт) vmovapd ymm0, [rel source] ; копируем из source в ymm0 vmovapd [rel dest], ymm0 ; копируем из ymm0 в dest ; выводим строку dest на консоль mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли mov rdx, dest ; Второй параметр WriteFile - загружаем указатель на строку в регистр RDX mov r8d, len ; Третий параметр WriteFile - длина строки для записи в регистре R8D xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 40 ret
Но стоит не забывать главную вещь - эти инструкции требуют выравнивания. Например, в примере выше выравнивание устанавливалось по умолчанию - обе переменных автоматически было выровнены
по 16 байтам (как того требует инструкции movaps
), поскольку перед первой переменной ничего нет, а она сама занимает 4 * 32 = 128 байт, то есть размер кратный 16. Соответственно вторая переменная также была выравнена по 16 байтам.
Но возьмем другой пример программы под Linux:
global _start section .data temp1 db 1 source dd 12, 13, 14, 15 ; данные НЕ выровнены по 16 битам section .bss dest resd 4 ; данные могут быть НЕ выровнены по 16 битам section .text _start: movaps xmm0, [source] ; копируем из source в xmm0 movaps [dest], xmm0 ; копируем из xmm0 в dest mov edi, [dest] ; RDI = 12 mov rax, 60 syscall
Перед вектором source идет однобайтная переменная, поэтому он не выровнен по 16 байт, соответственно также не выровнен и вектор dest. И здесь как раз можно использовать выравнивание:
section .data temp1 db 1 align 16 ; последующие данные выровнены по 16 битам source dd 12, 13, 14, 15
Из минусов - программа занимает больше места. Однако если порядок переменных не играет значения, то лучше переупорядочить, чтобы избежать избыточных выравниваний:
section .data source dd 12, 13, 14, 15 ; данные выровнены по 16 битам dest resd 4 ; данные выровнены по 16 битам temp1 db 1.3
Если нельзя гарантировать, что переменные-операнды выровнены по 16- или 32-байтовой адресной границе, то для копирования данных между регистрами XMM или YMM и памятью можно использовать ряд других инструкций:
(v)movups
: копирование невыровненных 4-байтных чисел
(v)movupd
: копирование невыровненных 8-байтных чисел
(v)movdqu
: копирование невыровненных 16-байтных чисел
Буква u является сокрашением от "unaligned" ("невыровненный"). Работают они аналогично:
global _start section .data source dq 22, 23, 24, 25 section .bss dest resd 8 section .text _start: movupd xmm0, [source] ; копируем из source в xmm0 movupd [dest], xmm0 ; копируем из xmm0 в dest mov rdi, [dest] ; RDI = 22 mov rax, 60 syscall
Хотя считается, что эти инструкции работают в целом медленнее, чем их аналоги для выровненных данных.