Реализация компиляторов с языка ассемблера

 

Язык ассемблера, как правило, содержит мнемонические коды машинных ко­манд. Чаще всего используется англоязычная мнемоника команд, но существуют и другие варианты языков ассемблера (в том числе существуют и русскоязыч­ные варианты). Именно поэтому язык ассемблера раньше носил названия “язык мнемокодов” (сейчас это название уже практически не употребляется). Все воз­можные команды в каждом языке ассемблера можно разбить на две группы: в первую группу входят обычные команды языка, которые в процессе трансля­ции преобразуются в машинные команды; вторую группу составляют специаль­ные команды языка, которые в машинные команды не преобразуются, но используются компилятором для выполнения задач компиляции (таких, например, как задача распределения памяти).

Синтаксис языка чрезвычайно прост. Команды исходной программы записыва­ются обычно таким образом, чтобы на одной строке программы располагалась одна команда. Каждая команда языка ассемблера, как правило, может быть раз­делена на три составляющих, следующих последовательно одна за другой: поле метки, код операции и поле операндов. Компилятор с языка ассемблера обычно предусматривает и возможность наличия во входной программе комментариев, которые отделяются от команд заданным разделителем.

Поле метки содержит идентификатор, представляющий собой метку, либо явля­ется пустым. Каждый идентификатор метки может встречаться в программе на языке ассемблера только один раз. Метка считается описанной там, где она не­посредственно встретилась в программе (предварительное описание меток не требуется). Метка может быть использована для передачи управления на поме­ченную ею команду. Нередко метка отделяется от остальной части команды спе­циальным разделителем (чаще всего – двоеточием “:”).

Код операции всегда представляет собой строго определенную мнемонику одной из возможных команд процессора или также строго определенную команду са­мого компилятора. Код операции записывается алфавитными символами вход­ного языка. Чаще всего его длина составляет 3—4 символа, реже – 5 или 6 символов.

Поле операндов либо является пустым, либо представляет собой список из одно­го, двух, реже – трех операндов. Количество операндов строго определено и за­висит от кода операции – каждая операция языка ассемблера предусматривает жестко заданное число своих операндов. Соответственно каждому из этих вари­антов соответствуют безадресные, одноадресные, двухадресные или трехадресные команды (большее число операндов практически не используется, в современ­ных ЭВМ даже трехадресные команды встречаются редко). В качестве операн­дов могут выступать идентификаторы или константы.

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

Семантика языка ассемблера целиком и полностью определяется целевой вычислительной системой, на которую ориентирован данный язык. Семантика языка ассемблера определяет, какая машинная команда соответствует каждой команде языка ассемблера, а также то, какие операнды и в каком количестве допустимы для того или иного кода операции.

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

Схемы синтаксического и семантического анализа в компиляторе с языка ассемб­лера могут быть, таким образом, реализованы на основе обычного конечного ав­томата. Именно эта особенность определила тот факт, что компиляторы с языка ассемблера исторически явились первыми компиляторами, созданными для ЭВМ. Существует также ряд других особенностей, которые присущи именно языкам ассемблера и упрощают построение компиляторов для них.

Во-первых, в компиляторах с языка ассемблера не выполняется дополнительная идентификация переменных – все переменные языка сохраняют имена, присво­енные им пользователем. За уникальность имен в исходной программе отвечает ее разработчик, семантика языка никаких дополнительных требований на этот процесс не налагает. Во-вторых, в компиляторах с языка ассемблера предель­но упрощено распределение памяти. Компилятор с языка ассемблера работает только со статической памятью. Если используется динамическая память, то для работы с нею нужно использовать соответствующую библиотеку или функции ОС, а за ее распределение отвечает разработчик исходной программы. За переда­чу параметров и организацию дисплея памяти процедур и функций также отве­чает разработчик исходной программы. Он же должен позаботиться и об отделе­нии данных от кода программы – компилятор с языка ассемблера, в отличие от компиляторов с языков высокого уровня, автоматически такого разделения не выполняет. И в-третьих, на этапе генерации кода в компиляторе с языка ассемб­лера не проводится оптимизация, поскольку разработчик исходной програм­мы сам отвечает за организацию вычислений, последовательность машинных ко­манд и распределение регистров процессора. За исключением этих особенностей компилятор с языка ассемблера является обычным компилятором, но значительно упрощенным по сравнению с любым компилятором с языка высокого уровня.

Компиляторы с языка ассемблера реализуются чаще всего по двухпроходной схеме. На первом проходе компилятор выполняет разбор исходной программы, ее преобразование в машинные коды и одновременно заполняет таблицу иденти­фикаторов. Но на первом проходе в машинных командах остаются незаполнен­ными адреса тех операндов, которые размещаются в оперативной памяти. На втором проходе компилятор заполняет эти адреса и одновременно обнаруживает неописанные идентификаторы. Это связано с тем, что операнд может быть опи­сан в программе после того, как он первый раз был использован. Тогда его адрес еще не известен на момент построения машинной команды, а поэтому требует­ся второй проход. Типичным примером такого операнда является метка, преду­сматривающая переход вперед по ходу последовательности команд.