Положил `http://` в строку - и ассемблер молча собрал 0 байт
В предыдущей части серии я обещал заняться разбором исходного текста на отдельные элементы: определить, где заканчивается одно слово и начинается следующее. Казалось, задача будет простой. Ассемблер читал строки, удалял комментарии, проверял наличие меток, а оставшуюся часть делил по запятым.
Но почти сразу нашёлся пример, на котором вся эта схема дала сбой:
```asm
.string "http://example.com"
```
Мой ассемблер обработал строку без единого сообщения об ошибке, завершился с кодом успеха и выдал ноль байт. При этом проблема была не в кодировке инструкции и не в директиве `.string`: исходная строка исчезла ещё до этапа генерации данных.
В этой серии я создаю ассемблер с нуля на Python. Он получает текст вроде:
```asm
addi sp, sp, -16
```
и преобразует его в четыре байта, которые понимает процессор. К пятой части программа уже умела собирать одну тестовую программу, причём результат совпадал с GNU `as` байт в байт. Однако анализ исходников пока оставался слишком примитивным: комментарий отрезался по специальному символу, метка определялась по двоеточию, а операнды разделялись запятыми.
Такой подход знаком всем, кто когда-либо разбирал текст простым `split()`. Запятая внутри CSV-значения не является разделителем, точка с запятой внутри строки не начинает комментарий, а кавычка внутри экранированного литерала не завершает его. В ассемблере происходит то же самое, только вместо запятой или точки с запятой неожиданно опасными оказываются две косые черты.
Как сломался разбор
Обычно мой ассемблер ведёт себя предсказуемо: если встречает непонятную конструкцию, останавливается и указывает номер строки. Это неудобно, но ошибку легко найти. В случае с `http://` всё выглядело успешно.
Трассировка показала следующую последовательность:
1. Правило удаления комментариев увидело `//` и оставило от строки только:
```asm
.string "http:
```
2. Затем обработчик меток заметил двоеточие в конце оставшегося фрагмента.
3. В результате появилась метка с именем `.string "http`, внутри которого оказались пробел и кавычка.
4. Директива строки до генератора машинного кода уже не дошла.
Каждое правило само по себе выглядело разумно. Всё после `//` действительно часто является комментарием, а двоеточие может обозначать метку. Ошибка возникла из-за того, что ни один обработчик не понимал: сейчас он находится внутри строкового литерала.
Именно поэтому тема "ассемблер строки и директивы .string" на практике оказывается не только вопросом синтаксиса директив данных. Нужно учитывать контекст, в котором встретился каждый символ. Полезное описание этого эксперимента и дальнейшего перехода к посимвольному разбору можно найти в материале о разборе строковых литералов в собственном ассемблере.
Почему простые заплатки не помогли
Первое очевидное решение - при удалении комментария помнить, находится ли текущий символ внутри двойных кавычек. Я реализовал именно его. Затем добавил поддержку одиночных кавычек: получилось два независимых флага, каждый отвечал за свой тип ограничителя.
На небольшом тестовом наборе результат выглядел убедительно. Но вторая правка исправила одну строку и одновременно испортила другую:
```asm
.byte '#'
.string "a'b" # хвост
```
В первом случае символ `#` находится внутри литерала и не должен начинать комментарий. Во втором апостроф внутри двойных кавычек не должен переводить парсер в состояние одиночной строки.
Проблема возникла потому, что два флага не образуют полноценный автомат. Апостроф в `a'b` включал режим одиночных кавычек, хотя ассемблер всё ещё находился внутри двойных. После этого обработчик уже не понимал, какой ограничитель сейчас главный.
Так проявились типичные ошибки ассемблера при работе со строками: отдельные проверки могут давать верный результат на простых примерах, но конфликтовать на комбинациях синтаксических конструкций. Последовательность независимых заплаток не заменяет единого понимания состояния.
Правило, о котором я забыл
При повторной проверке выяснилось, что правил обработки текста не два, а три. Причём первое применяется ещё до построчного анализа: многострочные комментарии `/* ... */` удаляются сразу из всего файла.
Этот этап вообще не знает, где находятся кавычки. Поэтому фрагмент внутри строкового литерала тоже может быть уничтожен:
```asm
.string "ab/*cd*/ef"
```
После предварительной обработки из строки исчезнут четыре символа. Ассемблер не сообщит об ошибке, потому что формально комментарий был найден и удалён.
Обратная ситуация тоже возможна. Если внутри строки встречается незакрытый `/*`, он может сохраниться: удаление срабатывает только при наличии закрывающего `*/`. Получается непоследовательное поведение, вызванное не содержанием строки, а порядком применения правил.
В этом и заключается ключевая причина сбоя. Нельзя надёжно обрабатывать исходник цепочкой преобразований, если каждое преобразование видит только уже изменённый текст и не знает истории предыдущих символов.
Один проход и явные состояния
Более устойчивый вариант - проходить файл слева направо одним лексическим анализатором. Он должен помнить текущее состояние:
- обычный текст;
- двойные кавычки;
- одиночные кавычки;
- многострочный комментарий;
- однострочный комментарий;
- экранированный символ.
В обычном режиме `//`, `#` или `/*` могут запускать комментарий. Внутри строкового литерала те же последовательности становятся обычными символами. Если встречается обратная косая черта, следующий символ нужно пропустить как экранированный, иначе `"` ошибочно завершит строку.
Такой подход одновременно решает несколько задач: корректно сохраняет `http://`, не принимает двоеточие внутри текста за метку, не делит строку по запятой и не удаляет содержимое литерала вместе с комментариями.
Именно так должен выглядеть ответ на вопрос "как объявить строку в ассемблере": недостаточно знать директиву `.string`; необходимо, чтобы весь лексический слой уважал границы строкового значения.
Что изменилось в архитектуре
После перехода к единому разбору обработка исходника стала состоять из нескольких понятных этапов. Сначала лексический анализатор удаляет комментарии только там, где это разрешено текущим состоянием. Затем он выделяет метки и директивы, не путая символы внутри кавычек с синтаксическими разделителями. И лишь после этого разбираются операнды и выражения.
Это важнее, чем кажется. Если сначала разрушить строку, восстановить её позже уже невозможно. Поэтому ассемблер компилятор и директивы данных должны взаимодействовать через структурированный поток токенов, а не через серию строковых замен.
Практическое правило получилось простым: каждый символ должен интерпретироваться с учётом контекста, а не только собственного значения. Двоеточие бывает разделителем метки или частью текста, `#` - началом комментария или символом строки, а `//` - комментарием либо частью URL.
После исправления директива `.string "http://example.com"` снова стала обычной строкой данных. Она перестала исчезать ещё на этапе предварительного анализа, а генератор получил возможность корректно сформировать байты текста и завершающий нулевой символ.
Такой случай хорошо показывает, почему ассемблер синтаксис строковых литералов нельзя реализовывать набором независимых условий. Даже маленький язык требует настоящего лексического анализатора, пусть в нём пока всего несколько состояний. Именно он защищает программу от тихих ошибок, которые особенно опасны: сборка завершается успешно, но результат оказывается неверным.


