Строковые инструкции и таблицы поиска
Контекст: x86/x86-64, NASM. Краткая таблица мнемоник есть в справочнике; здесь — как ими пользоваться осознанно.
Идея "строковых" команд
Play ITЗагрузка интерактивного демо…
Группа инструкций с суффиксом B/W/D/Q (байт, слово, dword, qword) работает с парой адресов, заданных неявно:
| Регистр | Роль |
|---|---|
RSI (или ESI, SI) | источник — "откуда читаем" |
RDI (или EDI, DI) | приёмник — "куда пишем" или "что сканируем" |
RCX | счётчик повторений (с префиксом REP) |
AL / AX / EAX / RAX | операнд для STOS, SCAS, LODS |
Одна мнемоника без REP выполняется один раз и сдвигает RSI/RDI на размер элемента. С префиксом REP / REPE / REPNE — в цикле, пока RCX ≠ 0 и (для E/NE) пока выполняется условие по ZF.
Эти инструкции особенно полезны, когда нужно писать компактный и понятный "побайтовый" код — копирование, сканирование, заполнение буфера, простые парсеры. Они не всегда абсолютные чемпионы по скорости, но часто дают хороший баланс между размером кода и читаемостью.
Направление — CLD и STD
Play ITЗагрузка интерактивного демо…
Флаг DF (Direction Flag) в RFLAGS:
CLD— DF = 0: после операцииRSIиRDIувеличиваются (обход вперёд).STD— DF = 1: указатели уменьшаются (удобно при копировании с конца буфера).
Перед любым блоком строковых команд задайте направление явно, обычно CLD.
Копирование с конца буфера (когда области пересекаются):
; rsi = src, rdi = dst, rcx = count
std
mov rsi, src_end
mov rdi, dst_end
rep movsb
cld ; вернуть DF=0 для последующего кода
Разбор:
stdустанавливает Direction Flag: послеmovsbуказатели уменьшаются.src_endиdst_end— адреса после последнего байта (src + count, dst + count).rep movsbкопирует с конца в начало, как вmemmoveпри перекрытии вперёд.cldв конце обязателен: иначе следующие строковые инструкции пойдут "назад" по ошибке.- Без
std/cldлегко получить тихий баг в смешанном коде.
Копирование блока — MOVSB / MOVSD
Эквивалент memcpy для непересекающихся областей:
; rsi = src, rdi = dst, rcx = байт (для movsb)
cld
rep movsb
Разбор:
- Комментарий фиксирует входные данные — источник в
RSI, приёмник вRDI, число байт вRCX. cldсбрасывает Direction Flag, поэтому указатели двигаются вперёд.rep movsbкопирует по одному байту из[RSI]в[RDI], автоматически инкрементируя оба регистра.- Инструкция повторяется
RCXраз, после чегоRCXстановится нулём. - Это компактный низкоуровневый эквивалент
memcpyдля непересекающихся областей.
Для выровненных блоков кратных 4 или 8 байтам быстрее rep movsd / rep movsq, если границы позволяют.
Современные CPU часто быстрее на mov из обычного цикла или внутренней memcpy libc из-за микрокода и предвыборки; строковые инструкции остаются компактными в коде и полезны в ядрах, загрузчиках и учебных примерах.
Заполнение буфера — STOSB
; rdi = начало, rcx = длина, al = байт-заполнитель
cld
rep stosb
Разбор:
RDIуказывает на начало заполняемой области памяти.- В
ALхранится байт, которым будет заполнен буфер. RCXзадаёт количество повторений, то есть длину области в байтах.cldвключает движение вперёд по памяти.rep stosbпоследовательно записываетALв память и увеличиваетRDIдо полного заполнения.
Обнуление области: xor al, al + rep stosb.
Полная подготовка перед rep movsb:
section .bss
buf_dst resb 64
section .rodata
buf_src db 'copy me', 0
section .text
lea rsi, [rel buf_src]
lea rdi, [rel buf_dst]
mov rcx, 7 ; 7 байт без терминатора
cld
rep movsb
Разбор:
lea rsi, [rel buf_src]иlea rdi, [rel buf_dst]задают источник и приёмник.mov rcx, 7— явная длина копирования (не включает завершающий0, если он есть).cldфиксирует движение указателей вперёд.- После
rep movsbвbuf_dstлежат первые 7 байт строки'copy me'. RSIиRDIсдвинуты на 7,RCXобнулён — регистры "израсходованы" префиксомREP.
Обнуление буфера:
lea rdi, [rel buf_dst]
mov rcx, 64
xor al, al
cld
rep stosb
Разбор:
xor al, alформирует заполнитель0быстрее, чемmov al, 0.rep stosbзаписывает ноль в 64 байта подряд — аналогmemset(buf, 0, 64).- После операции
RDIуказывает на байт сразу за концом буфера.
Длина C-строки — SCASB
Поиск нулевого терминатора:
; rdi = строка, al = 0
cld
xor rcx, rcx
not rcx ; rcx = -1 (максимальный счёт)
xor al, al
repne scasb
not rcx
dec rcx ; rcx = число байт до (не включая) '\0'
Разбор:
cldзадаёт прямое направление сканирования строки.xor rcx, rcxзатемnot rcxустанавливаютRCX = -1, то есть максимально длинный лимит поиска.xor al, alподготавливает искомый байт0(терминатор C-строки).repne scasbсравниваетALс байтами по адресуRDIи двигается вперёд, пока байты не совпадут.- После выхода
RCXсодержит остаток счётчика, поэтомуnot rcxиdec rcxпреобразуют его в длину строки без\0. - Приём повторяет классический алгоритм
strlenна строковых инструкциях x86.
REPNE SCASB останавливается, когда найден AL или исчерпан "длинный" счётчик.
Поиск символа в буфере
; rdi = буфер, rcx = известная длина, al = '?'
cld
repne scasb
je .found
; не найдено (ZF=0 после repne scasb если не совпало на последнем)
.found:
; rdi указывает на байт *после* совпадения
Разбор:
- Поиск ограничен известной длиной буфера в
RCX, поэтому выход за границы исключается. ALсодержит искомый символ, например'?'.repne scasbидёт по байтам, пока не найдёт совпадение или пока не обнулится счётчик.je .foundсрабатывает только если установленZF=1, то есть совпадение найдено.- После нахождения
RDIуже сдвинут на позицию после найденного байта, это важно для последующей логики.
Проверяйте ZF после REPNE: совпадение — ZF=1.
Сравнение двух блоков — CMPSB
cld
repe cmpsb ; пока равны и rcx > 0
jne .not_equal
Разбор:
cmpsbсравнивает байт по[RSI]с байтом по[RDI]и продвигает оба указателя.- Префикс
repeповторяет сравнение, пока байты равны (ZF=1) иRCXещё не ноль. - Если найдено различие,
ZFсбрасывается и цикл останавливается. jne .not_equalпереводит выполнение в ветку неравенства.- Если переход не выполнен, либо все байты равны, либо сравнение завершилось по лимиту
RCX.
Таблица поиска (lookup table)
Частая задача: по коду символа или индексу 0..255 получить значение из статической таблицы (классификация, шифр подстановки, палитра).
Подход 1 — явный индекс (64-бит):
section .rodata
; 256 байт: class_table[i] = категория символа i
class_table:
db 0, 0, 0, 0, 0, 0, 0, 0 ; 0..7
; ... остальные 248 байт ...
section .text
movzx rax, byte [rdi] ; rdi -> входной байт
lea rbx, [rel class_table]
mov al, [rbx + rax]
Разбор:
- Таблица
class_tableхранит предвычисленную категорию для каждого байта0..255. movzx rax, byte [rdi]читает входной байт и расширяет его до 64-битного индекса.lea rbx, [rel class_table]получает базовый адрес таблицы в позиционно-независимом стиле.mov al, [rbx + rax]извлекает значение таблицы по индексу.- Такой lookup заменяет длинные цепочки
cmp/jccи даёт O(1)-доступ.
Подход 2 — XLATB (историческая мнемоника):
mov rbx, table_base ; в 64-битном режиме база таблицы в RBX
mov al, [rsi] ; индекс в AL (0..255)
xlatb ; AL := byte [RBX + AL]
Разбор:
RBXзадаёт базовый адрес таблицы преобразования.ALсодержит индекс, поэтому диапазон ограничен0..255.xlatbавтоматически читает байт по адресуRBX + ALи возвращает его вAL.- Инструкция удобна для компактного кода, но скрывает адресацию и хуже читается в современных проектах.
XLATB эквивалентен mov al, [rbx + al] с ограничением индекса байтом. В новом коде чаще пишут явную форму — читаемее и проще для отладки.
Таблица слов (не байт): индекс умножают на размер элемента:
mov eax, [index]
lea rbx, [rel words_table]
mov edx, [rbx + rax*4]
Разбор:
mov eax, [index]загружает индекс элемента таблицы.lea rbx, [rel words_table]получает базу массива 32-битных слов.- Масштаб
*4в адресации учитывает размер одногоdword. mov edx, [rbx + rax*4]читает нужный элемент таблицы вEDX.- Подход подходит для таблиц
int32, переходов состояний и заранее подготовленных коэффициентов.
См. типы данных про endianness и выравнивание.
Где lookup table реально полезна
Типовые практические кейсы:
- Быстрая классификация символов (
digit,space,delimiter) в парсере. - Замена длинной цепочки
cmp/jeна O(1) доступ по индексу. - Табличная конвертация (например, hex-символ -> значение 0..15).
- Лёгкие конечные автоматы, где состояние и вход дают индекс в таблицу переходов.
Если логика повторяется для диапазона значений, таблица почти всегда выигрывает в простоте.
Таблица цифры '0'–'9' → значение 0..9:
section .rodata
digit_val:
times '0', db -1
db 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
times 256 - ('9' + 1), db -1
section .text
movzx rax, byte [rdi] ; rdi -> символ, например '7'
lea rbx, [rel digit_val]
movsx eax, byte [rbx + rax] ; eax = 7 или -1
Разбор:
times '0', db -1заполняет байты0..47значением "ошибка" (-1как signed byte).- Десять байт
0..9соответствуют кодам символов'0'..'9'. - Хвост
times 256 - ('9' + 1), db -1помечает все остальные коды как недопустимые. - Индекс — ASCII-код символа, доступ
digit_val[code]за O(1). movsx eax, byte [...]даёт знаковый результат:-1легко проверить черезjs/cmp eax, 0.- Для hex добавляют ещё диапазон
'A'..'F'и'a'..'f'в ту же таблицу 256 байт.
Чтение следующего байта — LODSB:
mov rsi, src_ptr
lodsb ; al = [rsi], rsi++
Разбор:
LODSBзагружает байт из[RSI]вALи увеличиваетRSI(приDF=0).- Удобно в ручных парсерах, где нужно идти по входному буферу без явного
mov/inc. - Перед циклом с
lodsbобычно вызываютcld. RSIв Linux x86-64 — caller-saved; при вызовах функций его нужно сохранять или использовать после всехcall.
Соглашения и осторожности
| Тема | Замечание |
|---|---|
Перекрытие src/dst при movs | при пересечении регионов поведение как у memmove — нужен другой алгоритм |
RSI/RDI в вызовах ABI | в Linux x86-64 RSI/RDI — аргументы; сохраняйте callee-saved или используйте после настройки вызова |
RCX | caller-saved; REP портит счётчик |
| Строки UTF-8 | побайтовый разбор; один символ — 1–4 итерации, не один SCAS |
Связанные материалы
- Взаимодействие с C — строки как
char *. - Чтение листинга — как выглядит
rep movsbв дизассемблере. - Векторная обработка нескольких байт параллельно — числа с плавающей точкой и SIMD.