Язык ассемблера - Обработка строк

Обработка строк — распространённое требование в программировании.

Обработка строк в ассемблере означает прямое манипулирование последовательностями байтов в памяти; это более низкоуровневый подход, но также более гибкий.


Определение и хранение строк

В 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):

Пример

; Путь к файлу: strlen_demo.asm
; Два способа вычисления длины строки

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:

Пример

; Путь к файлу: strcpy_demo.asm
; Две реализации копирования строк

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(повторять до несовпадения) побайтовое сравнение:

Пример

; Путь к файлу: strcmp_demo.asm
; Сравнить, равны ли две строки

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] = ALEDI=назначение, AL=значение, ECX=счётчик
LODSBЗагрузить байт: AL = [ESI]ESI=источник
CMPSBСравнить байт: [ESI] - [EDI]ESI=источник, EDI=назначение, ECX=счётчик
SCASBСканировать байт: AL - [EDI]EDI=назначение, AL=искомое значение, ECX=счётчик

Пример преобразования регистра

Пример

; Путь к файлу: case_convert.asm
; Преобразовать строчные буквы в строке в заглавные

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).

Другие расширения