Копирование и сохранение в строку

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

В широком смысле слова строки представляют набор значений-байтов, который хранится в непрерывной области памяти. Процессор 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

Префикс 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

Инструкции 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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850