На предыдущих уроках мы уже использовали строки разной длины.
Строки в Ассемблере
Строки с переменной в качестве длины могут содержать столько символов, сколько необходимо. Как правило, длина строки указывается одним из следующих двух способов:
явное содержание длины строки;
использование сигнального символа.
Мы можем явно хранить длину строки, используя символ счетчика местоположения $
, который предоставляет текущее значение счетчика местоположения строки. Например:
1 2 |
msg db 'Hello, world!',0xa ; наша строка len equ $ - msg ; длина нашей строки |
Символ $
указывает на byte после последнего символа строковой переменной msg
. Следовательно, $ - msg
указывает на длину строки. Мы также можем написать:
1 2 |
msg db 'Hello, world!',0xa ; наша строка len equ 13 ; длина нашей строки |
В качестве альтернативы мы можем хранить строки с завершающим сигнальным символом. Сигнальным символом является специальный символ, который не должен находиться внутри строки. Например:
1 |
message DB 'I am loving it!', 0 |
Строковые инструкции
Каждая строковая инструкция может требовать исходного операнда и операнда назначения. Для 32-битных сегментов строковые инструкции используют регистры ESI и EDI, чтобы указать на операнды источника и назначения, соответственно.
Однако для 16-битных сегментов, чтобы указать на источник и место назначения, используются другие регистры: SI и DI.
Существует 5 основных инструкций для работы со строками в Ассемблере:
MOVS
— эта инструкция перемещает 1 byte, word или doubleword данных из одной ячейки памяти в другую;
LODS
— эта инструкция загружается из памяти. Если операндом является значение типа byte, то оно загружается в регистр AL, если типа word — загружается в регистр AX, если типа doubleword — загружается в регистр EAX;
STOS
— эта инструкция сохраняет данные из регистра (AL, AX или EAX) в память;
CMPS
— эта инструкция сравнивает два элемента данных в памяти. Данные могут быть размера byte, word или doubleword;
SCAS
— эта инструкция сравнивает содержимое регистра (AL, AX или EAX) с содержимым элемента, находящегося в памяти.
Каждая из вышеприведенных инструкций имеет версии byte, word или doubleword, а строковые инструкции могут повторяться с использованием префикса повторения.
Эти инструкции используют пары регистров ES:DI
и DS:SI
, где регистры DI и SI содержат валидные адреса смещения, которые относятся к байтам, хранящимся в памяти. SI обычно ассоциируется с DS (сегмент данных), а DI всегда ассоциируется с ES (дополнительный сегмент).
Регистры DS:SI
(или ESI) и ES:DI
(или EDI) указывают на операнды источника и назначения, соответственно. Предполагается, что операндом-источником является DS:SI
(или ESI), а операндом назначения — место в памяти, на которое указывает пара ES:DI
(или EDI).
Для 16-битных адресов используются регистры SI и DI, а для 32-битных адресов используются регистры ESI и EDI.
В следующей таблице представлены различные версии строковых инструкций и предполагаемое место операндов:
Основная инструкция | Операнды в: | Операция byte | Операция word | Операция doubleword |
MOVS | ES:DI, DS:SI | MOVSB | MOVSW | MOVSD |
LODS | AX, DS:SI | LODSB | LODSW | LODSD |
STOS | ES:DI, AX | STOSB | STOSW | STOSD |
CMPS | DS:SI, ES:DI | CMPSB | CMPSW | CMPSD |
SCAS | ES:DI, AX | SCASB | SCASW | SCASD |
Инструкция MOVS
Инструкция MOVS используется для копирования элемента данных (byte, word или doubleword) из исходной строки в строку назначения. Исходная строка указывается с помощью DS:SI
, а строка назначения указывается с помощью ES:DI
.
Рассмотрим это на практике:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку mov ecx, len mov esi, s1 mov edi, s2 cld rep movsb mov edx,20 ; длина сообщения mov ecx,s2 ; сообщение для вывода на экран mov ebx,1 ; файловый дескриптор (stdout) mov eax,4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov eax,1 ; номер системного вызова (sys_exit) int 0x80 ; вызов ядра section .data s1 db 'Hello, world!',0 ; первая строка len equ $-s1 section .bss s2 resb 20 ; назначение |
Результат:
Инструкция LODS
В криптографии шифр Цезаря является одним из самых простых известных методов шифрования. В этом методе каждый символ в открытом тексте заменяется символом, находящимся на некотором постоянном числе позиций левее или правее него в алфавите.
В этом примере давайте зашифруем данные, просто заменив каждую букву алфавита со сдвигом в две буквы, таким образом A
будет заменено на C
, B
на D
и т.д.
Мы используем инструкцию LODS для помещения оригинальной строки password
в память:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку mov ecx, len mov esi, s1 mov edi, s2 loop_here: lodsb add al, 02 stosb loop loop_here cld rep movsb mov edx,20 ; длина сообщения mov ecx,s2 ; сообщение для вывода на экран mov ebx,1 ; файловый дескриптор (stdout) mov eax,4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov eax,1 ; номер системного вызова (sys_exit) int 0x80 ; вызов ядра section .data s1 db 'password', 0 ; источник len equ $-s1 section .bss s2 resb 10 ; назначение |
Результат:
rcuuyqtf
Инструкция STOS
Инструкция STOS копирует элемент данных из регистров AL (для byte — STOSB), AX (для word — STOSW) или EAX (для doubleword — STOSD) в строку назначения, на которую указывает ES:DI
в памяти.
В следующем примере мы с помощью инструкций LODS и STOS будем конвертировать строки из верхнего регистра в нижний регистр:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку mov ecx, len mov esi, s1 mov edi, s2 loop_here: lodsb or al, 20h stosb loop loop_here cld rep movsb mov edx,20 ; длина сообщения mov ecx,s2 ; сообщение для вывода на экран mov ebx,1 ; файловый дескриптор (stdout) mov eax,4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov eax,1 ; номер системного вызова (sys_exit) int 0x80 ; вызов ядра section .data s1 db 'HELLO, WORLD', 0 ; источник len equ $-s1 section .bss s2 resb 20 ; назначение |
Результат:
Инструкция CMPS
Инструкция CMPS сравнивает две строки. Эта инструкция сравнивает два элемента данных одного byte, word или doubleword, на которые указывают регистры DS:SI
и ES:DI
, и устанавливает соответствующие флаги. Вы также можете использовать инструкции прыжков вместе с этой инструкцией.
В следующем примере мы будем сравнивать две строки, используя инструкцию CMPS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку mov esi, s1 mov edi, s2 mov ecx, lens2 cld repe cmpsb jecxz equal ; выполняем прыжок, когда ECX равен нулю ; Если не равно, то выполняем следующий код mov eax, 4 mov ebx, 1 mov ecx, msg_neq mov edx, len_neq int 80h jmp exit equal: mov eax, 4 mov ebx, 1 mov ecx, msg_eq mov edx, len_eq int 80h exit: mov eax, 1 mov ebx, 0 int 80h section .data s1 db 'Hello, world!',0 ; наша первая строка lens1 equ $-s1 s2 db 'Hello, there!', 0 ; наша вторая строка lens2 equ $-s2 msg_eq db 'Strings are equal!', 0xa len_eq equ $-msg_eq msg_neq db 'Strings are not equal!' len_neq equ $-msg_neq |
Результат:
Инструкция SCAS
Инструкция SCAS используется для поиска определенного символа или набора символов в строке. Искомый элемент данных должен находиться в регистрах AL (для SCASB), AX (для SCASW) или EAX (для SCASD). Искомая строка должна находиться в памяти, и на нее должны указывать ES:DI
(или EDI
).
Рассмотрим использование инструкции SCAS на практике:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку mov ecx,len mov edi,my_string mov al , 'e' cld repne scasb je found ; когда нашли ; Если не нашли, то выполняем следующее mov eax,4 mov ebx,1 mov ecx,msg_notfound mov edx,len_notfound int 80h jmp exit found: mov eax,4 mov ebx,1 mov ecx,msg_found mov edx,len_found int 80h exit: mov eax,1 mov ebx,0 int 80h section .data my_string db 'hello world', 0 len equ $-my_string msg_found db 'found!', 0xa len_found equ $-msg_found msg_notfound db 'not found!' len_notfound equ $-msg_notfound |
Результат:
Префиксы повторения
Префикс REP, если он установлен перед строковой инструкцией (например, REP MOVSB
), вызывает повторение инструкции на основе счетчика, размещенного в регистре CX. REP
выполняет инструкцию, уменьшает CX на 1 и проверяет, равен ли CX нулю. Он повторяет обработку инструкций, пока CX не станет равным нулю.
Флаг направления (DF) определяет направление операции:
Используйте CLD
(англ. «Clear Direction Flag» = «Сбросить флаг направления», DF = 0
), чтобы выполнить операцию слева направо.
Используйте STD
(англ. «Set Direction Flag» = «Установить флаг направления», DF = 1
), чтобы выполнить операцию справа налево.
Префикс REP также имеет следующие вариации:
REP
— это безусловное повторение, которое повторяет операцию, пока CX не станет равным нулю.
REPE
или REPZ
— это условное повторение, которое повторяет операцию до тех пор, пока нулевой флаг (ZF) указывает на равенство нулю результата (сам ZF установлен в единицу). Повторение останавливается, когда ZF указывает на неравенство результата нулю (ZF сброшен в ноль), или когда CX равен нулю.
REPNE
или REPNZ
— это также условное повторение, которое повторяет операцию до тех пор, пока нулевой флаг указывает на неравенство нулю результата. Повторение останавливается, когда ZF указывает на равенство нулю результата, или когда CX уменьшается до нуля.
Такой небольшой вопрос возник, зачем мы используем инструкцию rep movsb дляф перемещения строки из si в di если инструкция stosb и так это делает, перемещая изменённую на +2 строку сразу в di? У меня все работает и без rep movsb
Ага, я тоже это подметил)