Язык ассемблера - Обработка строк
Обработка строк — распространённое требование в программировании.
Обработка строк в ассемблере означает прямое манипулирование последовательностями байтов в памяти; это более низкоуровневый подход, но также более гибкий.
Определение и хранение строк
В NASM строки по сути являются последовательностями байтов и могут быть определены с помощью псевдоинструкции DB:
Пример
section .data
; Метод 1: стандартный
str1 db 'Hello, example!', 0 ; стиль C: с завершающим нулём
; Метод 2: с переводом строки
str2 db 'Line1', 0xA, 'Line2', 0xA
; Метод 3: обратные кавычки поддерживают escape-последовательности
str3 db `hello\nworld\n`, 0 ; Автоматически преобразует escape-последовательности
; Метод 4: определение посимвольно
str4 db 'A', 'B', 'C', 'D', 0
; Метод 5: использование dup для генерации повторяющихся символов
border db 40 dup('-') ; 40 дефисов
; Вычисление длины строки (во время компиляции)
str1_len equ $ - str1 ; включая завершающий null
Вычисление длины строки
Существует два способа получить длину строки: вычисление во время компиляции (EQU) и вычисление во время выполнения (обход до нахождения null):
Пример
; Два способа вычисления длины строки
section .data
; Метод A: вычисление во время компиляции (для константных строк)
msg1 db 'Hello, EXAMPLE!', 0xA
msg1_len equ $ - msg1 ; Автоматически вычисляется во время компиляции
; Метод B: строка с нулевым окончанием (вычисление во время выполнения)
msg2 db 'Find my length', 0 ; Завершается 0
section .text
global _start
_start:
; Метод A: напрямую использовать длину, вычисленную во время компиляции
mov eax, 4
mov ebx, 1
mov ecx, msg1
mov edx, msg1_len ; Использовать константу напрямую
int 0x80
; Метод B: вычислить длину строки с нулевым окончанием во время выполнения
mov esi, msg2 ; esi указывает на начало строки
mov ecx, 0 ; счётчик
strlen_loop:
cmp byte [esi], 0 ; Текущий байт равен нулю?
je strlen_done ; Да, конец
inc ecx ; счётчик + 1
inc esi ; указатель + 1
jmp strlen_loop
strlen_done:
; ecx теперь содержит длину строки (без null)
mov eax, 4
mov ebx, 1
mov ecx, msg2
mov edx, ecx ; Использовать вычисленную длину
; Вот проблема: ecx перезаписывается, сначала нужно сохранить его
; Правильный подход: push ecx, затем pop в edx
mov eax, 1
mov ebx, 0
int 0x80
Копирование строки
Используйте цикл для копирования байт за байтом или используйте строковые инструкции x86MOVSB:
Пример
; Две реализации копирования строк
section .data
src db 'example source string', 0
src_len equ $ - src
section .bss
dest_manual resb 64 ; Целевой буфер для ручного копирования
dest_fast resb 64 ; Целевой буфер для быстрого копирования
section .text
global _start
_start:
; Метод A: ручное побайтовое копирование
mov esi, src ; адрес источника
mov edi, dest_manual ; адрес назначения
mov ecx, src_len ; количество байтов
copy_loop:
mov al, [esi] ; прочитать один байт
mov [edi], al ; записать один байт
inc esi ; указатель источника++
inc edi ; указатель назначения++
loop copy_loop
; Способ B: использовать строковые инструкции (быстрее)
cld ; Сбросить флаг направления (DF=0, копирование вперёд)
mov esi, src ; адрес источника (ESI)
mov edi, dest_fast ; адрес назначения (EDI)
mov ecx, src_len ; количество байтов (ECX)
rep movsb ; повторно выполнять movsb ecx раз
; rep movsb:while(ecx>0) { [edi]=[esi]; esi++; edi++; ecx--; }
; Проверка: вывести два результата копирования
mov eax, 4
mov ebx, 1
mov ecx, dest_manual
mov edx, src_len
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, dest_fast
mov edx, src_len
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
REP MOVSBЭто самый классический метод копирования строк на x86. Однако на современных процессорах цикл ручного копирования с развёртыванием цикла может быть быстрее, чем REP MOVSB, потому что современные процессоры лучше оптимизируют конвейер для простых операций.
Сравнение строк
ИспользуйтеCMPSBвместе сREPE(повторять до несовпадения) побайтовое сравнение:
Пример
; Сравнить, равны ли две строки
section .data
str_a db 'example', 0
str_b db 'example', 0
str_c db 'EXAMPLE', 0
eq_msg db 'Строки равны', 0xA
eq_len equ $ - eq_msg
ne_msg db 'Строки НЕ равны', 0xA
ne_len equ $ - ne_msg
section .text
global _start
_start:
cld ; Прямое сравнение
mov esi, str_a ; Первая строка
mov edi, str_b ; Вторая строка
mov ecx, 7 ; Сравнить до 7 байт (включая null)
repe cmpsb ; Повторять сравнение до первого несовпадения или ecx=0
; repe: продолжать, если ZF=1 (равно) и ecx>0
; cmpsb: сравнить [esi] и [edi], затем esi++, edi++
je strings_equal ; Если ZF=1, все байты равны
; Не равны
mov eax, 4
mov ebx, 1
mov ecx, ne_msg
mov edx, ne_len
int 0x80
jmp compare_next
strings_equal:
mov eax, 4
mov ebx, 1
mov ecx, eq_msg
mov edx, eq_len
int 0x80
compare_next:
; Сравнить str_a и str_c (разный регистр)
cld
mov esi, str_a
mov edi, str_c
mov ecx, 7
repe cmpsb
jne not_equal_2
mov eax, 4
mov ebx, 1
mov ecx, eq_msg
mov edx, eq_len
int 0x80
jmp exit
not_equal_2:
mov eax, 4
mov ebx, 1
mov ecx, ne_msg
mov edx, ne_len
int 0x80
exit:
mov eax, 1
mov ebx, 0
int 0x80
Сводка инструкций строковых операций
| Инструкция | Функция | Используемые регистры |
|---|---|---|
| MOVSB | Копировать байт: [EDI] = [ESI] | ESI=источник, EDI=назначение, ECX=счётчик, DF=направление |
| MOVSW | Копировать слово (2 байта) | То же, что выше |
| MOVSD | Копировать двойное слово (4 байта) | То же, что выше |
| STOSB | Сохранить байт: [EDI] = AL | EDI=назначение, AL=значение, ECX=счётчик |
| LODSB | Загрузить байт: AL = [ESI] | ESI=источник |
| CMPSB | Сравнить байт: [ESI] - [EDI] | ESI=источник, EDI=назначение, ECX=счётчик |
| SCASB | Сканировать байт: AL - [EDI] | EDI=назначение, AL=искомое значение, ECX=счётчик |
Пример преобразования регистра
Пример
; Преобразовать строчные буквы в строке в заглавные
section .data
msg db 'Hello, example! Welcome to Assembly.', 0xA
len equ $ - msg
section .text
global _start
_start:
; Вывести исходную строку
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
; Преобразование: пройти по строке, строчные -> заглавные
mov esi, msg ; Установить указатель на начало строки
mov ecx, len ; Счётчик цикла
convert_loop:
mov al, [esi] ; Прочитать символ
cmp al, 'a' ; Это >= 'a'?
jb next_char ; Меньше 'a', пропустить
cmp al, 'z' ; Это <= 'z'?
ja next_char ; Больше 'z', пропустить
; Преобразование строчной буквы в прописную: 'a'(97) - 'A'(65) = 32
sub al, 32 ; Преобразовать в верхний регистр
mov [esi], al ; Записать обратно
next_char:
inc esi
loop convert_loop
; Вывести преобразованную строку
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
Результат выполнения:
$ nasm -f elf32 case_convert.asm -o case_convert.o $ ld -m elf_i386 case_convert.o -o case_convert $ ./case_convert Hello, example! Welcome to Assembly. HELLO, EXAMPLE! WELCOME TO ASSEMBLY.
Другие расширенияФлаг направления DF определяет направление строковых операций: когда DF=0, ESI/EDI увеличиваются (вперёд), когда DF=1 — уменьшаются (назад). Используйте
CLDчтобы сбросить DF, используйтеSTDчтобы установить DF. Перед вызовом C-функций или системных вызовов убедитесь, что DF=0 (требование cdecl).