Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
compilers.docx
Скачиваний:
9
Добавлен:
09.11.2018
Размер:
108.47 Кб
Скачать

1.4. Родственники компилятора

Как видно из рис. 1.3, входная информация для компилятора может порождаться од­ним или несколькими препроцессорами; кроме того, после компиляции может потребо­ваться дополнительная обработка для получения выполняемого машинного кода. В этом разделе мы рассмотрим типичное окружение, в котором работает компилятор.

Препроцессоры

Препроцессоры создают входной поток информации для компилятора. С их помощью можно выполнить следующие функции.

  1. Обработка макросов. Пользователь может определить макросы — краткие записи длинных конструкций.

2. Включение файлов. В текст программы можно включить заголовочные файлы. Например, при обработке файла препроцессор С заменяет выражение #include <global. h> содержимым файла global. h.

3. "Интеллектуальные" препроцессоры. К старым языкам добавляются более совре­менные возможности управления выполнением программы и работы со сложными структурами данных. Например, с помощью таких препроцессоров можно использо­вать встроенные макросы для построения циклов while или условных конструкций, отсутствующих в языке программирования.

4. Языковые расширения. Примером может послужить язык Equel ([413])— язык за­просов к базе данных, внедренный в код С. Препроцессор получает инструкции, на­чинающиеся с # # (это инструкции доступа к базе данных, не имеющие никакого от­ношения к С), и переводит их в вызовы процедур, реализующих обращения к базе данных.

Обработчики макросов работают с двумя видами инструкций — определение макро­сов и их использование. Определения обычно указываются с помощью определенного символа или ключевого слова типа define или macro и состоят из имени определяемо­го макроса и его тела, формируя определение макроса. Зачастую макропроцессоры по­зволяют применять в определениях макросов формальные параметры, т.е. символы, за­меняемые значениями при использовании макроса (в данном контексте "значение" — строка символов). Использование макроса представляет собой его имя с фактическими параметрами, т.е. значениями для подстановки вместо формальных параметров. Макро­процессор подставляет в тело макроса фактические значения вместо формальных пара­метров; затем преобразованное тело макроса замещает его имя в программе.

Пример 1.2

ТЕХ, о котором упоминалось в разделе 1.2, позволяет работать с макросами. Опреде­ление макроса имеет вид

\define <имя макроса> <шаблон> {<тело>}

Имя макроса представляет собой строку символов, начинающуюся с обратной косой черты. Шаблон — строка символов, в которой строки типа #1, #2 ... #9 рассматривают­ся как формальные параметры. Эти символы могут появляться в теле макроса сколько угодно раз. Например, следующий макрос определяет ссылку на Journal of the ACM.

\define \JACM #1;#2;#3.

{{\s1 J. ACM} {\bf #1}:#2, pp. #3.}

Имя макроса— \JACM, а шаблон—"#1; #2; #3."; точки с запятой разделяют от­дельные параметры, а за последним параметром следует точка. Использование такого макроса должно иметь тот же вид, что и шаблон, с тем отличием, что вместо формаль­ных параметров могут использоваться произвольные строки.4 Таким образом, мы можем записать

\JACM 17;4;715-728.

и получить при этом

J. АСМ 17:4, pp. 715-728.

Часть тела макроса { \ s1 J. АСМ} обеспечивает вывод текста J. АСМ наклонным (slanted5) шрифтом. Выражение {\bf #1} говорит о том, что первый фактический па­раметр должен быть выведен полужирным шрифтом (boldface). Этот параметр представ­ляет собой номер тома журнала.

ТЕХ. позволяет использовать любые знаки пунктуации или строки текста для разделе­ния тома, выпуска и номеров страниц в определении макроса \JACM. Можно обойтись и без разделителей — в этом случае ТЕХ будет считать фактическими параметрами отдель­ные символы или строки, взятые в фигурные скобки {} .

Ассемблеры

Некоторые компиляторы создают ассемблерный код, как в (1.5), который передается для дальнейшей обработки ассемблеру. Другие компиляторы самостоятельно выполняют работу ассемблера, производя перемещаемый машинный код, который непосредственно передается загрузчику/редактору связей. Читатель наверняка знает, как выглядит ассемб­лерный код и что такое ассемблер. Здесь же мы рассмотрим отношения между ассемб­лерным и машинным кодами.

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

MOV a, R1

ADD #2, R1 (1.6)

MOV R1, b

Этот код перемещает содержимое памяти по адресу а в регистр 1, затем добавляет к нему константу 2, рассматривая содержимое регистра 1 как число с фиксированной точкой, и сохраняет результат в именованной ячейке памяти b. Таким образом вычис­ляется b:= а+2.

Имеет ли язык ассемблера возможности работы с макросами, зависит от типа ас­семблера.

Двухпроходный ассемблер

Простейший ассемблер делает два прохода по входному потоку (в данном случае проход — разовое считывание входного файла). При первом проходе находятся все идентификаторы, обозначающие ячейки памяти, и размещаются в таблице символов (отличной от таблицы символов компилятора). Идентификаторам назначаются адре­са в памяти, так что после чтения (1.6) таблица символов может содержать записи, показанные на рис. 1.12. Мы предположили, что для каждого идентификатора выде­ляется одно слово памяти, состоящее из четырех байт, и адреса начинаются с нуле­вого адреса.

Идентификатор Адрес

а 0

b 4

Рис. 1.12. Таблица символов ассемблера с идентификаторами из (1.6)

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

В результате второго прохода обычно получается перемещаемый (relocatable) ма­шинный код, что означает, что он может быть загружен в память с любого стартового адреса L. Если L будет добавлено ко всем адресам в коде, то все ссылки будут совершен­но корректны. Таким образом, выходной код ассемблера должен различать части инст­рукций, ссылающиеся на адреса, которые могут быть перенесены.

Пример 1.3

Далее следует гипотетический машинный код, в который переводятся инструкции из (1.6).

0001 01 00 00000000 *

0011 01 10 00000010 (1.7)

0010 01 00 00000100 *

Инструкция представлена в виде слова, в котором первые четыре бита являются ко­дом инструкции (0001, 0010 и 0011 соответствуют загрузке, сохранению и сложению). Под загрузкой и сохранением подразумевается перемещение из памяти в регистр и наоборот. Следующие два бита определяют используемый регистр; 01 означает, что во всех трех командах используется первый регистр. Два последующих бита определяют "дескриптор". 00 означает режим обычной адресации, при котором последующие восемь бит представляют собой адрес памяти; дескриптор 10 указывает на "непосредственный" режим, когда последующие восемь бит являются операндом. Этот режим используется во второй команде (1.7).

В (1.7) есть также символ "*"— бит перемещаемости— который имеется у каждо­го операнда в перемещаемом машинном коде. Предположим, что адресное пространство содержит данные, загруженные начиная с адреса L. В этом случае символ * означает, что L должно быть добавлено к адресу операнда. Таким образом, если L=00001111, т.е. 15, то а и b размещаются по адресам 15 и 19. Теперь (1.7) в абсолютном (или непереме-щаемом) машинном коде будет выглядеть как

0001 01 00 00001111

0011 01 10 00000010 (1.8)

0010 01 00 00010011

Заметьте, у второй команды в (1.7) нет связанного бита перемещаемости, поэтому во второй команде прибавления L не происходит (так как, по сути, восьмибитовое значение представляет собой не адрес 2, а константу 2).

Загрузчики и редакторы связей

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

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

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

Например, код (1.7) должен предваряться таблицей

a 0

b 4

Если загружается файл с кодом (1.7), ссылающимся на Ь, то эта ссылка будет замене­на на 4 плюс смещение, на которое перемещены данные из файла (1.7).

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]