Строки

Операции со строками

Последнее обновление: 03.07.2023

Строки представляют набор значений, который хранится в непрерывной области памяти. Процессор x86-64 поддерживает работу с 4 типами строк: строки значений byte, строки из чисел word, строки из двойных слов и строки из четверных слов. Для работы с этим строками процессор 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.

Например, скопируем одну строку в другую:

.data
nums1 word 10, 11, 12, 13, 14, 15, 16, 17
nums2 word 8 dup (?)
.code
main proc
    lea rsi, nums1
    lea rdi, nums2
    mov rcx, 8  ; 8 элементов в nums1
    movsw
    ret
main endp
end

Здесь копируем данные из nums1 в nums2. Для этого адрес исходной строки nums1 загружаем в RSI, а адрес целевой строки nums2 - в регистр RDI. В RCX помещаем количество элементов , которые надо скопировать. Важно, что речь идет о количестве элементов, а не байт. Так, в данном случае мы хотим скопировать все 8 элементов из nums1 в nums2, которые в целом занимают 16 байт. И для копирования слов выполняем инструкцию movsw.

Однако по умолчанию все инструкции копирования помещают только первый элемент исходной строки в целевую строку. Но нам то нужно в данном случае скопировать 8 чисел. Для повторения копирования применяется префикс rep

Префикс rep

Префикс rep (repeat повторять) указывается перед инструкцией, которая работает со строками. Так, если префикс rep присутствует, при выполнении инструкции movsb процессор проверяет RCX, чтобы увидеть, содержит ли этот регистр число 0. Если не содержит, то процессор перемещает байт из RSI в RDI и уменьшает значение в регистре RCX. Этот процесс повторяется до тех пор, пока RCX не станет равным 0. Если RCX содержит 0 при первоначальном выполнении, инструкция movsb не будет копировать байты данных.

Например, изменим предыдущую программу, применив rep:

.data
nums1 word 10, 11, 12, 13, 14, 15, 16, 17
nums2 word 8 dup (?)
.code
main proc
    lea rsi, nums1
    lea rdi, nums2
    mov rcx, 8
    rep movsw

    movzx rax, nums2[4] ; проверяем 3-й элемент из nums2, он должен быть равен 12
    ret
main endp
end

Префикс rep указывает процессору повторить эту операцию столько раз, сколько указано в регистре RCX.

Стоит отметить, что конкретно в случае выше процесс копирования не является оптимальным: нам надо скопировать 8 слов или 16 байт, и это количество кратно 8. На первый взгляд для копирования слов надо использовать инструкцию movsw, которая собственно и предназначена для копирования слов. Но для ассемблера в данном случае нет разницы - скопирует он 8 раз по 2 байта последовательно расположенные данные или скопирует теже данный 2 раза но по 8 байт. Поэтому в случае выше мы могли бы применить копирование по восьмеричным словам:

.data
nums1 word 10, 11, 12, 13, 14, 15, 16, 17
nums2 word 8 dup (?)
.code
main proc
    lea rsi, nums1
    lea rdi, nums2
    mov rcx, 2      ; 8 слов - это два четверных слова
    rep movsq       ; копируем по четверному слову

    movzx rax, nums2[4] ; проверяем 3-й элемент из nums2, он должен быть равен 12
    ret
main endp
end

Если количество байто не кратно 8 или вообще нечетное, то для большей эффективности можно комбинровать копирование различных данных:

.data
nums1 word 10, 11, 12, 13, 14, 15, 16, 17, 18
nums2 word 9 dup (?)
.code
main proc
    lea rsi, nums1
    lea rdi, nums2
    mov rcx, 2      ; 9 слов - это 2 четверных слова + 1 слово
    rep movsq       ; копируем по четверному слову
        movsw       ; копируем одно слово

    movzx rax, nums2[8*2] ; RAX = 18
    ret
main endp
end

В данном случае для копирования данных потребуется две инструкции movsq и одна инструкция movsw. Даная схема наиболее эффективна, если оба массива выровнены по границам четырех слов. Если подобного выравнивания нет, то можно переместить инструкции, которые копируют меньшее количество данных - в данном случае movsw до инструкции movsq, чтобы movsq работала с данными, выровненными по четырём словам:

.data
rnd1 dword 1
rnd2 word 2 
nums1 word 10, 11, 12, 13, 14, 15, 16, 17, 18 ; не выровнено по четверным словам
nums2 word 9 dup (?)
.code
main proc
    lea rsi, nums1
    lea rdi, nums2
    mov rcx, 2      ; 9 слов - это два четверных слова + 2 двойных слова
    movsw           ; копируем 1 слово
    rep movsq       ; копируем по четверному слову

    movzx rax, nums2[6*2] ; RAX = 18
    ret
main endp
end

Направление копирования

Флаг направления в регистре FLAGS управляет навправлением копирования элементво строк. Если флаг направления сброшен, процессор увеличивает RSI в RDI после обработки каждого элемента строки. Например, инструкции movs скопируют байт, слово, двойное слово или четверное слово из RSI в RDI, а затем увеличат RSI и RDI на 1, 2, 4 или 8 (соответственно). Если флаг направления установлен, x86-64 уменьшает RSI в RDI после обработки каждого элемента строки.

Для управления флагами направления применяются инструкции cld (сброс флага направления) и std (установка флага направления). Например, скопируем данные из одной строки в другую в противоположном направлении:

.data
nums1 word 10, 11, 12, 13, 14, 15, 16, 17
len = $ - nums1         ; размер массива
count = len / 2         ; количество элементов
elemSize = 2            ; размер одного элемента
nums2 word 8 dup (?)
.code
main proc
    lea rsi, nums1
    add rsi, len - elemSize ; указывает на последний элемент
    lea rdi, nums2
    add rdi, len - elemSize ; указывает на последний элемент
    
    mov rcx, count; RCX = 8
    std         ; устанавливаем флаг направления
    rep movsw
    movzx rax, nums2[7 * elemSize] ; проверяем последний элемент, он должен быть равен 17
    ret
main endp
end

Здесь регистры RSI и RDI перед копированием строки указывают на последний элемент строки. И при вызове инструкции movsw сначала копируются последниие элементы строки, потом предпоследние и так далее вплость до первого элемента.

Стоит отметить, что при работе с внешними API могут применяться определенные требования к флагам направления. Так, Microsoft ABI требует, чтобы флаг направления был сброшен при вызове функции, которая совместима с Microsoft ABI (например, функции C/C++ на Windows). Поэтому, если внутри процедуры на ассемблере требуется установить флаг направления, то после завершения использования флага его надо сбросить (в частности перед вызовом любого другого кода или возвратом из процедуры).

Копирование в обратном направлении может быть применяться, когда нам надо сдвинуть элементы строки. Например:

.data
nums1 word 0
nums2 word 10, 11, 12, 13, 14, 15, 16, 17
len = $ - nums2   ; размер массива
count = len / 2      ; количество элементов
elemSize = 2            ; размер одного элемента
.code
main proc
    lea rsi, nums1
    add rsi, len - elemSize ; указывает на предпоследний элемент nums2
    lea rdi, nums2
    add rdi, len - elemSize     ; указывает на последний элемент
    
    mov rcx, count; RCX = 8
    std         ; устанавливаем флаг направления
    rep movsw
    movzx rax, nums2[7*elemSize] ; проверяем 7-й (последний) элемент, он должен быть равен 16
    ret
main endp
end

Здесь следует отметить одну особенность - поскольку данные в памяти будут располагаться последовательно, и за числом nums1 сразу будет идти nums2, то на уровне программы мы можем рассматривать nums1 не просто как одиночное число, но и как массив, который перекрывает nums2. Соответственно выражение nums1[7*elemSize] - аналогично обращению к 6-му элементу массива nums2 (nums1[6*elemSize]). И при установки источника копирования регистр RSI содержит адрес предпоследнего элемента в nums2:

lea rsi, nums1
add rsi, len - elemSize

Хотя в принципе мы могли бы использовать в качестве источника и массив nums2:

lea rsi, nums2
add rsi, len - 2 * elemSize ; указывает на предпоследний элемент

В итоге при копировании последний элемент nums1 (предпоследний элемент nums2) копируется в последний элемент nums2 и так далее:

0, 10, 11, 12, 13, 14, 15, 16, 17
0,  0, 10, 11, 12, 13, 14, 15, 16
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850