- •Университет наяновой
- •М. А. Шамашов основные структуры данных и алгоритмы компиляции
- •Предисловие
- •Введение
- •1. Краткий обзор процесса компиляции
- •2. Лексический анализ
- •0123...9 Пробел
- •3. Организация таблиц компилятора
- •3.1. Общий вид таблиц
- •3.2. Прямой доступ к таблице или метод индексов
- •3.3. Неупорядоченная таблица или метод линейного списка
- •3.4. Упорядоченная таблица. Бинарный, двоичный или логарифмический поиск
- •3.5. Сбалансированные деревья
- •3.6. Деревья оптимального поиска
- •3.7.1. Рехеширование
- •3.7.3. Метод цепочек или гроздей
- •4. Общие методы синтаксического анализа
- •4.1. Нисходящий разбор с возвратами
- •4.2. Восходящий разбор с возвратами
- •4.3. Символьный препроцессор на основе бэктрекинга
- •4.3.1. Фаза анализа и перевода грамматики во внутреннее представление
- •4.3.2. Лексичекий анализ в сп
- •4.3.3. Синтаксический анализ в сп
- •4.3.4. Выполнение семантических действий
- •5. Однопроходный синтаксический анализ без возвратов
- •5.1. Ll(k) языки и грамматики
- •5.1.1. Предсказывающие алгоритмы разбора и разбор для ll(1)-грамматик
- •5.1.2. Рекурсивный спуск
- •5.2. Языки и грамматики простого предшествования
- •Xy, если u xy
- •X y, если u xU1) (y l(u1))
- •X y, если (u u1y) (X r(u1)) or
- •5.2.1. Алгоритм Вирта–Вебера для анализа языков простого предшествования
- •5.2.2. Функции предшествования.
- •5.2.3. Проблемы построения грамматик предшествования
- •5.3. Операторная грамматика предшествования
- •6. Введение в семантику
- •6.1. Внутренние формы исходной программы
- •6.1.1. Польская инверсная запись
- •If выр then инстр 1 else инстр 2
- •6.1.2. Интерпретация полиЗа
- •6.1.3. Генерирование команд по полиЗу
- •6.1.4. Тетрады и триады
- •6.2. Семантические подпрограммы перевода инфиксной записи в полиз и аспекты их реализации
- •6.3. Семантические подпрограммы для перевода в тетрады
- •6.4. Метод замельсона–бауэра для перевода в полиз и тетрады
- •6.5. Нейтрализация ошибок
- •6.5.1. Исправления орфографических ошибок
- •6.5.2. Нейтрализация семантических ошибок
- •6.5.3. Нейтрализация синтаксических ошибок
- •7. Машинно-независимая оптимизация программ
- •7.1. Исключение общих подвыражений
- •7.2. Вычисления во время компиляции
- •7.3. Оптимизация булевых выражений
- •7.4. Вынесение инвариантных вычислений за цикл
- •8. Машинно-зависимые фазы компиляции
- •8.1. Распределение памяти
- •8.2. Генерация кода и сборка
- •8.3. Трансляция с языка ассемблера
- •Заключение
- •Список литературы
- •Содержание
- •1. Краткий обзор процесса компиляции 5
- •2. Лексический анализ 10
- •3. Организация таблиц компилятора 16
- •4. Общие методы синтаксического анализа 28
- •5. Однопроходный синтаксический анализ без возвратов 52
- •6. Введение в семантику 78
- •7. Машинно-независимая оптимизация программ 102
- •8. Машинно-зависимые фазы компиляции 109
8.2. Генерация кода и сборка
Назначение фазы генерации кода состоит в формировании кода на языке ассемблера или машинном языке. Эта фаза в качестве исходной информации использует промежуточную форму программы (ПОЛИЗ или матрицу тетрад), а также кодовые продукции (чаще всего макроопределения), которые определяют все операции, появляющиеся в промежуточной форме. Она, кроме того, обращается к таблице идентификаторов и констант для генерации соответствующих адресов и преобразований типов.
На рис. 8.1 представлены примеры кодовых продукций в виде ассемблерных фрагментов для ряда операций из матрицы тетрад.
В учебных компиляторах имеет смысл для каждой определяемой Вами операции подготовить макроопределения и представлять каждую тетраду макрокомандой, возложив формирование кода на стандартный транслятор с языка ассемблера. Пример макроопределения для операции сложения, где операнды могут быть словами или байтами, а результат всегда – слово представлен на рис. 8.2.
В реальных компиляторах формирование кода зачастую также осуществляется с помощью специализированных макропроцессоров. При этом директивы условной трансляции в макроопределениях операций могут обеспечить не только преобразование типов данных, но и машинно-зависимую оптимизацию, связанную с сохранением значений регистров во временной памяти и их загрузку, эффективном использовании всей совокупности регистров процессора и формировании более коротких и быстродействующих регистровых команд [6].
Фаза сборки зависит от того, что является результатом фазы генерации кода. В простейшем случае фаза сборки должна обработать все метки объектной программы, сформировать объектный модуль и информацию для загрузчика (таблицу переместимых и внешних имен). Функционально фаза сборки в этом случае похожа на второй просмотр ассемблера. В другом случае, если фаза генерации оставляет коды и метки в символическом виде, фаза сборки должна осуществлять, по сути дела, полную трансляцию с языка ассемблера и решать следующие задачи:
разрешить все символьные ссылки;
вычислить адреса;
сгенерировать двоичные машинные команды;
подготовить информацию для загрузчика.
8.3. Трансляция с языка ассемблера
Большинство конструкций языка ассемблера с точки зрения синтаксиса, исключая арифметические выражения, которые могут присутствовать в поле операндов, описываются с помощью автоматных грамматик и в основе синтаксического анализатора большинства ассемблеров лежит модель конечного автомата. Нейтрализация ошибок здесь тривиальна, так как каждая команда на языке ассемблера представляется отдельной строкой и, встретив ошибку в строке и проидентифицировав ее, транслятор просто переходит к анализу следующей строки.
На рис. 8.3 представлена упрощенная структура двухпроходного ассемблера. Напомним, что транслятор ассемблера обычно называется ассемблером, в отличие от языка ассемблера, который ассемблер транслирует в машинный код.
Цель первого прохода – получить всю информацию о местоположении идентификаторов (имен, меток), а второго, – непосредственно генерировать код. Для локализации имен в ассемблере предусмотрен счетчик адреса (ячеек). Идентификатор, обнаруженный в поле метки анализируемого оператора заносится в таблицу имен и ему ставится в соответствие текущее значение счетчика адреса. При просмотре программы происходит увеличение счетчика адреса на число байт, занимаемых операторами. При переходе от одного сегмента программы к другому счетчик адреса обнуляется. Таким образом, счетчик адреса – это указатель, динамически фиксирующий относительные позиции (смещения) операторов (команд или директив) внутри одного сегмента. Итак, на первом проходе строится таблица имен (см. таблицу 7.1), а на втором проходе эта таблица используется для формирования адресов операндов.
Таблица 8.1.
Имя |
Смещение |
Сегмент, в котором определено имя |
Тип |
Размер и прочее |
@Data |
00H |
|
сегмент |
|
Vari |
00H |
@Data |
переменная |
|
Array |
0AH |
@Data |
переменная |
|
|
|
|
|
|
_Text |
00H |
|
сегмент |
|
Start |
00H |
_Text |
метка |
|
Repeat |
09H |
_Text |
метка |
|
|
|
|
|
|
При работе ассемблер использует также две постоянные таблицы зарезервированных имен, содержащих всю необходимую информацию о командах и директивах. Там находятся мнемоники, коды операций и форматы, информация о длине, необходимая для увеличения счетчика адреса и т.п.
В отличие от рассмотренного, однопроходный ассемблер работает эффективнее. Он может легко генерировать команды, где имена операндов уже известны, определяются в программе до их использования. Иное дело, когда команда ссылается на имя пока неизвестное и, определяемое, например, как имя переменной или метка команды идущей за анализируемой строкой. В этом случае в таблице имен (идентификаторов) появляется дополнительная информация – признак определения метки. Если какое-либо неопределенное ранее имя встречается в поле операнда команды или директивы, то это имя помещается в таблицу с отметкой о том, что адрес (смещение) для этого имени еще не известно. Вместо смещения в таблицу помещается указатель головы списка, хранящего адреса команд ссылающихся на данную, пока неопределенную метку. После того, как данное имя появится в программе в поле метки и для него будет определено смещение, то ассемблеру достаточно “пробежаться по списку” ссылавшихся на данное имя команд и сформировать для них адреса операндов.