Компилятор домашнего приготовления (ч.5)

Автор: Виталий Белик

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

Shards of mirror

Я разделил опкоды на группы, чтобы проще было понять, с чем они работают. Операции с центральным камушком в одной группе, а с сопроцессором в другой. В первой группе нам пригодятся следующие функции:

 
  // Сервисные опкоды *****************************************************
  // Поднимает планку стека. Фактически избавляет стек от мусора, «затирая» ненужные 
  // данные. «Затирая» -  слово не совсем удачное, ибо на самом деле ничего не затирается
  // Просто указатель стека перемещается так, что эти, уже ненужные, данные оказываются 
  // вне игры. Они потом наверняка затрутся другими данными
  Function AddESP (i:byte):String;;       begin Result:=#$83#$C4+chr(i);                  end;

  // Помещает значение регистра ЕАХ в стек. Будет использоваться если в выражении
  // появится подвыражение, и уже рассчитанное целое число необходимо будет сохранить
  // Так же может применяться при приведении целого типа к вещественному. В этом случае
  // рассчитанный результат переносится в стек, а уж из него выбирается в сопроцессор.
  function PushEAX;      begin Result:=#$50;                             end;

  // Выбирает из стека в регистр ЕАХ значение на его вершине. Может использоваться после 
  // расчета. Честно говоря, AddESP больше будет использоваться, но и эта команда может 
  // вполне пригодится. Она весит всего один байт в отличии от трех байтов AddESP, так что 
  // если позволит случай, присобачим ее в нужное место
  function PopEAX;       begin Result:=#$58;                             end;

  // Помещает в стек значение регистра EDX. Пригодится при операции деления и 
  // умножения. Помните операции DIV и MOD работают и с EDX регистром в случае с 32 
  // битными числами. Может быть,  его понадобится сохранить. На всякий случай.
  function PushEDX;      begin Result:=#$52;                             end;

//Выбирает из стека в EDX. Соответственно восстанавливать после операций 
// целочисленного деления
 function PopEDX;       begin Result:=#$5A;                             end;

  // Обнулятор для EDX. Опять-таки, пригодится при операциях деления умножения,
  // чтоб не дай бог процессор не умножил число на какой-то мусор, ведь при умножении 32
  // битных чисел один из множителей находится в EDX:EAX.
  function XOR_EDX_EDX;  begin Result:=#$33#$D2;                         end;

  // Обменивает регистры EAX EDX содержимым. После того как прошла операция деления
  // В ЕАХ остается частное, а в EDX остаток. И если нам нужен остаток, то либо проводить 
  // выбор из EDX, либо поменять их местами. Второе проще, если принять во внимание что
  // все операции будем проводить только в ЕАХ. В общем эта операция пригодится.
  function XCHG_EAX_EDX; begin Result:=#$92;                             end;

  // Помещает в регистр ЕАХ то, что сохранено в вершине стека, не смещая при этом 
  // верхушку стека. Если нужно оставить вершину на своем месте, но позарез выбрать из 
  // него значение  - эта операция пригодится.
  function MOV_EAX_ESP;  begin Result:=#$8B#$04#$24;                     end;

   // Передает в переменную значение другой переменной. Поскольку мы договорились
   // что наши переменные будут вариантны нужно пересылать все 10 байт, чем и
   // занимается инструкция  REP     MOVS BYTE. Остается ей передать адреса
   // источника и получателя и дело в шляпе. Это пригодится при присваивании
   // одной переменной другой, поскольку данные просто копируются
   function XCHG_MEM_MEM2;
   begin
    Result:=       #$B9+DWordToStr(10);//MOV     ECX, 0A
    Result:=Result+#$BE+mFrom.Addr;    //MOV     ESI, mFrom
    Result:=Result+#$BF+mTo.Addr;      //MOV     ESI, mTo
    Result:=Result+#$F3;               //REP     MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]
   end;

На всякий случай поясню, что #$ — говорит о том, что это символ с указанным кодом. Откуда я их взял? Да очень просто: написав команду в ассемблере, подсмотрел ее опкод в отладчике. Вот, например опкод для AddESP (см. рисунок 1):

* Комментарий автора.

Вообще, кстати, это смещение не обязательно должно быть кратно размеру элемента стека. В нашем случае кратно 32 битам или 4-м байтам. Никто не помешает написать там другое число, указатель стека сместится. Но при этом программа уже отработает неверно. Как минимум она неверно завершится. Данные в стеке приобретут другую форму для операторов работы со стеком. Впрочем, обратная сторона – никто не мешает нам освобождать элементы стека программы, в обратном порядке соблюдая лишь размерность помещенных в него данных, не важно какого они размера были. Но ради приличия, постараемся соблюдать кратность.

Идем дальше. Рассмотрим операции с ЦП: 

 
  // CPU опкоды *****************************************************************
  // Разница между значением в вершине стека и ЕАХ. Применяется при операции
  // вычитания, после отработки вложенного выражения, которое свой результат
  // помещает в ЕАХ, а результат до начала выражения должен быть помещен
  // в стек. Именно их разность и будет вычислять данная комбинация
  function Sub_ESP_EAX;  begin Result:=#$29#$04#$24;                     end;

  // Та же ерунда, но для операций сложения. Суммирует результат подвыражения
  // и результат, имеющийся до вычисления этого подвыражения, сохраненный в
  // стеке
  function ADD_EAX_ESP;  begin Result:=#$03#$04#$24;                     end;

  // То же для операции умножения. Напоминаю, что эти действия проводятся
  // над 32 битными целыми.
  function MUL_EAX_ESP;  begin Result:=#$F7#$24#$24;                     end;

 // Помещает в ЕАХ число. Будет использоваться, если в выражении встретится
 // целочисленная константа. В основном если она будет первой в выражении
 function MovEAXConst;  begin Result:=#$B8+DWordToStr(i);               end;

  // Вычитает из ЕАХ целую константу. Этот оператор не будет работать с
  // первыми операндами. Только со следующими, чтоб не получить неправильный
  // результат.
  function SubEAXConst;  begin Result:=#$2D+DWordToStr(i);               end;

  // Оператор суммирования ЕАХ и целой константы
  function ADDEAXConst;  begin Result:=#$05+DWordToStr(i);               end;

  // Заносит в стек целую константу. Например, может пригодится для передачи
  // целого сопроцессору через стек
  function PushConst;    begin Result:=#$68+dwordToStr(i);               end;

  // Заносит в стек целое из переменной. Стоит проследить за тем, чтобы эта
  // операция использовалась только в том случае если тип переменной целочисленный
  // Опять-таки можно использовать для передачи сопроцессору значения из переменной
  function PushMem;      begin Result:=#$FF#$35+m.Addr;                  end;

  // Выборка из вершины стека целого числа в переменную. Тоже может пригодится
  // в будущем 
 function PopMem;       begin Result:=#$8F#05+m.Addr;                   end;

  // Передача из переменной целого 32-битного числа в регистр ЕАХ
  // Его можно использовать в начале выражения, когда первый операнд
  // переменная, и ее нужно внести в регистр для начала операции
  function MovEAXMem;    begin Result:=#$A1+m.Addr;                      end;

  // Обратное действие - из регистра целое переносится в переменку
  // Применять после выражения, когда нужно присвоить вычисленное переменной
  function MovMemEAX;    begin Result:=#$A3+m.Addr;                      end;

  // Особые случаи, когда переменную инициализируют числом. При хорошем
  // оптимизаторе можно проверять, обрабатывалась ли эта переменная уже
  // и если не обрабатывалась записывать значение не в ходе выполнения
  // программы, а при компиляции в секцию данных. Но если переменная уже участвовала
  // в присвоении, придется этот опкод применить. В нашем случае оптимизатора нет
  // посему применять его будем везде где не лень.
  function MovMemConst;  begin Result:=#$C7#$05+m.Addr+DWordToStr(i);    end;

 //Используется для вычитания операнда-переменной из значения в ЕАХ, просчитанного
 //в ходе обработки выражения. Эта операция не должна применяться к первому
 //операнду. Только к последующим, а первый должен помещаться в регистр
 //вычисления как есть. 
 function SubEAXMem;    begin Result:=#$2B#$05+m.Addr;                  end;

  // Тоже самое касается и сложения. Что бы не подхватить мусор при вычислении,
  // первый операнд должен помещаться в регистр, а уж к последующим применять этот
  // опкод.
  function ADDEAXMem;    begin Result:=#$03#$05+m.Addr;                  end;

  // Эта функция проводит опкод умножения переменной на значение в регистре -
  // значение уже рассчитанное в результате вычисления выражения. Не забыть
  // при этом что для 32-битные числа при умножении и делении используют регистр
  // EDX, потому его нужно обнулять, при необходимости сохраняя в стек
  function MUL_EAX_Mem;  begin Result:=#$F7#$25+m.Addr;                  end;

  // Инкремент. Аналог сишного ++, но для регистра. Инкременты и декременты
  // наш язык унаследует от Си - очень уж там это удобно сделано.
  // Кстати, забегая наперед, скажу, что и самоприсваиваемые операторы типа
  // += мы позаимствуем.
  function _inc;         begin Result:=#$40;                             end;

  // Инкремент для переменной целого типа.
  function incMem;       begin Result:=#$FF#$05+m.Addr;                  end;

  // Декременты. Действуют по тем же правилам.
  function _dec;         begin Result:=#$48;                             end;
  function decMem;       begin Result:=#$FF#$0D+m.Addr;                  end;

  // Операция деления значения из ЕАХ, того которое рассчитывается при вычислении
  // выражения, на значение, сохраненное по каким-то соображениям в стеке.
  // Предполагается применяться при операции деления, после расчета вложенного
  // выражения. В стек помещается уже рассчитанное целое, рассчитывается
  // вложенное выражение, после чего сохраненное делится на результат
  // подвыражения. Грубо говоря, если есть 2*8/(5+3) то 16 пойдет в стек, а после
  // вычисления число 8 будет поделено на 16. Здесь нужно будет указать
  // какой элемент стека рассчитывать, если это верхушка, то 0 если он лежит чуть
  // глубже (вдруг в стек еще пришлось что-то сохранить) то указать номер
  // позиции, который должен быть кратен 4-м.
  function DIV_EAX_ESP_;begin Result:=#$F7#$74#$24;if(i>0)then Result:=Result+chr(i); end;

  // Достаточно непростая процедура деления. Делится целое число на значение в ЕАХ
  // Поскольку операция деления затрагивает регистр EDX, приходится обнулять его
  // сохраняя если необходимо значение регистра в стеке программы, вдруг понадобится
  // Здесь же учитывается, что нужно получить на выходе - остаток от деления или
  // частное. Если остаток - после деления необходимо добавить операцию смены содержимого
  // ЕАХ и EDX, поскольку остаток помещается в EDX, а работаем то мы с ЕАХ, ввиду
  // отсутствия какой-либо оптимизации. Был бы оптимизатор, он бы не менял значения
  // а просто переключал на выборку из EDX.  В общем это дело будущего - избавится
  // от "ничего" не весящей инструкции обмена.
  function DIV_EAX_CONST;
  begin
    Result:=PushEDX+PushConst(i)+XOR_EDX_EDX+DIV_EAX_ESP;
    if not ItNotMod then Result:=Result+XCHG_EAX_EDX;
    Result:=Result+PopEDX+AddESP(4);
   end;

   // Тоже скомбинированная функция, чтоб в классах много не писать. Делит ЕАХ
   // и значение, сохраненное в стеке. Работает с Целыми числами
  function DIV_EAX_ESP;
  begin
   Result:=PushEAX+XOR_EDX_EDX+DIV_EAX_ESP_(4);
   if not ItNotMod then Result:=Result+XCHG_EAX_EDX;
   Result:=Result+AddESP(4);
   {внимание EDX не бережется. Осторожнее в будущем}
  end;

  // Их вариация. Работает с переменной. Тоже скомбинированна, для того чтоб
  // в классах обработчиках писать поменьше. Принцип деления тот же.
  function DIV_EAX_Mem;
  begin
   Result:=PushMem(m)+XOR_EDX_EDX+DIV_EAX_ESP_(4);
   if not ItNotMod then Result:=Result+XCHG_EAX_EDX;
   Result:=Result+AddESP(4);
  end;

Это только для начала. Особо много их не понадобиться, конечно, но вполне возможно этот список будет пополняться по ходу дополнения компилятора новыми наворотами.

Теперь давайте посмотрим, как обстоит дело с функционалом для расчета вещественных чисел. Все знают, как работает сопроцессор? В нем есть регистры ST0-7. Семь штук всего. Они работают скорее как стек, чем как обычные регистры ЦП. Когда из памяти число передается сопроцессору, значения, уже содержащиеся в регистрах сдвигаются, так что верхушка этого стека – регистр ST0 становится свободным, его значение перемещается в ST1, смещая его значение в ST2 и так далее. Значение из ST7, если не ошибаюсь, выпадает из сопроцессора в никуда. Давайте посмотрим, как это выглядит в натуре. Если прописать в ассемблере код (см. рисунок 2):

И открыть его в отладчике, то можно, пройдясь пошагово, посмотреть, как сопроцессор «питается» (см. рисунок 3):

Число 45.256 ушло в вершину стека сопроцессора. Если продолжить трассировку, то это число сместится по стеку ниже. Или выше? Какая разница – сместится в следующий регистр и все тут (см. рисунок 4):

Видите. 45 с копейками уступило месту новому значению. И так далее, помещая значение в стек сопроцессора, программа сдвигает предыдущий «мусор», который в принципе то может по праву считаться мусором. Думаю что ситуаций, когда не хватает семи регистров, будет настолько мало, что нам их не стоит пока что, по крайней мере, учитывать.

Пожалуй, единственное вычисление – вычисление трансцедентных функций типа синуса-косинуса потребует задействовать 3-4 регистра сопроцессора. Соответственно когда число «выгребается» из сопроцессора, по идее оно «выгребается» из верхушки стека из ST0. Идущие после него значения пододвигаются поближе к вершине. 3.0 уйдет в память, а 45 с копейками займет его место в ST0. То, что было в ST2 переселится в ST1, на место 45-ти, и так далее за одним маленьким исключением – стек прокрутится, вершина его не исчезнет, а переместится по кругу в последний регистр стека ST7, и продолжит вращаться, двигаясь к своему прежнему месту. Можете попробовать побаловаться примером типа (см. рисунок 5):

Заодно заметьте, что если стек переполнен, а мы продолжаем в него посылать данные, процессор не помещает в него данные, а ставит NAN значение. Это тоже стоит учитывать. И стараться не посылать более чем 7 значений без выборки. Впрочем, если уж пришлось нагрузить проц по «самые помидорки», дело может спасти команда FINIT, которая сбрасывает сопроцессор в изначальное состояние. Если точнее инициализирует его регистры значением по умолчанию. Выглядит это так: все правильные (valid-ные) значения собираются, и перестраиваются к вершине стека в порядке их внесения в стек (это я с позиции наблюдателя говорю), таким образом, чтобы между ними не было инвалидных значений. Инвалидные же значения падают на дно стека. Сбрасываются флаги состояний. Эта информация для интереса, не более. Врядли нам понадобятся такие «выкрутасы».

Итак. Операции, которые нам понадобятся для работы с вещественными:

 
  // FPU опкоды ******************************************************************
  // Инициализирует сопроцессор. Пригодится если встретится вещественное, для включения
  // сопроцессора к действу над выражением. Кстати подробнее об этом можно почитать в
  // хелпе или в [1]
  function FINIT:String; begin Result:=#$DB#$E3;                         end;

   // Помещает 10 байтовое значение из переменной в верхушку стека сопроцессора
   // FSTP    TBYTE PTR DS:[mem]
   function FPopDMem;     begin Result:=#$DB#$3D+m.Addr;                  end;

   // Помещает в вершину стека единицу. Будем использовать для инкремента-декремента.
   // Почему бы не применить его для вещественных.
   function FLD1;         begin Result:=#$D9#$E8;                         end;

   // Вычисляет Синус и косинус. Может быть, и не пригодится на первых порах, 
  // но показательно будет ее запрограммировать.
  function FSIN;         begin Result:=#$D9#$FE;                         end;
  function FCOS;         begin Result:=#$D9#$FF;                         end;
  // Кстати. в функционале сопроцессора есть опкод FSINCOS, который рассчитывает
  // сразу и синус и косинус, помещая в первые два регистра результаты.
  // В принципе можно использовать и ее, но... Религия Пятен На Солнце мешает 😀
  // Кстати бытует мнение, что эта операция медленная. Не могу ручаться за его правдивость.

  // Вносит в вершину стека число ПИ. Оно настолько магическое, что его
  // специально вставили отдельной командой. Круто!
  function FLDPI;        begin Result:=#$D9#$EB;                         end;

  // Эта команда вычитает из вершины стека целое число, помещенное в стек программы
  // Вообще при общении с сопроцессором я предполагаю активно использовать
  // программный стек (ту часть памяти, на которую указывает ESP),
  // и через него передавать данные сопроцессору.  FISUB   DWORD PTR SS:[ESP]
  function FISUB_ESP;    begin Result:=#$DA#$24#$24;                     end;

  // функция, меняющая местами значения в регистрах. Аналог XCHG.
  // Он понадобится, скажем, при вычитании (см. на функцию ниже), когда
  // результат вычитания нужно поместить в ST0 для взятия его оттуда 
  function FXCH;         begin Result:=#$D9#$C9;                         end;

  // Операции вычитания. Вычитаются вершины стека сопроцессора. В зависимости от
  // ситуации могут понадобится разные вариации вычитания либо ST0 из ST1 либо наоборот 
  function FSUB_ST1_ST;  begin Result:=#$DC#$E9+FXCH;                    end;
  function FSUB_ST_ST1;  begin Result:=#$D8#$E1;                         end;

  // Помещает в стек сопроцессора 32-битное целое значение из вершины стека программы
  // Таким образом, происходит обмен между программой и сопроцессором - через 
  // программный стек
  // FILD    DWORD PTR SS:[ESP]
 function FILD_ESP;     begin Result:=#$DB#$04#$24;                     end;

  // Обратная операция. Помещение в стек 10 байтного значения из регистра
  // сопроцессора. Нужно ли говорить что в стеке программы должно быть
  // заранее зарезервировано необходимое кол-во элементов? Думаю нет.
  // Впрочем, потом я покажу это на примере
  // Фактический аналог:  PUSH    EAX(3раза);  FSTP  TBYTE PTR SS:[ESP]
  function FSTP_ESP;     begin Result:=#$50#$50#$50#$DB#$3C#$24;         end;

 //То же самое, но в стек помещается 32-битное целое (округленное) из ST0
 //сопроцессора. В стеке программы должно быть зарезервировано 4 байта - стандартный
 //элемент стека. FISTP   DWORD PTR SS:[ESP]
 function FISTP_ESP;    begin Result:=#$DB#$1C#$24;                     end;

  // Помещает в вершину стека сопроцессора, размещенные в стеке программы
  // 10 байт, представляющие вещественное число. На самом деле ради выравнивания стека
  // программы резервироваться под вещественное будет 12 байт, хотя конечно
  // никто не мешает резервировать чисто 10 байт, главное потом не запутаться
  // с указателем программного стека (ESP) и не выйти из программы
  // в какую-нибудь неизвестность.   FLD     TBYTE PTR SS:[ESP]
  function FLD_ESP;      begin Result:=#$DB#$2C#$24;                     end;

  // суммирует вершину стека сопроцессора (ST0) и другой регистр сопроцессора
  // В параметрах указывается номер регистра. Результат помещается в ST0 
  function FADD_ST0_ST;  begin Result:=#$D8+chr(i+$C0);                  end;

  // Умножение ST0 и другого регистра, по указанному номеру. Результат так же
  // помещается в верхушку стека сопроцессора.
  function FMUL_ST0_ST;  begin Result:=#$D8+chr(i+$C8);                  end;

  // Деление ST0 и другого регистра, по указанному номеру. Результат так же
  // помещается в верхушку стека сопроцессора.
  function FDIV_ST0_ST;  begin Result:=#$D8+chr(i+$F0);                   end;

  // Может пригодиться вариация деления. Когда делится ST1 на ST0
  function FDIV_ST1_ST;  begin Result:=#$DC#$F9+FXCH;                    end;

  // Комбинация (для удобства). Помещает в сопроцессор целое из ЕАХ.
  // Может часть применяться, поэтому удобнее сформировать этот набор
  // операций заранее, и в классах-обработчиках применять уже его, дабы не
  // писать много одно и тоже
  function FPushEAX;     begin Result:=PushEAX+FILD_ESP+AddESP(4);       end;

  // Помещают в сопроцессор из переменной вещественное или целое, минуя
  // пересылку через стек программы.
  // Аналоги: FLD     TBYTE PTR DS:[mem] и  FILD    DWORD PTR DS:[mem]
  function FPushDMem;    begin Result:=#$DB#$2D+m.Addr;                  end;
  function FPushIMem;    begin Result:=#$DB#$05+m.Addr;                  end;

  // Помещает целое число в вершину сопроцессора. Использует программный стек
  // куда целое предварительно помещается. Естественно после вложения
  // в сопроцессор стек нужно очищать, что и проделывает операция  ADD     ESP, 4
  function FPushIConst;
  begin
      Result:=#$68+DWordToStr(i);         //PUSH    Const
      Result:=Result+FILD_ESP;            //FILD    DWORD PTR SS:[ESP]
      Result:=Result+AddESP(4);           //ADD     ESP, 4
  end;

  // Помещает в сопроцессор в ST0 вещественное. Константу. Само число занимает
  // 80бит (10 байт), функция разбивает их на 5 двойных слов, чтоб достигалось
  // выравнивание относительно стека программы, кратное 4-м. Каждое слово
  // помещается в стек, начиная со старшего слова заканчивая младшим.
  // После чего выбирается из стека (не все, а только 10 байт, не забывайте
  // помещаем мы 12 байт) наше значение. Естественно после помещения
  // нужно очищать 12 байт стека, чем и занимается  AddESP(sizeof(TDoubleConst))
  function FPushDConst;
  var dc:TDoubleConst;i:integer;
  begin Result:='';
       FillChar(dc,sizeof(TDoubleConst),0);
       move(d,dc,sizeof(d));
       i:=high(dc);while i>=low(dc) do begin
        Result:=Result+PushConst(dc[i]);
        dec(i);
       end;
       Result:=Result+FLD_ESP+AddESP(sizeof(TDoubleConst));
  end;

  // Помещает округленное значение из вершины сопроцессора в регистр ЕАХ
  // для последующей целочисленной обработки. Например, может применяться в
  // качестве функции округления. Скажем так предусмотренна на будущее.
  function FPop_EAX_ST0;
  begin
    Result:=PushEAX+FISTP_ESP+MOV_EAX_ESP+AddESP(4);
  end;

Фух… Сколько писанины… Нелегкая это работа компилятор тащить за… гм… кхм…

Road on Selva

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

Еще раз повторюсь, что классы-обработчики операторов будут унаследованы от класса-шаблона, с едиными для всех методами. С какого оператора стоит начать? Полагаю что проще всего с операции сложения. Выше, в коде, я предусмотрел список операций, которые стоит сейчас запрограммировать – зарезервированные слова. Заранее давайте договоримся, что каждая операция будет являться функцией, т.е. что-то возвращать. Результат операций можно не учитывать, но на примере инкремента Си, сравнивая его с паскалем можно просмотреть выгоду такого. В Паскале для инкремента предусмотрена процедура inc(), которая увеличивает значение переменной на некое число. Но это – процедура. Я не могу использовать ее в выражении. В Си же немного по-другому. Я могу использовать инкремент в выражении без особого стеснения. В этом случае увеличится значение в переменной, и вернется уже увеличенное в выражение:

 
  int n=1;  int k=++n+1;

В результате мы одной строкой увеличили n, и это повлияло на результат в k. В языках Вирта пришлось бы писать в две строки:

 
  Var n=1;
  Begin
   Inc(n);
   K:=n+1;
  End.

Только потому, что Вирт не захотел (его, кстати, можно понять) предусматривать такую возможность «самоинкремента» как функцию. Строгость его конек, и зачастую эта строгость играет на руку программисту. Но строгость любят не все. Все любят удобство, этого в Си предостаточно. По крайней мере, в классическом Си удобств в написании выражений больше чем в классическом паскале, хотя иногда это доходит до абсурдного маразма, когда программисты пишут загуголину вроде:

 
  int i=1;  int k=k+++i+++k++;

Прям BrainFuck… Видимо, именно от этого и хотел избавить Никлаус Вирт свой язык, не дав ни малейшего шанса воспаленной фантазии мозга программиста написать каую-нибудь** «отсебятину».

** Комментарий автора.

Такие выражения напоминают мне язык «падонкаф». Вроде понятно, но не литературно. Однако обфускацию можно написать не только на Си. Скажем так, все языки имеют свойство поражать своими костылями, построенными кривыми руками и больной фантазией программиста.

Но, оставим лирику в стороне. Попробуем описать один из классов, отвечающий за операцию сложения, с использованием того модуля, в котором лежат функции с опкодами. Надеюсь, никто не будет против того, чтобы поместить его в отдельный модуль? В любом случае, я это сделаю. Модуль назову – Plus. Вот его начало:

 
  interface
   uses UnitCompiler,types;
  type
   //** Сложение ************************************
     TAlisaPlus=class(TAlisaReserved)
      private
       Function OnDouble(d:Extended):String;override;
       Function OnInt(i:integer):String;  override;
       Function OnMember(m:TAlisaMem):String;override;
       Function OnOperator(Oper:string):String;override;
     end;
   //**************************************

Не сильно замысловатый класс, наследующий необходимые методы от шаблона. OnInt для обработки целого операнда, OnDouble для вещественного и т.д. Теперь давайте реализуем по порядку. OnDouble будет выглядеть так:

 
 // Обработка операнда, если он является вещественным числом
  function TAlisaPlus.OnDouble(d: Extended): String;

  //****************************************
  (*Если результат операции до этого операнда был целочисленный
  Придется принимать меры к приведению целого к вещественному.
  То-есть нужно передать сопроцессору рассчитанное целое число
  Как я уже говорил сделать это можно поместив результат в стек из
  регистра ЕАХ, и выбрать из стека в сопроцессор. После не забыть
  скормить сопроцессору наше вещественное и провести операцию сложения*)
  procedure IfInt;
  begin
    Result:=Result+FPushEAX+FPushDConst(d)+FADD_ST0_ST(1);
  end;

  //****************************************
  (*Если же результат операции уже известен как вещественное, он должен
  находится на вершине стека сопроцессора в ST0, наша задача сводится к простому
  внесению в сопроцессор очередного операнда и сложения его с подвинувшимся
  в ST1 результата. Полученное при сложении число помещается в ST0, заменяя
  операнд, помещенный в сопроцессор
  Нужно учесть два момента:
  1:Если сопроцессор пуст, в него еще не помещали операнды выражения операцию
    сложения делать нельзя, либо помещать в сопроцессор ноль. Нам проще
    отследить какой по номеру операнд обрабатывается, и если он первый то
    не применять операцию сложения, а просто поместить в сопроцессор операнд
  2:Не стоит забывать про переполнение процессора. Если понапомещать в него
    операндов штук 10, то как я писал выше стек сопроцессора переполнится, и
    все следующие после 7-го элемента в сопроцессор пойдут NaN. В этом случае
    придется прибегнуть к услугам инструкции FINIT. Чтоб сбросить сопроцессор
    в изначальное состояние. В принципе никто не мешает нам заранее    
    проинициализировать    сопроцессор*)
  //****************************************
  procedure IfDouble;
  begin
   // Если это первый операнд, и он вещественный инициализируем сопроцессор
   if OperandNumber=1 then Result:=FINIT;
   //и вкладываем операнд
   Result:=Result+FPushDConst(d);
   // Если он не первый то проведем суммирование
   if OperandNumber>1 then Result:=Result+FADD_ST0_ST(1);
  end;
  //****************************************
  begin
  (*Не будем забывать инициализировать переменную-результирующую функции
  дабы не выдать мусор, оставшийся в стеке.*)
  Result:='';
  (*Если тип операции еще не известен, но первый операнд вещественное
  то вся операция подразумевает работу с вещественными - ее тип вещественное число*)
  if TypeOfLastOperand=_Unknown then TypeOfLastOperand:=_Double;

  (*Проверим, какой тип операции был последним. Для каждого типа придется писать
  свой обработчик, потому как универсальных операций для всех на ассемблере
  не существует. Пока что у нас два типа - целые и вещественные числа*)
  case TypeOfLastOperand of
   _Int:IfInt;
   _Double:IfDouble;
  end;
   (*На последок в каждом обработчике нужно не забыть указать к какому типу привести
   результат выражения. Причем приоритет больше у вещественного, потому как
   целое является часным случаем его.*)
   if TypeOfLastOperand=_Int then TypeOfLastOperand:=_Double;
  end;

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

 
  (*Если же у нас операнд целое*)
  function TAlisaPlus.OnInt(i: integer): String;
  //****************************************
  (*Будем работать с ним в рамках ЦП. Незачем загружать сопроцессор такой мелочью
  тем паче с его округлением можно нарваться на неприятность. Все целые, пока
  они не будут приведены к вещественному типу будет обрабатывать центральным камушком*)
  procedure IfInt;
  begin
   (*Если тип выражения еще целый, то либо просто поместить в регистр ЕАХ
   первый операнд, если он первый, либо провести операцию сложения, если операнд
   не первый, точнее сложить то, что было в ЕАХ, с константой, которую ядро
   передаст в обработчик*)
   if OperandNumber=1 then
     Result:=Result+MovEAXConst(i)
   else
     Result:=Result+ADDEAXConst(i);
  end;
  //****************************************
  (*Если же выражение уже приведено к типу вещественного, и работа ведется
  с сопроцессором, нам придется скармливать сопроцессору наше целое, и при условии
  что это не первый операнд проводить операцию сложения. *)
  procedure IfDouble;
  begin
   if OperandNumber=1 then Result:=FINIT;
   Result:=Result+FPushIConst(i);
   If OperandNumber>1 then  Result:=Result+FADD_ST0_ST(1);
   (*Естественно не забудем предупредить компилятор о том, что наше выражение
   уже стало вещественным.*)
   if TypeOfLastOperand in [_Int,_Unknown] then TypeOfLastOperand:=_Double;
  end;
  //****************************************

  (*Тут стандартный набор - приведение к целому, если тип выражения еще не
  установлен и в зависимости от типа выражения выполнение работы в том или
  ином направлении*) 
  begin Result:='';
   if TypeOfLastOperand=_Unknown then TypeOfLastOperand:=_Int;
   case TypeOfLastOperand of
    _Int:IfInt;
    _Double:IfDouble;
   end;
  end;

Далее идет работа с переменными:

 
  (*Если же операндом является переменная, действия будут почти аналогичные
  учитывая конечно, что в операции будет ссылка по адресу на ячейку переменной*)
  function TAlisaPlus.OnMember(m: TAlisaMem): String;
       //****************************************
       (*Если наша переменная целого типа, и сама операция еще не получила статус
       вещественного то провести операции аналогично операциям с константой, но
       по адресу в памяти, а не просто по числу, оформленному в опкоде как операнд.
       Или если по-русски - операндом опкода будет указатель на переменную*)
       procedure IfInt;
       begin
        if OperandNumber=1 then
         Result:=Result+MovEAXMem(m)
        else
         Result:=Result+ADDEAXMem(m);
       end;

       //****************************************
       (*Аналогично если переменная вещественных кровей, скормим ее значение
       сопроцессору, и проведем операцию, если она не первая. Единственный
       затык в типе переменной. Если она целая, то и помещать ее в сопроцессор
       нужно операцией помещения целого FPushIMem. Если же вещественная
       то соответственно другой операцией FPushDMem*)
       procedure IfDouble;
       begin
        if OperandNumber=1 then Result:=FINIT;
        if m.TypeMem=_Double then
         Result:=Result+FPushDMem(m)
        else
         Result:=Result+FPushIMem(m);
        (*После чего следуют стандартные шаги суммирования, если конечно это не
        первый операнд.*) 
        if OperandNumber>1 then  Result:=Result+FADD_ST0_ST(1);
        (*Аналогично нужно напомнить компилятору, что начиная отсюда, и заканчивая
        выражением теперь он имеет дело с вещественными*)
        if TypeOfLastOperand in [_Int,_Unknown] then TypeOfLastOperand:=_Double;
       end;
       //****************************************

  begin  Result:='';  (*Тут важно Результ инициализировать. Не забудьте этого*)
    if TypeOfLastOperand=_Unknown then TypeOfLastOperand:=m.TypeMem;
    case TypeOfLastOperand of
     _Int:IfInt;
     _Double:IfDouble;
    end;
  end;

И, наконец, обработчик оператора-подвыражения:

 
  (*В выражениях могут участвовать другие выражения - вложенные. Этот случай тоже
  нужно предусмотреть отдельно. Посмотрите, как реализована обработка вложенных
  выражений в шаблоне - вызов и компиляция подвыражения, с возвратом скомпилированной строки    
  опкодов. Здесь и в дальнейшем мы переопределим обработчик, не забыв вызвать метод родителя -  
  нам же все равно нужно как-то скомпилировать
  подвыражение. Однако его результат нужно обработать, и тело такого обработчика
  будет разным в зависимости от оператора.*)
  function TAlisaPlus.OnOperator(Oper: string): String;
  (*Для начала нам нужно где-то запомнить какой тип операции был до выполнения
    вложенного выражения.*)
   var TypeBeforeOperation:TypeOper;

      //****************************************
      (*После выполнения подвыражения вполне возможна ситуация, когда тип выражения
      не совпадает с типом, возвращенным подвыражением, и в этом случае придется
      типы приводить. Всего мы имеем на руках два результата: один тот, что был до
      выполнения подвыражения, он помещен в стек программы, а другой тот что вернуло
      подвыражение, и он находится в регистрах ЦП или сопроцессора
      Поэтому придется и здесь приводить к единому типу по приоритетности типов*)
      procedure IfOperInt;
      begin
         case TypeBeforeOperation of
         (*Если это целое, то результат выражения хранится в стеке программы в
         4 байтах на вершине стека (о том нужно сурово позаботится, чтоб подвыражение
         не оставляло после себя в стеке программы мусор). Если подвыражение целое
         просто просуммируем значение в ЕАХ и верхушку стека, не забыв из стека
         выкинуть уже не нужно после суммирования значение.*)
          _Int:    Result:=Result+ADD_EAX_ESP+AddESP(4);

         (*В случае если подвыражение вещественное, телодвижений будет поболее
         Учитывая что здесь обрабатывается случай, когда выражение до начала
         подвыражения имело целый тип результата, нам придется привести сохраненное
         в стеке программы 32-битное значение в вещественное, передав его
         сопроцессору, так же не забыв очистить стек программы. Будем
         учитывать еще один момент - Перед вычислением подвыражения мы результат
         сохранили в стеке. Не смотря на то что сопроцессор не был затронут, я
         предлагаю все равно перед вычислением подвыражения сохранять в стек
         результат, дабы не потерять. Хотя конечно при наличии оптимизатора
         тот бы понял, что стек трогать не надо, но у нас его нет, так что будем
         сохранять. А поскольку он лежит в стеке программы, его нужно вернуть обратно
         сопроцессору. И после уж передать ему же результат уже вычисленной части 
выражения         просуммировав их*) 
          _Double:begin
              Result:=Result+PushEAX+FILD_ESP+PopEAX+FLD_ESP+
                      AddESP(sizeof(TDoubleConst))+FADD_ST0_ST(1);
            (*Естественно тип выражения будет приведен к вещественному, ибо у него
            приоритет выше.*)
            TypeOfLastOperand:=_Double;
          end;
         end;
      end;
      //****************************************
      (*Если до вычисления подвыражения тип был вещественный, то наша задача
      передать сохраненное в стеке программы в сопроцессор. Естественно
      не забыв просуммировать инструкцией FADD ST0,ST1*)
      procedure IfOperDouble;
      begin
         case TypeBeforeOperation of
          (*Если это целое - выбрать его, не забыв почистить стек*)
          _Int:    Result:=Result+FILD_ESP+FADD_ST0_ST(1)+AddESP(4);
          (*Если вещественное кроме этого еще и перевести тип выражения к
          вещественному*)
          _Double: begin
            Result:=Result+FLD_ESP+FADD_ST0_ST(1)+AddESP(sizeof(TDoubleConst));
            TypeOfLastOperand:=_Double;
          end;
         end;
      end;
      //****************************************

  begin  Result:='';
  (*Итак, перед компиляцией подвыражения запоминаем тип выражения*)
   TypeBeforeOperation:=TypeOfLastOperand;
  (*В зависимости от типа выражения либо сохраняем в стек программы
  из ЕАХ центрального процессора, либо из ST0 сопроцессора*) 
   if TypeBeforeOperation=_Double Then     Result:=Result+FSTP_ESP;
   if TypeBeforeOperation=_Int    Then     Result:=Result+PushEAX;
  (*После чего не побоимся скомпилировать подвыражение*)
   Result:=Result+ inherited OnOperator(Oper);
  (*И в зависимости от типа подвыражения выполнить описанные выше функции
  по сочетанию результатов и приведению к единому типу*)
   case TypeOfLastOperand of
     _Int:IfOperInt;            // Если оператор выдал целое
    _Double:IfOperDouble;      // Если оператор выдал вещественное
   end;

  end;

Надеюсь, я ничего не забыл описать… Работу ядра и вызовы объектов-обработчиков, пример объекта обработчика для оператора суммирования, и модуль сборник функций с опкодами, чтоб не писать в телах обработчиков много одинакового.

Если все правильно сделано, теперь можно попробовать построить «выраженьеце суммы», и посмотреть в отладчике как оно себя поведет. Давайте для примера возьмем что-нибудь типа 2+2+(28.3+45)+5. Понятное дело, что скобочки тут не имеют особого смысла, но для проверки обработчика оператора пригодятся как тестовые. В ЛИСП-синтаксисе это выражение будет выглядеть как (+ 2 2 (+ 28,3 45) 5). Так и напишем в редакторе программы (см. рисунок 6):

Вы же не забыли прикрутить к вышеприведенному коду красивый редактор? Или забыли? Если забыли – прикрутите. А если прикрутили – компилируем. На выходе в папке с компилятором должен появиться файл File.exe. Откроем его в «Оле» (Olly Debuger). О! пока не забыл. Еще раз условимся – разделителем в вещественном числе будет точка и только точка.

Итак, посмотрим на кишки скомпилированного файла (см. рисунок 7):

Что тут происходит?

в регистр ЦП помещается первый операнд. Компилятор распознал его как целое, и решил что аферку с ним стоит провернуть в ЦП без сопроцессора;

к помещенному в регистр значению добавляется константа. Она тоже целая, компилятор ее скармливает ЦП без тени смущения. Это первая часть выражения 2+2;

что же происходит дальше? Компилятор встречает подвыражение. Он знает что, встретив подвыражение, нужно запомнить в стеке программы, на который указывает регистр ESP, уже просчитанный результат. Поскольку до этого момента он имел дело с целыми, вполне резонно, что в стек попадает командой PUSH EAX живность 32-битного регистра, равное кстати 4-м. Пройдите пошаговку (F8) – убедитесь сами;

встретились скобки, парсер взял его как строку, а ядро определило что это подвыражение — теперь компилятор отложил выражение, и занялся вовсю встреченным подвыражением. Первый операнд его вещественное число равное 28.3. ЦП с вещественными работать не умеет, посему компилятор принял решение задействовать тяжелую артиллерию в лице сопроцессора. Для этого число 28.3 помещается в стек программы тремя операторами PUSH, естественно начиная со старшего двойного слова (слова, а не байта, потому что стек программы то у нас 32 битный), после чего команда FLD TBYTE PTR SS:[ESP] выбирает из стека эти м.м.м. 10 байт, два байта не трогает походу, после чего стек необходимо почистить от мусора, чем и занимается команда ADD ESP, 0C, передвигая указатель стека на 12 байт вниз. Можно было выполнить для очистки реестра три раза POP EAX, но это некрасиво смотрится, и мало ли вдруг еще значение в ЕАХ необходимо;

таким образом, сопроцессор получил свой первый операнд, поскольку он первый, компилятор, зная это, не прикрутил еще операцию сложения;

теперь нужно поместить второй операнд подвыражения за пазуху сопроцессора, поскольку уже понятно, что дальнейшая работа будет в с вещественными. Этим займется набор команд PUSH 2D и FILD DWORD PTR SS:[ESP]. Где 2D собственно число 45. Не забыв почистить стек программы, компилятор дописал ADD ESP, 4, чтоб убрать из стека этот операнд, уже не нужный там;

сопроцессор получил два операнда подвыражения. Теперь он может провести с ними операцию, задуманную нами. Это операция сложения, за которую отвечает оператор FADD ST, ST(1). Результат поместится в вершину стека сопроцессора в ST0;

подвыражение закончено. Компилятор присвоил ему статус вещественного и больше не хочет в рамках этого выражения трогать центральный процессор. Теперь работа пойдет только с сопроцессором, поэтому ему скармливается результат материнского выражения, поскольку он целочисленный, за это ответит операция FILD DWORD PTR SS:[ESP]. Помните, мы до вычисления подвыражения благоразумно сохранили результат в стек. Теперь он пригодится для продолжения вычисления, ибо третий операнд – результат подвыражения вычислен и готов к соитию с суммой первых двух;

результат подвыражения любезно подвинется в ST1, дабы уступить место числу 4 (это 2+2 дало) из стека программы, после чего команда FADD ST, ST(1) проведет сложение операндов, и результат поместит в ST0. Не будем забывать, что ни одна из этих команд не чистит стек от уже ненужного значения, посему компилятор предусмотрительно (под нашим чутким руководством) добавил операцию смещения указателя стека вниз на одну позицию (на 4 байта) ADD ESP, 4;

стек очищен, но у нас есть еще один, последний операнд. Он целочисленный, равный 5-ти. Его так же передаем сопроцессору через стек программы, так же сопроцессор добавит его к общей сумме командой FADD ST, ST(1), а перед ней компилятор любезно вставит команду очистки стека программы от этой пятерки.

И в результате мы получаем на верхушке сопроцессора результат (см. рисунок 8):

Проверим на калькуляторе: 2+2+28,3+45+5 = 82,3. Что есть неоспоримым доказательством работоспособности, и главное правильной работоспособности программы, а значит и самого компилятора.

Welcome home

Уф… Как вы думаете, сможет ли такая работенка потянуть на диплом? Думаю да. Если продолжать «наворачивать» компилятор операциями и прочими вкусностями, комиссии будет интересно поиграться с новым языком, попробовать его в деле. На фоне зачастую банальных и скучных дипломок, сто раз передранных у предыдущего поколения студентов что-то новое и свежее однозначно станет предметом всеобщего внимания. Я, конечно, не пробовал нагрузить компилятор сложными выражениями, предоставляю эту работенку вам, но, думаю, основные принципы компиляции соблюдены в достаточном количестве условностей.

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

А на сегодня, Auf Wiedersehen.

Вы можете ответить или разместить запись на вашем сайте.

Ответить

Powered by Procoder