Данному образовательному сайту пришлось несколько раз менять свое имя. С 2022 года доступ к нему обеспечивается по URL
emc.km.ru (2001-2007) ==> educomp.org.ru (2007-2011) ==> educomp.runnet.ru (2011-2021) ==> emc.orgfree.com (2022-...)
Более подробно об истории сайта можно прочитать здесь.
|
Компилятор?.. Это очень простоВ настоящее время большое распространение получил так называемый пользовательский подход, когда сидящий за компьютером человек не вникает (а часто и не хочет вникать!) в то, что происходит внутри компьютера. Работа при этом, если несколько утрировать, протекает по принципу "нажмешь сюда - получишь вот это". Любое отклонение в поведении компьютера от ожидаемого приводит такого пользователя в состояние если не паники, то уж в легкое замешательство точно. Можно до хрипоты спорить, сколько должен знать о работе компьютера использующий его человек, но очевидно, что чем лучше он понимает своего электронного помощника, тем успешнее будет его компьютерная деятельность. Особую актуальность приобретает знание "внутренней логики" компьютера для программиста, т.к. он пытается получить от машины значительно больше различных и сложных реакций, чем человек, работающий в "Лексиконе". И вот здесь, по-моему, нас подстерегает некоторая незаметная на первый взгляд опасность. Дело в том, что современный начинающий программист, как правило, не хочет обременять себя знакомством с языком низкого уровня, а сразу берется за Паскаль или Бейсик. В результате на вопрос "Во что преобразуется текст твоей программы и как она работает?", такой человек в лучшем случае может ответить кратко: "Текст генерирует исполнимый код". На этом его познания и заканчиваются. Мне кажется, что когда наши школьники и студенты бодро рассуждают об особенностях последних версий "Борландовского" Турбо-Паскаля и даже могут писать на нем какие-то программы, это еще не все. Надо, чтобы они хотя бы в самых общих чертах представляли, что такое переменная с точки зрения компьютера, как хранятся те или иные данные в памяти, как реализуются различные алгоритмические структуры и т.п. И вот тут мы, преподаватели, неожиданно осознаем, что в действительности и сами до сих пор как-то не очень задумывались об этом. Так что же все-таки происходит с нашей программой? Ответ на этот вопрос в популярной литературе найти не удается, а "вгрызаться" в объемные труды системных программистов для понимания основ вряд ли целесообразно. Автору этого материала захотелось создать более простой путь и написать демонстрационную программу, поработав с которой обучаемый получил бы какое-то представление о том, что такое компиляция. Отметим, что в опубликованной недавно статье [1] при выборе алгоритмического языка для обучения высказываются аналогичные мысли по поводу языков типа Бейсика или Паскаля: "Самым катастрофическим изъяном таких систем с точки зрения использования их для изучения программирования является огромное ядро интерпретатора или компилятора, закрытое не только от вмешательства программиста, но и вообще от его взгляда". Теперь, кажется, настало время извиниться за длинное предисловие и сказать следующее. Если все приведенные аргументы читателю показались неубедительными и желания рассматривать процесс компиляции не возникло, то сейчас самое время завершить знакомство со статьей. В противном случае перейдем к построению нашего демонстрационного учебного компилятора. Сразу оговоримся, что мы будем МОДЕЛИРОВАТЬ работу компилятора, т.е. нас будет сейчас интересовать не внутренняя структура самого компилятора, а результаты его деятельности. Иными словами, мы не будем выяснять, КАК именно компилятор выделяет отдельные символы текста программы и их анализирует, а сосредоточим свое внимание на том, ЧТО при продвижении по тексту программы формируется в ОЗУ в качестве эквивалентного исполнимого машинного кода. Прежде всего определимся с алгоритмическим языком, на котором будет написан текст исходной программы. Видимо, лучше всего для этой цели подойдет Паскаль - наиболее простой и наглядный язык, с которого программа может компилироваться. (Бейсик с его нумерованными строками хотя и может быть реализован в виде компилятора, все же содержит в себе целый ряд "некомпиляторных" черт). Несколько сложнее осуществить выбор машины, для которой будет разрабатываться демо-компилятор. Взять какой-нибудь реальный процессор? Не очень удобно, т.к. слишком много потребуется предварительно рассказывать ученику о принципах его работы. Поэтому мне показался более подходящим другой путь - выбрать условную вычислительную машину с минимальной системой команд. Наиболее простой и распространенной учебной моделью является ЭВМ "Кроха", разработанная и подробно описанная в школьном учебнике [2]. В ее системе команд всего 8 операций:
Объем памяти в авторском варианте ЭВМ "Кроха" [2] был всего 8 ячеек, что слишком мало даже для реализации самого примитивного демонстрационного компилятора. Поэтому ОЗУ придется увеличить хотя бы до 16 ячеек, что в воображаемом компьютере сделать заметно проще, чем в реальном. Дальнейшее увеличение количества ячеек приведет к некоторой потере наглядности - для 32 ячеек уже невозможно отобразить их все на экране одновременно. Следовательно для учебных целей придется ограничиться именно шестнадцатью ячейками ОЗУ. Указанная мера приводит к увеличению размера адреса ячеек памяти: если раньше он умещался в трех битах, то теперь потребуется уже четыре. Учитывая, что ЭВМ "Кроха" является трехадресной, общая длина команды возрастет на 3 бита и станет равной 15 битам. Таким образом, в качестве ЭВМ для демо-компилятора была выбрана условная машина (назовем ее для определенности "Кроха-М") с полностью идентичной [2] системой команд, но с расширенным объемом ОЗУ. Практически реализован учебный компилятор на компьютере УКНЦ, что объясняется "местными условиями" Пермского педагогического университета, где работает автор. Впрочем, для тех, кто не умеет пользоваться никакими другими машинами кроме IBM, имеется полный аналог программного обеспечения и для этой машины. Прежде чем перейти к обсуждению конкретных деталей устройства компилятора, последнее замечание. Автор ясно осознает, что далеко не всем читателям понравится обоснование выбора модели учебного компилятора. Если рассказ о конкретной реализации покажется неубедительным, но идея все же понравится, каждый может сам написать аналогичную программу для своей любимой машины и взять за основу работы компилятора систему команд любимого процессора. Перейдем теперь непосредственно к компилятору. Как известно, программа на Паскале состоит из трех частей: заголовка, описаний и операторов, т.е. собственно программы. Наиболее просто разбирается заголовок. В нем распознается слово PROGRAM и пропускается имя программы, т.к. оно носит "декоративный" характер. При желании можно контролировать, чтобы в имени не было недопустимых символов. Наконец, последнюю часть заголовка, перечисляющую используемые программой файлы, в демо-компиляторе можно смело опустить. Как известно, при отсутствии работы с диском используются только стандартные файлы INPUT и OUTPUT, а их разрешается не указывать практически во всех версиях Паскаля. Таким образом, заголовок только контролируется на корректность синтаксиса, не порождая в памяти ЭВМ никаких конструкций. Далее следуют описания. Учитывая, что наша ЭВМ "Кроха" может работать только с целыми числами, в описании переменных после слова VAR оставим допустимым только тип INTEGER. По этой же причине не будем рассматривать и описания типов. Метки удалим за ненадобностью: незачем учить использовать GOTO в структурном языке! С огромным сожалением придется исключить описание процедур и функций, ибо в системе команд ЭВМ "Кроха" не предусмотрен переход с возвратом, без которого эти конструкции реализовать очень трудно. Наконец, описание констант можно было бы и сохранить, но для упрощения компилятора я пожертвовал и ими. Таким образом, "когда дым рассеялся", всевозможные описания для нашего учебного компилятора выродились в единственную строку: При разборе этой строки помимо контроля синтаксисатребуется произвести резервирование ячеек ОЗУ для упоминающихся в списке переменных. Желательно, чтобы на экране дисплея было видно, что по описанию лишь отводится место в памяти под переменную, а не задается ее значение. Отметим еще, что при обработке списка необходим дополнительный контроль на отсутствие повторного описания переменных и (на всякий случай) на наличие свободной памяти. Остановимся подробнее на вопросе, в какой части ОЗУ размещать переменные. Согласно [2], исполнение программы ЭВМ "Кроха" всегда начинает с нулевой ячейки. Следовательно, программу удобно располагать в младших адресах, заполняя память в сторону их возрастания. Поскольку длина программы заранее неизвестна, то наиболее разумным и простым решением будет выделять место под переменные в старших адресах в сторону уменьшения, т.е. "навстречу" программе (рис.1).
Суммируя все сказанное, отметим, что обработка описания сводится к выделению ячеек памяти под используемые переменные и запоминанию их адресов в специальной таблице. Эта таблица в дальнейшем потребуется компилятору при замене в операторах имен переменных на соответствующие им адреса памяти. Теперь перейдем, наконец, к трансляции исполнимой части программы на Паскале. Прежде всего, определим список операторов, подлежащих распознаванию в нашем компиляторе. Это, конечно, присвоение (стандартное обозначение ":="), условный оператор IF/THEN/ELSE и все циклы: WHILE/DO, REPEAT/UNTIL, FOR/TO (DOWNTO) /DO. Конструкцию CASE в ОЗУ объемом 16 ячеек реализовывать нецелесоообразно, т.к. по самым простым прикидкам в него вряд ли поместится более трех ветвей. Еще раз отметим отсутствие операторов вызова процедур и функций, что, по мнению автора, является главным недостатком проекта. Впрочем, без одной стандартной процедуры - WRITELN - все же не обойтись, поэтому ее в порядке исключения придется разрешить, увеличив список допустимых служебных слов еще на одно. Таким образом, при расшифровке очередного оператора необходимо прежде всего проверять ключевые слова IF, WHILE, REPEAT, FOR, WRITELN и при совпадении с одним из них перейти к расшифровке соответствующей конструкции. В противном случае оператор может быть только присвоением; к его более подробному рассмотрению мы сейчас и переходим. Оператор присвоения содержит в левой части имя переменной, в которую помещается результат вычислений, затем символы присвоения := и после этого выражение, определяющее значение переменной. Неоднозначность может возникать только в правой части, поскольку выражения могут быть самыми разнообразными. Учитывая учебный характер нашего компилятора, ограничимся наиболее простыми выражениями: будем предполагать, что они содержат не более одного знака арифметической операции. Конечно, такое ограничение сильно снизит универсальность компилятора, зато резко упростит результирующую исполняемую программу: каждый оператор всегда будет компилироваться в одну машинную команду и не возникнет необходимости в рабочих ячейках под промежуточные результаты. Подобное упрощение выражений придаст еще один недостаток проекту, но, думается, с ним можно смириться, заменяя, например, A := 2 * (B - C) на A := B - C; A := 2 * A; . Рассмотрим основные типы допустимых выражений в операторе присвоения. Если в правой части есть (единственное!) арифметическое действие, то код операции определяется этим действием (+, -, *, div - цельночисленное деление). Если же оператор имеет вид A := B, то он транслируется в команду переписи информации из одной ячейки в другую. Имена переменных обязательно проверяются по составленной при расшифровке конструкции VAR таблице. В случае, когда переменная в ней отсутствует, выдается сообщение об ошибке; при успешном поиске имя переменной заменяется ее адресом в ОЗУ. В выражение могут входить не только имена переменных, но и некоторые непосредственно написанные константы, например, A := 2 или B := C + 30. Распознавать их будем по первому символу: если он цифра, значит операндом в выражении служит число. В этом случае придется указанную в программе константу перевести из текстовой формы в числовую (не забыв проверить, чтобы она не превышала максимально допустимого значения!). При отсутствии синтаксических ошибок полученное число поместим в ячейку ОЗУ "ниже" области, где зарезервировано место под переменные (см. рис.1). Особенно важно для экономии памяти предварительно проверить, не встречалась ли такая константа ранее. Например, при трансляции операторов A := A + 1; B := B + 1 константа 1 должна быть общей для обоих операторов. Приведем примеры трансляции выражений, в предположении, что описание имеет вид VAR A,B,C:INTEGER; а приведенные операторы следуют сразу за открывающим программу словом BEGIN:
Напомним, что переменные создаются в следующем порядке: A - ячейка 1111, B - 1110, C - 1101. В ячейку 1100 будет помещена константа 2. Кодовая часть команд берется из таблицы 1. Итак, если ограничить степень сложности арифметических выражений, как указано выше, оператор присвоения при раскрытии всегда будет порождать в результирующей программе одну машинную команду. Для реализации простейших линейных программ требуется еще иметь средства для выводов результатов на экран, поэтому нам необходима процедура WRITELN. Учитывая совмещение у ЭВМ "Кроха" команды вывода с остановом, придется в Паскаль-программе потребовать использование процедуры только с тремя аргументами (можно совпадающими) в сочетании с командой HALT. Например: ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM proba1; VAR a:INTEGER; адрес содержимое BEGIN a:=1; 0000 000 1110 0000 1111 WRITELN(a,a,a);HALT 0001 111 1111 1111 1111 END. ........................ 1110 1 1111 A Перейдем теперь к рассмотрению алгоритмических структур. Начнем с условного оператора IF/THEN/ELSE. В Паскале он записывается в виде: где <условие> - это <левая часть> <знак неравенства> <правая часть> На выражения в левой и правой части уместно наложить те же самые ограничения, что и на выражения в операторе присвоения. С учетом всего сказанного, полная условная конструкция в памяти ЭВМ будет иметь вид, представленный на рис.2 (на нем знаком * справа отмечены необязательные элементы).
Рис.2. Структура IF/THEN/ELSE Приведем в качестве иллюстрации конкретный пример разветвляющегося участка программы. Пусть переменная A должна принимать значение переменной B, если B/2 >= C, и значение 2B+1 в противном случае. ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM proba2; адрес содержимое VAR a,b,c:INTEGER; 0000 010 1110 1100 1011 BEGIN IF b div 2 >= c 0001 110 1101 1011 0100 THEN a:=b 0010 000 1110 0000 1111 0011 100 0000 0000 0110 ELSE BEGIN a:=2*b; 0100 101 1100 1110 1111 a:=a+1 0101 001 1111 1010 1111 END ........................ END. 1010 1 1011 рабочая ячейка (B div 2) 1100 2 1101 C 1110 B 1111 A Новой чертой оттранслированной программы является потребность в рабочей ячейке для хранения промежуточного результата B div 2. Т.к. правая часть условия вычислений не требует, в данной программе удается обойтись только одной рабочей ячейкой с адресом 1011. Особо отметим, что в системе команд ЭВМ "Кроха" всего два условных перехода и совсем нет безусловного. Последний несложно имитировать командой 100 0000 0000 ADR: если содержимое ячейки 0000 равно самому себе, что всегда справедливо, следующая исполняемая команда будет иметь адрес ADR. Сложнее обстоит дело с кодированием условий - для реализации некоторых приходится использовать комбинацию условного и безусловного переходов. Все 6 возможных вариантов отношений сведены в таблицу 2.
ОБОЗНАЧЕНИЯ таблицы 2. Перейдем теперь к вопросу о компиляции циклов. Конструкция WHILE/DO очень похожа на IF/THEN без ELSE (сравните рис.2 и 3). Единственным ее отличием является наличие безусловного перехода, возвращающего выполнение программы на начало цикла, адрес которого ADR0 при обнаружении слова WHILE необходимо запоминать. Учитывая, что рассмотренные алгоритмические структуры очень похожи, более подробное описание трансляции цикла WHILE приводить не будем.
Рис.3. Структура WHILE/DO Второй тип цикла - REPEAT/UNTIL имеет следующий вид:
Рис.4. Структура REPEAT/UNTIL Приведем пример компиляции фрагмента, содержащего REPEAT. ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM proba3; VAR i,s:INTEGER; адрес содержимое BEGIN REPEAT s:=s+i; 0000 001 1110 1111 1110 i:=i+1 0001 001 1111 1101 1111 until i=10 0010 100 1111 1100 0100 END. 0011 100 0000 0000 0000 ........................ 1100 10 1101 1 1110 S 1111 I Наконец, наиболее сложным для компиляции оказывается цикл FOR/TO(DOWNTO)/DO (рис.5). Напомним, что он имеет следующую
структуру:
Рис.5. Структура FOR/TO(DOWNTO)/DO При трансляции этой конструкции будем руководствоваться идеями автора Паскаля Н.Вирта, изложенными, например, на стр.185-186 в [3]. После слова FOR стоит оператор присвоения переменной цикла начального значения. Согласно [3] он выполняется в два этапа: сначала вычисляется значение правой части и результат помещается в некоторую рабочую ячейку, а затем, при корректности соотношения между начальным и конечным значениями, он переписывается в переменную цикла. Такой алгоритм обеспечивает "неприкосновенность" переменной в случае, когда цикл не выполняется ни разу. Платой за такую осторожную стратегию является заметное увеличение длины результирующего кода, хотя при нормальном обращении к циклу описанные меры оказываются излишними. Поэтому после некоторых размышлений ради экономии памяти я пожертвовал этой тонкостью трансляции и разрешил демо-компилятору раскрывать выражение после FOR обычным образом: результат вычисления правой части сразу заносится в переменную цикла, так что при ошибочно заданных параметрах ее значение меняется. Во всех остальных случаях цикл работает в точности как описано в [3]. Забегая вперед, скажу, что это единственное несоответствие классической идеологии конструкции FOR. Далее следует служебное слово TO или DOWNTO, определяющее шаг цикла (1 или -1). Учитывая, что у ЭВМ "Кроха" отсутствуют отрицательные числа, при компиляции шаг в обоих случаях принимается равным 1, но при TO он прибавляется к переменной цикла, а при DOWNTO - вычитается. Запомнив код необходимой операции, продолжаем разбор дальше. Компилируем выражение, стоящее после TO (DOWNTO), а под результат вычислений заводим специальную рабочую ячейку, с которой в дальнейшем будем сравнивать переменную цикла. Отметим очень распространенное исключение: если правой частью служит константа, занимать рабочую ячейку не стоит - можно ссылаться непосредственно на ячейку с константой. Последней командой подготовки к выполнению цикла поставим проверку корректности начального и конечного значений: для TO начальное значение переменной не должно превышать конечного, а для DOWNTO, наоборот, быть не ниже его. При нарушении этого условия цикл обходится, что важно для предотвращения зацикливания. Переходим к компиляции основной части цикла, соответствующей нормальным значениям параметров в заголовке. Запоминаем адрес ОЗУ ADR0, соответствующий началу тела цикла и обычным образом транслируем операторы, предназначенные для циклического повторения. Затем помещаем проверку на равенство между текущим значением переменной цикла и требуемым конечным значением: при их совпадении цикл завершается (подчеркнем, что это условие одинаково для TO и DOWNTO). Для противоположной ветви остается сформировать команду приращения переменной цикла в соответствии с требуемым шагом и поместить ее в ОЗУ вместе с командой перехода на начало цикла, т.е. к адресу ADR0. Пример трансляции фрагмента с циклом FOR выглядит так: ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM proba4; VAR i,s:INTEGER; адрес содержимое BEGIN FOR i:=1 0000 000 1101 0000 1111 TO 100 0001 110 1111 1100 0110 DO s:=s+i 0010 001 1110 1111 1110 0011 100 1111 1100 0110 0100 001 1111 1101 1111 0101 100 0000 0000 0010 END. ........................ 1100 100 1101 1 1110 S 1111 I Завершая обсуждение принципов компиляции Паскаль-программы, отметим необходимость постоянного контроля наличия памяти под результирующую программу. Для этого необходимо перед каждой записью машинной команды сравнивать адреса границ программы и констант (AP и AC на рис.1): переполнение памяти наступает, когда AP > AC. Наконец, приведем примеры трансляции полных программ, в которых задаются значения переменных и результат выводится на экран. В отличие от описанных ранее фрагментов, это уже вполне реальные задачи. Пример 1. Заданы числа A и B. Большее из них разделить на меньшее и найти остаток. ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM primer1; VAR a,b,d,o:INTEGER; адрес содержимое BEGIN a:=5; 0000 000 1011 0000 1111 b:=14; 0001 000 1010 0000 1110 if b>=a 0010 110 1111 1110 0110 then begin d:=a; 0011 000 1111 0000 1101 {меняем A и B} a:=b; 0100 000 1110 0000 1111 b:=d 0101 000 1101 0000 1110 end; d:=a div b; 0110 010 1111 1110 1101 o:=b*d; 0111 101 1110 1101 1100 o:=a-o; {остаток} 1000 011 1111 1100 1100 WRITELN(a,d,o);halt 1001 111 1111 1101 1100 END. 1010 14 1011 5 1100 O 1101 D 1110 B 1111 A Пример 2. N Вычислить Y = 3 . ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM primer2; VAR n,y,w:INTEGER; адрес содержимое BEGIN n:=4; 0000 000 1100 0000 1111 w:=1; 0001 000 1011 0000 1101 y:=1; 0010 000 1011 0000 1110 while w<=n do 0011 110 1101 1111 0111 begin y:=3*y; 0100 101 1010 1110 1110 w:=w+1 0101 001 1101 1011 1101 end; 0110 100 0000 0000 0011 writeln(n,y,y);halt 0111 111 1111 1110 1110 END. ........................ 1010 3 1011 1 1100 4 1101 W 1110 Y 1111 N Пример 3. Вычислить s = 1+x(2+x(3+x(4+x*5))) (схема Горнера). ПРОГРАММА РЕЗУЛЬТАТ КОМПИЛЯЦИИ PROGRAM primer3; VAR x,s,k:INTEGER; адрес содержимое BEGIN x:=2; 0000 000 1100 0000 1111 s:=5; 0001 000 1011 0000 1110 for k:=s-1 0010 011 1110 1010 1101 downto 1 do 0011 110 1010 1101 1001 begin s:=s*x; 0100 101 1110 1111 1110 s:=s+k 0101 001 1110 1101 1110 end; 0110 100 1101 1010 1001 0111 011 1101 1010 1101 1000 100 0000 0000 0100 writeln(k,s,s);halt 1001 111 1101 1110 1110 END. 1010 1 1011 5 1100 2 1101 K 1110 S 1111 X Наступило время подвести итоги. Основные принципы преобразования текста Паскаль-программы в машинный код выработаны, практическая реализация демонстрационного учебного компилятора, как уже отмечалось ранее, существует. Написан демо-компилятор на языке Паскаль и откомпилирован на школьный компьютер УКНЦ, а также на IBM. Разумеется, в последнем случае черно-белый вариант программы, созданный в расчете на компьютер с объемом ОЗУ 64 К, смотрится довольно скромно, но функции свои исправно выполняет и для обучения вполне пригоден. В настоящее время ведется работа по адаптации программы демо-компилятора на "Ямаху". Надеюсь, что подробное изложение материалов статьи будет представлять интерес для широкого круга работников образования.
© Е.А.Еремин, 1995 Статья: Еремин Е.А. Компилятор? Это очень просто. Компьютер УКНЦ. М.: Компьютика, 1995, N 3, c.25-33. |