В широком смысле слова строки представляют набор значений-байтов, который хранится в непрерывной области памяти. Процессор x86-64 поддерживает работу с 4 типами строк: строки значений byte, строки из чисел word, строки из двойных слов и строки из четверных слов qword. Для работы с этим строками процессор x86-64 предоставляет ряд инструкций для копирования, сравнения, поиска в строке и ряд других инструкций, которые далее будут рассмотрены.
Строковые инструкции используют определенные регистры в качестве операндов:
RSI
: хранит индекс значений исходной строки
RDI
: хранит индекс значений целевой строки
RCX
: счетчик значений
AL, AX, EAX
и RAX
Флаги в регистре FLAGS
Для копирования строк применяются следующие инструкции:
movsb
: извлекает байт по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает регистры RSI и RDI на 1.
movsw
: выбирает слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает RSI и RDI на 2.
movsd
: извлекает двойное слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает адрес в регистрах RSI и RDI на 4.
movsq
: извлекает четверное слово по адресу RSI, сохраняет его по адресу RDI, а затем увеличивает или уменьшает адрес в регистрах RSI и RDI на 8.
Например, скопируем одну строку в другую:
global _start section .data nums dw 10, 11, 12, 13, 14, 15, 16, 17 section .bss copy resw 8 ; 8 чисел по 2 байта равных 0 section .text _start: mov rsi, nums ; в RSI - откуда копируем mov rdi, copy ; в RDI - куда копируем mov rcx, 8 ; в RCX - сколько копируем movsw ; выполняем копирование по отдельным словам ; проверяем movzx rdi, word [copy] ; RDI=10 mov rax, 60 syscall
Здесь копируем данные из nums в copy. Для этого адрес исходной строки nums загружаем в RSI, а адрес целевой строки copy - в регистр RDI. В RCX помещаем количество элементов , которые
надо скопировать. Важно, что речь идет о количестве элементов, а не байт. Так, в данном случае мы хотим скопировать все 8 элементов из nums в copy, которые в целом занимают 16 байт.
И для копирования слов выполняем инструкцию movsw
.
Однако по умолчанию все инструкции копирования помещают только первый элемент исходной строки в целевую строку. Но нам то нужно в данном случае скопировать 8 чисел. Для повторения копирования применяется префикс rep
Префикс rep (repeat повторять) указывается перед инструкцией, которая работает со строками. Так, если префикс rep
присутствует, при выполнении инструкции
movsb
процессор проверяет RCX, чтобы увидеть, содержит ли этот регистр число 0. Если не содержит,
то процессор перемещает байт из RSI в RDI и уменьшает значение в регистре RCX.
Этот процесс повторяется до тех пор, пока RCX не станет равным 0. Если RCX содержит 0 при первоначальном выполнении, инструкция movsb не будет копировать байты данных.
Например, изменим предыдущую программу, применив rep
:
global _start section .data nums dw 10, 11, 12, 13, 14, 15, 16, 17 section .bss copy resw 8 ; 8 чисел по 2 байта равных 0 section .text _start: mov rsi, nums ; в RSI - откуда копируем mov rdi, copy ; в RDI - куда копируем mov rcx, 8 ; в RCX - сколько копируем rep movsw ; выполняем копирование по отдельным словам ; проверяем 2-элемент массива movzx rdi, word [copy+2] ; RDI=11 mov rax, 60 syscall
Префикс rep
указывает процессору повторить эту операцию столько раз, сколько указано в регистре RCX.
Стоит отметить, что конкретно в случае выше процесс копирования не является оптимальным: нам надо скопировать 8 слов или 16 байт, и это количество кратно 8. На первый взгляд для копирования
слов надо использовать инструкцию movsw
, которая собственно и предназначена для копирования слов. Но для ассемблера в данном случае нет разницы - скопирует он 8 раз по 2 байта последовательно
расположенные данные или скопирует те же данный 2 раза но по 8 байт. Поэтому в случае выше мы могли бы применить копирование по восьмеричным словам:
global _start section .data nums dw 10, 11, 12, 13, 14, 15, 16, 17 section .bss copy resw 8 ; 8 чисел по 2 байта равных 0 section .text _start: mov rsi, nums ; в RSI - откуда копируем mov rdi, copy ; в RDI - куда копируем mov rcx, 2 ; 8 слов - это два четверных слова qword rep movsq ; копируем по четверному слову qword ; проверяем 2-й элемент из copy, он должен быть равен 12 movzx rdi, word [copy+4] ; RDI=12 mov rax, 60 syscall
Если количество байт не кратно 8 или вообще нечетное, то для большей эффективности можно комбинровать копирование различных данных:
global _start section .data nums dw 10, 11, 12, 13, 14, 15, 16, 17, 18 section .bss copy resw 9 ; 9 чисел по 2 байта section .text _start: mov rsi, nums ; в RSI - откуда копируем mov rdi, copy ; в RDI - куда копируем mov rcx, 2 ; 9 слов - это 2 четверных слова + 1 слово rep movsq ; копируем по четверному слову qword movsw ; копируем одно слово word ; проверяем 8-й элемент из copy, он должен быть равен 18 movzx rdi, word [copy+16] ; RDI=18 mov rax, 60 syscall
В данном случае для копирования данных потребуется две инструкции movsq
и одна инструкция movsw
.
Флаг направления в регистре FLAGS управляет навправлением копирования элементво строк. Если флаг направления сброшен, процессор увеличивает RSI и RDI после обработки каждого элемента строки.
Например, инструкции movs
скопируют байт, слово, двойное слово или четверное слово из RSI в RDI, а затем увеличат RSI и RDI на 1, 2, 4 или 8
(соответственно). Если флаг направления установлен, x86-64 уменьшает RSI и RDI после обработки каждого элемента строки.
Для управления флагами направления применяются инструкции cld (сброс флага направления) и std (установка флага направления). Например, скопируем данные из одной строки в другую в противоположном направлении:
global _start section .data nums dw 10, 11, 12, 13, 14, 15, 16, 17 ; исходный массив len equ $-nums ; размер массива в байтах elemSize equ 2 ; размер одного элемента count equ len / elemSize ; количество элементов lastPosition equ len - elemSize ; смещение последнего элемента относительно начала section .bss copy resw 8 ; 8 чисел по 2 байта section .text _start: mov rsi, nums add rsi, lastPosition ; указывает на последний элемент mov rdi, copy add rdi, lastPosition ; указывает на последний элемент mov rcx, count ; RCX = 8 std ; устанавливаем флаг направления rep movsw mov rdi, [copy+14] ; проверяем 7-й элемент из copy, он должен быть равен 17 mov rax, 60 syscall
Здесь регистры RSI и RDI перед копированием строки указывают на последний элемент строки. И при вызове инструкции movsw
сначала копируются последниие элементы строки,
потом предпоследние и так далее вплоть до первого элемента.
Инструкции stos сохраняют определенное значение на определенную позицию в строке. Эта инструкция также имеет 4 вида:
STOSB: сохраняет 8-битное значение из регистра AL в строку
STOSW: сохраняет 16-битное значение из регистра AX в строку
STOSD: сохраняет 32-битное значение из регистра EAX в строку
STOSQ: сохраняет 64-битное значение из регистра RAX в строку
Для работы этой инструкции требуется установить 3 регистра:
RDI: адрес строки, в которую сохраняем символ
AL/AX/EAX/RAX: значение, которое надо сохранить в строку
RCX: сколько раз значение из регистра AL/AX/EAX/RAX должно быть сохранено в строку
Рассмотрим следующую программу под Linux:
global _start section .data message db "Hello" ; исходная строка len equ $-message char db "A" section .text _start: mov rdi, message ; загружаем адрес переменной message mov al, [char] ; устанавливаем символ stosb ; заменяем символ ; проверяем строку - выводим на консоль mov rdi, 1 mov rsi, message mov rdx, len mov rax, 1 syscall mov rax, 60 syscall
Здесь в регистр RDI помещаем адрес строки "Hello". В регистр AL помещаем символ "A" (именно сам символ, а не его адрес). Затем с помощью инструкции stosb выполняем сохранение байта из AL в строку. То есть в итоге мы получим строку "Aello". При выполнении инструкции stosb она копирует значение из AL по адресу, на который указывает RDI. Затем увеличивает значение адреса в RDI на размер копируемых данных (в данном случае на 1 байт).
Однако это было однократное копирование. Если нам надо многократно скопировать некоторое значение в строку, то применяется префикс rep. При каждом копировании он уменьшает значение в RCX и, если RCX равно нулю, завершает копирование. Подобная инструкция удобна, когда нам надо инициализировать некоторый массив данных/строку определенным значением. Например:
global _start section .data char db "A" ; символ для замены section .bss ; неинициализированные данные buffer resb 8 ; строка из 8 байтов len equ $-buffer section .text _start: mov rdi, buffer ; загружаем адрес строки mov al, [char] ; устанавливаем символ mov rcx, len ; сколько раз выполнить замену rep stosb ; заменяем символ ; проверяем строку - выводим на консоль mov rdi, 1 mov rsi, buffer mov rdx, len mov rax, 1 syscall mov rax, 60 syscall
Здесь RCX указывает, сколько раз выполнить инструкцию stosb
. Поскольку заменяем байты, то соответственно это число равно количеству байт в строке buffer. В итоге мы получим строку
"AAAAAAAA".
Также, как и в случае с инструкциями movs
, для инструкций stos можно использовать флаг направления - если он установлен, замена идет с конца строки, на которую указывает RDI.