
                                Компилятор.
                                 Часть 2.

                         Создание компилятора языка
                         Милан на платформе 80x86.

    NB!  Перед чтением этого текста рекомендуется прочитать файл howdoit1.txt,
в котором содержится глава из учебного пособия 'Основы трансляции',  посвящен-
ная компиляторам.  Это необходимо,  так как дальнейшее изложение будет базиро-
ваться на содержании этого пособия.

    Итак, будем считать, что Вы прочли этот текст,  то есть имеете хотя бы по-
верхностное представление о синтаксических диаграммах и функциях компилятора.
    Теперь постараемся применить теорию на практике. Это значит,  что мы попы-
таемся создать действующий компилятор для компьютеров PC, который будет по ис-
ходному тексту программы на Милане выдавать исполнимый модуль,   функционирую-
щий в среде MS DOS.
    Как Вы могли заметить,  в методичке шла речь о некой абстрактной 'стековой
машине', которую характеризуют:

    - линейная память для хранения программ
    - линейная память для хранения данных
    - стековая память для хранения промежуточных результатов
    - определенный набор команд

    Для того, чтобы создать свой компилятор, нам необходимо только поставить в
соответствие командам абстрактной машины (далее АМ) команды процессора i8086 и
позаботится о распределении памяти под данные и стек. Это относительно нетруд-
но сделать. Главная проблема возникает при загрузке нашей программы  в  память
средой MS DOS.
    Как известно, в DOS есть два формата для исполнимых модулей: com и exe. Не
представляя себе организацию этих файлов, такую вещь, как компилятор,  создать
невозможно!  Для нас достаточно тех сведений,  которые даются дисциплиной 'Ма-
шинно-ориентированные языки'.
    Основное различие в том, что в com-модуле данные,  стек и код объединены в
одном сегменте, а в exe - находятся в разных.  Для наших целей больше подходит
именно com-формат, так как небольшие программы на Милане умещаются в 64K памя-
ти (ограничение для размера сегмента в архитектуре i8086)  вместе  с  данными,
кодом и стеком.
    Еще одно преимущество (для меня оно оказалось решающим) - формат  com-фай-
ла значительно проще. Более того,  мне так и не удалось осуществить  необходи-
мое размещение сегментов для exe-модуля.  Поэтому дальнейшее  изложение  будет
о том, как сделать компилятор для небольших com-файлов.
    Ведущая идея проста: чтобы не вдаваться в тонкости того,   как  происходит
взаимодействие программы с DOS, каков должен быть заголовок файла и других по-
добных вещей,  предоставим всю 'черную' работу системным средствам,  таким как
ассемблер и линковщик, то есть будем следовать принципу:

               Человек должен думать, машина должна работать.

    Это значит,  что заголовок файла,  области памяти,  отводимые под данные и
стек, а также код вспомогательных функций создаст за нас ассемблер.  Таким об-
разом мы получим работающий экземпляр файла, который является своеобразной за-
готовкой,  болванкой для нашего компилятора.  В подкаталоге ML307\SOURCES\ со-
держится текст ассемблерной программы com.asm,  который я использовал в качес-
тве прототипа при создании исполнимых модулей.  В зависимости от задач,  стоя-
щих перед Вами,  придется,  конечно,  придумать нечто свое,  но главное понять
принципы, которыми необходимо руководствоваться при создании подобных вещей. В
файле содержатся все необходимые комментарии, здесь лишь представлю его струк-
туру:


 ┌───────────────────────────┬──────────────────────────────────────────────┐
 │       Название            │              Комментарий                     │
 │                           │                                              │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Заголовок com-файла       │ Необходим DOS для справки о формате файла,   │
 │                           │ настройки адресов и т.п. вещей. Этой части   │
 │                           │ нет в ассемблерном листинге, она создается   │
 │                           │ линковщиком и содержит системную информацию, │
 │                           │ указанную выше.                              │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Точка входа.              │ Сюда DOS передаст управление после загрузки  │
 │                           │ программы в память. Для com-файла необходимо │
 │                           │ вручную настроить счетчик команд директивой  │
 │                           │                   org 100h                   │
 │                           │ У меня после настройки стека управление      │
 │                           │ передается в конец файла.                    │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Массив констант           │ Массив из 20 слов, в которые компилятор      │
 │                           │ после работы сканера запишет значения        │
 │                           │ констант.                                    │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Массив идентификаторов    │ Массив из 20 слов, в котором на этапе выпол- │
 │                           │ нения программы будут храниться текущие зна- │
 │                           │ чения идентификаторов.                       │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Стек                      │ Стек программы необходимый для корректной    │
 │                           │ работы программы в среде DOS и для хранения  │
 │                           │ промежуточных результатов.                   │
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Вспомогательные функции   │ Функции вывода на экран и ввода с клавиатуры.│
 ├───────────────────────────┼──────────────────────────────────────────────┤
 │ Коды выхода               │ Сюда будет передано управление после         │
 │                           │ настройки стека программы. Так как наш       │
 │                           │ файл - всего лишь заготовка, разместим здесь │
 │                           │ коды выхода.                                 │
 └───────────────────────────┴──────────────────────────────────────────────┘

    Таким образом,  мы создали вполне работоспособный файл (хотя он ничего  не
делает), который ассемблируется и линкуется командами:

    tasm.exe /l /w2 /z com.asm com.obj com.lst
    tlink.exe /t /x /l /v /Tdc com.obj наше_имя_модуля.com

    Я назвал его com.com. Этот файл будет использоваться компилятором,  поэто-
му любое его изменение или повреждение приведет к тому,   что  сгенерированные
программы не будут работать. В файле com.lst находится листинг, который содер-
жит очень важную для нас информацию. О том, как она используется, будет сказа-
но ниже.
    Раз я уже упомянул о tasm и tlink, сразу замечу,  что нам необходима также
программа-отладчик типа td, чтобы смотреть, что же получилось в результате ра-
боты компилятора.
    Посмотрите еще раз на структуру файла.  Именно такой порядок в  следовании
его частей был выбран не случайно: все неизменяемые части  оказались  вначале,
поэтому коды,  сгенерированные компилятором,  можно просто дописывать в конец,
не заботясь о согласовании адресов,  которые понадобились,  если бы код встав-
лялся куда-нибудь в середину.  Чтобы лучше понять целесообразность такого под-
хода,  позволю себе образное сравнение - что легче сделать: построить на гото-
вом фундаменте дом,  или добавить новый этаж между 6 и 7 этажами высотного до-
ма ?
    Однако достаточно воды,  перехожу к главному (надеюсь,  Вы не забыли,  что
наша задача - создать компилятор).
    В принципе, понять как работает компилятор,  можно пользуясь его синтакси-
ческими диаграммами и описанием его семантических функций,   представленных  в
файле  ML307\DOC\howdoit1.txt,    а    пример    реализации    содержится    в
ML307\SOURCES\compiler.cpp.  Однако там не содержится никаких (или почти ника-
ких) комментариев. Чтобы исправить этот недостаток,  и предназначен этот файл,
который представляет собой более доступное  описание  работы  компилятора  (по
крайней мере я на это очень надеюсь).

    Для создания компилятора нам необходимо знать несколько вещей:

    - во-первых, смещения констант и переменных относительно начала  сегмента
    - во-вторых, чтобы обращаться к функциям ввода/вывода, необходимо знать их
      адреса (так как функции находятся в том же сегменте, для обращения к ним
      достаточно знать только смещения)
    - в третьих,  нужно знать позицию,  с которой будем записывать свои коды в
      файл.  Задача упрощается тем,  что писать будем в конец,  начиная с того
      места, где записаны коды выхода

    Все эти данные можно получить,  пользуясь листингом (он лежит,  кстати,  в
ML307\SOURCES\com.lst) и собственно модулем com.com,  просматривая его в режи-
ме 'Коды' в редакторе Norton'а (там очень хорошо видно  расположение  массивов
констант и переменных, стека и кода. Допустим, в обычном режиме просмотра стек
предстает как поле сердечек).
    Для того, чтобы компилятор было легче модифицировать, я храню все эти дан-
ные в переменных:

    EntryPoint = 0x0B6D. Точка входа в код,  сгенерированный компилятором.  На
                         этот адрес произойдет переход, после того,  как будет
                         инициализирован стек.
    ConstantOffset=0x010A. Смещение блока констант относительно начала файла.
    IdentOffset=0x0132. Смещение блока переменных относительно начала файла.

    Из листинга, сгенерированного ассемблером, узнаем адреса функций :

    ProcOutNum = 0x0969. Вывод на экран числа.
    ProcInInt  = 0x0AB9. Ввод числа с клавиатуры.

    Кроме вышеперечисленных, компилятор использует следующие переменные:

    LexAmount - общее число лексем.
    LexN[500],LexZ[500] - таблицы лексем.
    CurLex - номер текущей обрабатываемой лексемы (начиная с нуля  -  в  стиле
             языка Си).
    ConstAmount - число констант.
    Const[20] - массив значений констант.
    IdentAmount - число идентификаторов переменных.
    Ident[20] - значения идентификаторов (этот массив не используется).
    CodeBuf[30000] - массив, в котором будет храниться двоичный код,  получен-
                     ный при работе компилятора.
    CodeBufPointer - указатель позиции в этом буфере.
    Counter - адрес текущей ячейки памяти.  Начнется со  значения  EntryPoint.
              Поясню: сначала Counter=EntryPoint, а CodeBufPointer=0. Когда мы
              получим  первый  байт  программы,    Counter=EntryPoint+1,     а
              CodeBufPointer=1, то есть они взаимосвязаны,  только Counter со-
              держит как бы физический адрес текущего  свободного  байта,    а
              CodeBufPointer - его позицию в буфере кода.

    Далее идут прототипы семантических функций z01 - z17 и функций,   отвечаю-
щих за генерацию кода отдельных нетерминалов грамматики:

    L_PROG - обработка последовательности операторов.
    S_PROG - обработка отдельного оператора.
    B_PROG - обработка условия.
    E_PROG - обработка арифметических выражений.
    T_PROG - обработка терма.
    P_PROG - обработка множителя.

    Ниже будет рассмотрен пример,  на основе которого станет ясно,  как  рабо-
тают эти функции.  А пока расскажу общую схему работы моего компилирующего мо-
дуля.
    Итак, на вход подается имя программы. Из файла Phase02.tmp читается значе-
ние количества лексем в программе на Милане,  массивы лексем,  количество кон-
стант, и их значения.
    После этого начинается работа собственно компилирующей части модуля. В ре-
зультате получаем массив кода.  Теперь мы открываем заготовленный файл-болван-
ку com.com и копируем его содержимое в файл

                       ИмяКомпилируемойПрограммы.com

    Затем указатель позиции устанавливается за 4 байта до конца  этого  файла,
так как в этих байтах содержатся команды выхода, которые нам здесь не нужны:

                         mov       ax,4Ch
                         int       21h

и происходит переписывание буфера кода, в конце которого и содержатся вышеупо-
мянутые команды.
    Таким образом получается исполнимый файл,  который абсолютно корректно ра-
ботает в среде MS DOS, при условии, что код, добавленный компилятором,  не со-
держит ошибочных команд.
    Да, чуть не забыл. После формирования файла, происходит инициализация мас-
сивов констант и переменных.  Под этим понимается запись в файл массивов Const
и Ident (последний в нашем случае содержит нули -  значение  'по  умолчанию'),
начиная с позиции 0x0A, которая была получена из 'гляделки' Нортона. До нее же
располагается заголовок com-файла.
    Теперь разберемся с семантическими функциями. Они описаны в методичке,  но
там имелась в виду абстрактная машина (АМ),  с выдуманным набором команд.  Нам
же надо переделать их под конкретный процессор. При этом зачастую то,  что де-
лалось в АМ одной командой, придется делать несколькими,  а команды INP и OUT,
отвечающие в АМ за ввод/вывод с консоли, вообще были заменены вызовом соответ-
ствующих ассемблерных подпрограмм.

    * z01 - сгенерировать команду останова.
    В буфер кода заносятся байты B4h,4Ch,CDh,21h. Они означают команды
                           mov      ah,4Ch
                           int      21h
то есть выход из нашей программы (это стандартная функция DOS).

    * z02 - сгенерировать команду сравнения.
    В зависимости от параметра s, означающего код операции сравнения (коды со-
держатся в таблице лексем),  будет сгенерирован соответствующий машинный  код.
Так как он практически одинаков для всех операций сравнения, рассмотрим только
сравнение  на  равенство.    В  этом  случае    в    буфер    попадут    коды:
5Bh,58h,3Bh,C3h,74h,06h,B8h,01h,00h,EBh,04h,90h. Мнемонически выглядит так:

                           pop     bx
                           pop     ax
                           cmp     ax,bx
                           je      Label1
                           mov     ax,FALSE
                           jmp     Label2
                   Label1: mov     ax,TRUE
                   Label2: push    ax

где FALSE=1,  а TRUE=0 - заранее определенные директивой equ значения (как ви-
дите, значения 'инверсные', как в методичке). Думаю, этот кусок вполне прозра-
чен для понимания.  Он эквивалентен команде CMP,s абстрактной машины и  делает
те же действия - со стека снимаются два числа,  они сравниваются,  и результат
этой операции заносится в стек. Для всех остальных операций сравнения команда
                           je Label1
просто заменятся необходимой (jne для операции 'не равно'  и  т.д.),    а  ос-
тальной код остается без изменений.

    * z03 - присвоение.
    В функцию передается параметр k - номер идентификатора,  а в  стеке  лежит
значение, которое ему нужно присвоить. Это реализуется следующими ассемблерны-
ми командами:
                           pop     ax
                           mov     bx,offset Ident
                           add     bx,k*2
                           mov     [bx],ax
    Сначала в ax со стека снимается значение,  потом в bx формируется смещение
необходимого идентификатора (при этом надо учесть,  что они занимают в  памяти
по два байта,  поэтому k умножается на двойку),  и используя косвенную адреса-
цию необходимое значение присваивается необходимому идентификатору.

    * z04 - вывод на экран
    В АМ вывод числа осуществлялся одной командой OUT. В системе команд i80x86
ничего подобного нет,  поэтому у нас вывод на экран осуществляется  с  помощью
подпрограммы ProcOutNum (название выдумал не я).  При вызове  этой  функции  в
стеке уже лежит число, т.е. команда OUT заменяется на
                           pop     ax
                           call    ProcOutNum
Команда pop ax имеет код 58h,  call - E8h,  затем должно  следовать  смещение,
т.е. расстояние от кода функции, до текущей выполняемой команды. Оно будет из-
меняться при каждом новом вызове,  поэтому встает вопрос,  как его определить?
Здесь уже придется немного поломать голову,  и поэкспериментировать.  Рассмот-
рим диаграмму, которая поможет нам понять, как же вычисляется смещение для ко-
манды call:

   Блок  Команды  Данные  Подпрограммы        Точка       Вызов
   PSP   инициа-  и стек  ... ProcOutNum ...  входа       ProcOutNum
   o─────o────────o───────o───*1──────────────o───────────*2────────── ...
         лизации                              Код, сге-
         стека и                              нерирован-
         переход                              ный компи-
         на точку                             лятором
         входа

    Эта диаграмма показывает,  как выглядит программа после загрузки в  память
системой DOS. Здесь встречается новый термин PSP  (Program  Segment  Prefix  -
префикс программного сегмента). Это блок данных размером в 256 байт, после ко-
торых начинается собственно код программы. Для нас его содержимое несуществен-
но,  и можно о нем и не вспоминать,  единственное,  чем он напоминает о себе -
нельзя забывать о директиве org 100h в начале каждой com-программы.
    Структура программы была рассмотрена выше,  поэтому не будем на ней  снова
останавливаться. Посмотрите на диаграмму.  В блоке кода подпрограмм  находится
первая звездочка - начало функции ProcOutNum - место,  куда нам надо  передать
управление из точки,  на которую указывает вторая звездочка - текущей команды.
Нам известны их адреса: для *1 это 0x0969,  а адрес *2 содержится в переменной
Counter.
    Таким образом, в первом приближении, смещение, т.е.  расстояние между *1 и
*2 можно найти по формуле:

               0x0969                             Counter
      ...  ──────*1────────────── ... ───────────────*2────── ...
                 │                                   │
                 <───────────────────────────────────>
                           ProcOutNumOffset

                 ProcOutNumOffset = -(Counter-0x0969)

    Минус появился оттого, что происходит вызов процедуры,  находящейся в теле
программы до инструкции call.
    Так как я увеличиваю значение счетчика после того,  как будет сгенерирован
код функции,  а не после каждого байта,  для согласования мне пришлось еще от-
нять 4 байта. Узнать об этом очень просто - просмотрев в TurboDebugger скомпи-
лированный программой код и установив расхождение в  числах  (в  нужном  месте
должно быть call 0x0969).

    * z05 - запомнить адрес, с которого начинается вычисление, в локальной пе-
            ременной n.

    Это действие можно было оформить и не в виде функции,  однако для строгос-
ти я все же сделал z05,  в которую передается ссылка на переменную,    которая
должна хранить адрес (переменная должна быть именно  локальной,    потому  что
необходимо обеспечит повторную входимость,  так как условия могут быть и  вло-
женными). Без комментариев:

                 n = Counter

    * z06 - команда обхода конструкции L при FALSE.

    В стеке находится результат предыдущей  операции:  значение  TRUE,    либо
FALSE.  В зависимости от него необходимо передать управление  в  разные  точки
программы. Если это TRUE, должна выполняться следующая команда (т.е. блок пос-
ле then).  Eсли же в стеке находится FALSE,  необходимо перепрыгнуть через код
для then: либо на выполнение кода для else (если условная конструкция полная),
либо выйти за нее (т.е. начать выполнение кода после od).
    Так как мы еще не просмотрели текст программы, мы не знаем,  куда же пере-
дать управление, если в стеке находится FALSE.  Поэтому в этом случае мы гене-
рируем 'прыжок в никуда',  и запоминаем адрес этой команды,  чтобы после  вер-
нуться к нему,  и обладая необходимой информацией,  вставить корректный  адрес
для перехода.
    Вот как это может выглядеть в ассемблерном мнемокоде:

                         pop       ax
                         cmp       ax,FALSE
                         jne       NotFalseLabel
                         jmp       0000h
          NotFalseLabel:

    Сначала в регистр ax снимаем значение со стека.  Сравниваем его  с  FALSE.
Если условие неверно (ax == TRUE),  переходим на следующую  команду  с  меткой
NotFalseLabel, которой реально еще не существует. Если же ax == FALSE,  делаем
прыжок в никуда.
    Коды таковы: 58h,3Dh,01h,00h,75h,03h,E9h,00h,00h.

    * z07 - генерация команды возврата на проверку условия повторения цикла.

    Это уже довольно сложная функция. Без диаграмм компилятора понять ее рабо-
ту довольно трудно. Рассмотрим ту часть,  которая поможет понять принцип  дей-
ствия z07.



                       JMF 0000h         JMP n
      while       B      do        L                od
    o────────>o───────>o───────>o───────>o──────────────────────────>o
       z05              z06                        z07            
              n        m                 занести в m: JMF quit       quit

    Перед нами часть диаграммы для нетерминала  S,    показывающая  последова-
тельность действий компилятора при генерации кода для цикла while.  Я дополнил
ее некоторыми вспомогательными 'комментариями',  которыми пользовался сам (они
обозначены стрелками).
    Итак, при встрече ключевого слова while начинает работать функция z05. Она
запоминает адрес следующей команды в локальной переменной n, что и отмечено на
диаграмме.
    Вслед за этим будет сгенерирован код для обработки условия цикла B. За ним
должно идти ключевое слово do, и начинает работать z06, рассмотренная чуть вы-
ше. Она генерирует команду JMF 0000h (что называется, 'прыжок в никуда') и за-
писывает адрес этой команды в локальную переменную m.  Теперь компилятор обра-
батывает конструкцию L, а после встречи ключевого слова окончания цикла od пе-
редает управление функции z07, которую мы и рассматриваем.
    На ее вход подаются два параметра: n и m, которые содержат адреса. Сама же
функция сначала создает команду безусловного перехода JMP n (т.е.  'генерирует
команду возврата на проверку условия повторения цикла'),  а затем заменяет ад-
рес в команде JMF 0000h на адрес выхода из цикла,  которому на диаграмме соот-
ветствует метка quit.
    Команда JMP имеет код E9h,  и так как прыжок осуществляется назад,  смеще-
ние должно быть отрицательным,  а при занесении адреса перехода в команду  JMF
используем положительное смещение, так как прыжок происходит вперед.
    Надеюсь, что пользуясь листингом compiler.cpp, текстом howdoit1.txt и этим
комментарием, Вы поймете как же все-таки работает эта заковыристая z07.

    * z08 - генерация команды выхода из конструкции выбора.

    Аналогична функции z07. Смотри howdoit1.txt и compiler.cpp.

    * z09 - описание дано в howdoit1.txt

    Без комментариев.

    * z10 - смотри howdoit1.txt

    No comment.

    * z11 - сгенерировать команду загрузки в стек значения k-ого идентификато-
            ра.

    Зная смещение массива идентификаторов, а также учитывая тот факт, что один
идентификатор занимает 2 байта памяти,  можно  записать  следующую  последова-
тельность команд, реализующую это действие:

                         mov       bx,offset Ident
                         add       bx,k*2
                         mov       ax,[bx]
                         push      ax

    Надеюсь,    вполне  понятно.    Коды    для    этой    последовательности:
BBh,32h,01h,83h,C3h,k*2,8Bh,07h,50h.

    * z12 - сгенерировать команду загрузки в стек значения k-ой константы.

    Абсолютно аналогична функции z11,  меняется только смещение,  из  которого
извлекаются данные,  так как вместо блока  идентификаторов  используется  блок
констант.

    * z13 - сгенерировать команду ввода числа с устройства ввода.

    Как и в случае с командой вывода,  данная команда будет реализована,   как
вызов подпрограммы. За ввод числа с клавиатуры  у  нас  отвечает  подпрограмма
ProcInInt, которая даже обрабатывает некоторые ошибки при вводе.  Правда,  эту
ее особенность я не тестировал, поэтому не могу сказать, что произойдет,  если
при работе скомпилированной программы пользователь введет ошибочные данные.
    В соответствии с описанием команды INP абстрактной машины,  мы должны счи-
тать число с устройства ввода (для нашего случая - клавиатуры),   и  поместить
его в стек. Учитывая, что в результате вызова функции ProcInInt введенное чис-
ло возвращается в регистре ax, это реализуется следующими командами:

                         call      ProcInInt
                         push      ax

    С генерацией команды push ax никаких команд не возникает  -  это  однобай-
тная команда с кодом 50h.  А вот с командой call придется повозиться.  Нам из-
вестен адрес ProcInInt - 0x0AB9.  Когда-то я пытался программировать на ассем-
блере процессора 6502 фирмы Motorola.  Для него  вызов  подпрограммы  выглядел
просто: сначала нужно было указать код команды вызова,  а за ним следовали два
байта - адрес самой подпрограммы (хотя шина данных микропроцессора была 8-бит-
ной, адрес содержал 16-бит, т.е.  можно было напрямую адресоваться к 64К памя-
ти). Это очень простой для понимания и реализации подход.  Однако он имеет ог-
ромный недостаток,  который сводит на нет все преимущества.  При таком формате
вызова нельзя писать перемещаемые программы!  Поэтому в сегментированной моде-
ли памяти процессоров i80x86 применяется другой  способ:  после  кода  команды
call используется не абсолютный адрес подпрограммы,  а  его  смещение  относи-
тельно значения, содержащегося в счетчике команд, т.е. относительно адреса те-
кущей выполняемой команды.
    Об этом надо было упомянуть выше,  когда я описывал функцию z04,  но лучше
поздно, чем никогда. Зато теперь я просто приведу формулу, по которой мы полу-
чаем необходимое смещение, а все комментарии к ней Вы найдете в описании z04:

                    ProcInIntOffset = - (Counter-0x0AB9)

    Итак, сначала мы заносим в буфер кода команду call - байт E8h, за ним сме-
щение ProcInIntOffset (переменная описана как int - это 2 байта, хранящие чис-
ло со знаком, причем при занесении их в CodeBuf, необходимо поменять их места-
ми, что можно сделать с помощью сдвигов, а я делаю при помощи небольшой ассем-
блерной вставки в тело программы на Си).  Осталось добавить команду push ax  -
код 50h, и не забыть увеличить значения Counter и CodeBufPointer на 4 байта.
    Маленькое замечание: если посмотреть листинг compiler.cpp,  то  в  функции
z13 от смещения ProcInIntOffset, вычисляемого по вышеприведенной формуле вычи-
тается число 3. Это не должно Вас смущать,  так как я уже упоминал,  что счет-
чик Counter инкрементируется не после каждого байта, занесенного в буфер кода,
а сразу на некоторое число,  поэтому число 3 - просто согласование,   и  взято
почти с потолка,  вернее из TurboDebugger,  в котором вызов функции  ProcInInt
должен выглядеть как call 0AB9h.  Если есть расхождение -  немного  исправляем
формулу, и всех делов!

    * z14 - генерация команды умножения или деления.

    В функцию передается параметр k - в зависимости от  его  значения  генери-
руется команда умножения или деления.  В соответствии с описанием команды  аб-
страктной машины MUL, ее представление на языке ассемблера для i80x86:

                              pop       bx
                              pop       ax
                              cwd
                              mul       bx
                              push      ax

    Со стека снимаются два перемножаемых 16-битовых числа, они умножаются (ко-
манда cwd 'расширяет' регистр ax до двойного слова dx:ax,  чтобы в  результате
получить 16-битовый результат), после чего результат заносится в стек.
    Команда DIV выполняется аналогично.

    * z15 - генерация команды сложения, либо вычитания.

    То же самое, что и z14. Для примера, команда сложения реализуется так:

                              pop       ax
                              pop       bx
                              add       bx,ax
                              push      ax

    * z16 - k=0

    * z17 - если k==1 то сгенерировать команду инвертирования числа.

    Без комментариев:

                              pop       ax
                              neg       ax
                              push      ax


    Мы рассмотрели все функции,  которые используются при работе  компилятора.
Теперь можно приступить к разбору конкретного примера,  на котором можно будет
понять, как работает компилятор в целом.
    В качестве примера возьмем небольшую программу на Милане,  в которой будет
задействовано большинство важнейших функций.
    В каталоге ML307\SAMPLE находятся файлы sample.mil и sample.com,   которые
представляют собой пример на языке Милан,  и его скомпилированную версию соот-
ветственно.
    Дальнейшее изложение будет базироваться именно на этих файлах.
    Итак, вот тот пример,  который я выбрал в качестве иллюстрации работы ком-
пилятора:

          begin
             n:=0;
             while n<100 do
                n:=n+1;
                output(n);
             od
          end

    Как видите,  здесь происходит и работа с идентификаторами,  и арифметичес-
кие действия, используется цикл (который кроме всего прочего,  включает выпол-
нение проверки условия) и вывод на экран (который демонстрирует,  как происхо-
дит работа с подпрограммами).
    Использовать более сложный пример,  который  задействует  все  возможности
компилятора  на  100%,    не  имеет  смысла,    так  как  он  станет  поистине
'неподъемным',  так как способности человека по восприятию больших объемов ин-
формации сильно ограничены. Если же Вы думаете, что этот пример слишком прими-
тивен,  то сильно ошибаетесь,  в чем убедитесь,  когда посмотрите на результат
дизассемблирования файла sample.com.
    Надеюсь, теперь все убеждены,  что пример почти идеален ;) и можно присту-
пить к его разбору.

    Запустим компилятор ml307 и загрузим файл sample.mil.   Откомпилируем  его
командой меню 'Компиляция'. Теперь мы имеем исполнимый файл sample.com,  кото-
рый необходимо загрузить в Turbo Debugger,  или любой другой  дизассемблер/от-
ладчик. В окне CPU можно видеть результат - коды команд,  сгенерированные ком-
пилятором,  представлены в виде ассемблерных команд.  Ниже я привожу этот лис-
тинг,  опуская только код для данных и вспомогательных подпрограмм,   и  попы-
таюсь объяснить,  как же компилятор генерирует команды в машинном коде процес-
сора на основании текста программы на языке Милан.

    Исполнение нашей программы начнется по адресу 100h,  который был задан ко-
мандой org. На эту команду и показывает маленький треугольник.  Первые две ко-
манды производят настройку указателя стека,  следующая за ними команда обеспе-
чивает переход на код,  сгенерированный ml307.  Многоточием показан  опущенный
код, который содержит массивы идентификаторов, констант, стек, а также вспомо-
гательные подпрограммы.
    Следующие за многоточием команды помечены номерами для удобства ссылок  на
них,  и получены в результате компиляции,  т.е.  это  и  есть  наша  программа
sample, но переведенная на язык двоичных кодов. Вооружившись диаграммами рабо-
ты компилятора, попробуем продраться сквозь них.

      cs:0100BC5A01         mov    sp,015A
      cs:0103 81C4FE07       add    sp,07FE
      cs:0107 E9630A         jmp    0B6D

  ..........................................

  001 cs:0B6D BB0A01         mov    bx,010A
  002 cs:0B70 83C300         add    bx,0000
  003 cs:0B73 8B07           mov    ax,[bx]
  004 cs:0B75 50             push   ax
  005 cs:0B76 58             pop    ax
  006 cs:0B77 BB3201         mov    bx,0132
  007 cs:0B7A 83C300         add    bx,0000
  008 cs:0B7D 8907           mov    [bx],ax
  009 cs:0B7F BB3201         mov    bx,0132
  010 cs:0B82 83C300         add    bx,0000
  011 cs:0B85 8B07           mov    ax,[bx]
  012 cs:0B87 50             push   ax
  013 cs:0B88 BB0A01         mov    bx,010A
  014 cs:0B8B 83C302         add    bx,0002
  015 cs:0B8E 8B07           mov    ax,[bx]
  016 cs:0B90 50             push   ax
  017 cs:0B91 5B             pop    bx
  018 cs:0B92 58             pop    ax
  019 cs:0B93 3BC3           cmp    ax,bx
  020 cs:0B95 7C06           jl     0B9D
  021 cs:0B97 B80100         mov    ax,0001
  022 cs:0B9A EB04           jmp    0BA0
  023 cs:0B9C 90             nop
  024 cs:0B9D B80000         mov    ax,0000
  025 cs:0BA0 50             push   ax
  026 cs:0BA1 58             pop    ax
  027 cs:0BA2 3D0100         cmp    ax,0001
  028 cs:0BA5 7503           jne    0BAA
  029 cs:0BA7 E93000         jmp    0BDA
  030 cs:0BAA BB3201         mov    bx,0132
  031 cs:0BAD 83C300         add    bx,0000
  032 cs:0BB0 8B07           mov    ax,[bx]
  033 cs:0BB2 50             push   ax
  034 cs:0BB3 BB0A01         mov    bx,010A
  035 cs:0BB6 83C304         add    bx,0004
  036 cs:0BB9 8B07           mov    ax,[bx]
  037 cs:0BBB 50             push   ax
  038 cs:0BBC 58             pop    ax
  039 cs:0BBD 5B             pop    bx
  040 cs:0BBE 03C3           add    ax,bx
  041 cs:0BC0 50             push   ax
  042 cs:0BC1 58             pop    ax
  043 cs:0BC2 BB3201         mov    bx,0132
  044 cs:0BC5 83C300         add    bx,0000
  045 cs:0BC8 8907           mov    [bx],ax
  046 cs:0BCA BB3201         mov    bx,0132
  047 cs:0BCD 83C300         add    bx,0000
  048 cs:0BD0 8B07           mov    ax,[bx]
  049 cs:0BD2 50             push   ax
  050 cs:0BD3 58             pop    ax
  051 cs:0BD4 E892FD         call   0969
  052 cs:0BD7 E9A5FF         jmp    0B7F
  053 cs:0BDA B44C           mov    ah,4C
  054 cs:0BDC CD21           int    21

    Проверив наличие ключевого слова begin, которое должно идти первым, компи-
лятор передает управление функции L_PROG,   отвечающей  за  разбор  последова-
тельности операторов. В соответствии с диаграммой для конструкции L, начинает-
ся выполнение S_PROG, то есть разбор конкретного оператора.
    Первым у нас в программе идет присвоение,  поэтому работает  самая  первая
ветка S-диаграммы. В локальной переменной k запоминается номер идентификатора,
проверяется, является ли следующая лексема знаком присвоения,  и если это так,
начинает работать E_PROG - подпрограмма,  отвечающая за разбор  арифметических
выражений. Я не буду слишком подробно разбирать,  как она работает,  это видно
по диаграмме,  надо только дотошно в ней разобраться,  однако в результате  мы
получаем первые коды - это команды с номерами 001-004.  Здесь сначала в bx за-
носится смещение массива констант, затем прибавляется значение 2*k,  где k-это
номер константы в массиве (в нашем случае прибавляется 0000h, так как констан-
та 0 появилась в тексте первой и на том же месте находится в массиве),   после
этого при помощи косвенной адресации в регистр ax загружается сама  константа,
которая сохраняется в стеке.  На этом выполнение E_PROG завершается,  и  начи-
нает работать функция z03 - со стека снимается значение,  которое присваивает-
ся идентификатору с номером k. Это команды 005-008.
    Происходит возврат в L_PROG, которая убедившись,  что разобранный оператор
заканчивается разделителем - точкой с  запятой,    снова  передает  управление
S_PROG, которая начнет обрабатывать следующий оператор.
    Остановимся немного,  и оглянемся назад.  В результате всех  вышеописанных
действий мы получили удивительную вещь - одна строка на Милане:

                                  n := 0 ;

была преобразована в двоичные коды процессора (совокупность  команд  001-008),
которые выполняют то же самое действие!  Что ни говорите,  но мне это  кажется
немного удивительным.
    Однако прочь восторженные чувства, которые чужды машине.  Следующий опера-
тор гораздо серьезнее и сложнее - это цикл с предусловием while.
    Посмотрите на диаграмму - при встрече ключевого слова while начинает рабо-
тать z05,  которая запоминает адрес следующей команды,  которой еще не  сущес-
твует, в переменной n. Затем происходит вызов B_PROG - функции,  отвечающей за
обработку условия.
    Внутри B_PROG вызывается E_PROG,  чтобы вычислить значение левой части ус-
ловного выражения.  В нашем случае условие представляет  собой  строку  n<100,
поэтому левая часть - всего лишь один идентификатор n,  поэтому на этом  этапе
его значение извлекается из массива, и заносится в стек - команды 009-012. Те-
перь обрабатывается код операции сравнения, хотя это слишком громко сказано -
пока он лишь только запоминается. Снова вызывается E_PROG - теперь уже для об-
работки правой части выражения. Она тоже проста - константа 100.  Так же прос-
то и действие - она извлекается из массива и заносится в стек.   За  эти  дей-
ствия отвечают команды 013-016.
    Теперь начинает работу z02, которая генерирует команду сравнения.  Так как
она уже достаточно подробно описана ранее,  скажу кратко - со стека  снимаются
два числа,  над ними производится необходимая операция сравнения (в нашем слу-
чае сравнение 'на меньше') и в стек заносится результат  операции  -  значение
TRUE (константа 0000h), если сравнение истинно,  или FALSE (0001h) в противном
случае.
    Выполнение B_PROG успешно завершено (оно может быть и прервано,   так  как
хотя я об этом и не упоминаю, компилятор обрабатывает и некоторые ошибки,  при
обнаружении которых компиляция прерывается с выдачей кода  ошибки)  и  продол-
жается обработка цикла - за условием идет ключевое слово do и выполняется z06,
которая генерирует код для обхода тела цикла при ложности  условия.    Команды
026-029 имеют следующий смысл: снять со стека число,    которое  является  ре-
зультатом операции сравнения,  если это FALSE - перепрыгнуть  через  код  тела
цикла, если же это TRUE, начать выполнять его первую команду.
    Теперь обрабатываются команды,  составляющие тело цикла -  последовательно
вызываются L_PROG, S_PROG,  и компилятор идет по первой ветке S_PROG,  так как
первым оператором цикла является присвоение

                               n:=n+1;

    Выше уже было рассмотрено,  как компилятор  работает  с  ним  (на  примере
n:=0), поэтому никаких комментариев давать не буду - смотрите команды 030-045.
    Следующий оператор цикла - вывод на экран

                               output(n);

    Идем по второй ветви диаграммы для конструкции S  -  компилятор  встречает
ключевое слово output, проверяет наличие открывающей скобки,  после чего вызы-
вает E_PROG, чтобы обработать арифметическое выражение, находящееся внутри.
    Именно E_PROG выдает команды 046-049,  которые помещают  в  стек  значение
арифметического выражения, в нашем случае просто значение идентификатора n.
    Теперь проверяется наличие закрывающей скобки, и начинает работать z04,  в
результате получаются еще две команды 050,051 - снятие со стека значения ариф-
метического выражения в регистр ax, и вызов подпрограммы вывода на экран,  для
которой содержимое регистра ax является входным параметром.
    Следующей лексемой в массиве является ключевое слово od,  которое сообщает
компилятору,  что тело цикла завершилось.  Для обработки этого события  служит
функция z07,  которая должна сгенерировать команду возврата на проверку  усло-
вия заголовка цикла. У нас уже есть все необходимые данные для этого,  так по-
лучается команда перехода 052.
    За od идет ключевое слово end - признак того,  что программа  закончилась.
Вызываем последнюю функцию z01, которая добавляет в буфер коды для двух заклю-
чительных команд:

                              mov       ah,4Ch
                              int       21

    Это просто вызов стандартной функции DOS - завершение программы.    Дальше
обо всем позаботится старушка DOS - освободит память,  которая была занята на-
ми и выполнит другие необходимые действия.

    Вот пожалуй, и все. Я попытался объяснить диаграммы и функции компилятора,
так как сам понял, как сам сделал. Привел пример,  демонстрирующий работу всей
системы целиком, разобрал сгенерированный двоичный код. Возможно,  мое изложе-
ние покажется Вам несколько сумбурным или непонятным. Что могу сказать?  Пожа-
луй, это действительно так.  Увы,  я не смог прыгнуть выше головы - не хватило
времени и сил, таланта наконец. Однако буду надеяться,  что кому-то моя работа
окажется хотя немного полезной,  и кто-нибудь помянет добрым словом сей скром-
ный труд и его Автора.

                                                12 февраля 2000 г.
                                                                                                                                                                                                     
