Исследование «промежуточного» кода на примере GP-кода языка NATURAL - Часть 3: Секция кода — Архив WASM.RU
Разбор секции кода и прост, и сложен. Прост, потому что молитвами разработчиков языка там нет ничего, кроме номеров строк исходника, кодов операций (команд) и операндов. И если с номерами строк все понятно, то с командами и операндами все гораздо запутаннее.
Какие сложности с командами? Во-первых, их несколько сотен; во-вторых, у каждой команды (точнее, группы команд) свой перечень параметров (аргументов), причем параметры могут быть представлены как в префиксном виде, так и постфиксном; в-третьих, команды могут быть размером и в байт, и в два байта.
Какие сложности с операндами? Во-первых, в их многоликости. Дело в том, что в качестве параметров (аргументов) для команд могут быть как операнды, явно присутствующие в Natural-выражениях (именно те, о которых мы говорили в предыдущей части статьи «Часть 2. Операнды в GP-коде», и чтобы не путаться в дальнейшем, условимся называть их nat-операндами), так и всевозможные флаги и просто константы, которых нет в Natural-выражениях, но которые появляются на этапе компиляции (например, «дальность» прыжка операторов семейства JMP, которые обязательно появятся при компиляции любого условного выражения в Natural-программах). Во-вторых, тип постфиксного операнда можно понять только в контексте последней команды – то ли это nat-операнд, то ли это значение флага, то ли константа.
И что со всем этим безобразием делать? Да ничего особенного! Просто набраться терпения – ведь как-то же код выполняется интерпретатором. Хотите знать как?
Смотрите, размер элемента исследуемой секции нам показывает значение поля Align в Таблице секций, соответствующее секции кода – всегда два байта. Перебираем секцию кода по элементам, определяя его «смысловую нагрузку» по его же значению.
Код (Text):
Если 0000 « значение элемента ‘ 8000, то перед нами операнд (1) Если 8000 ‘= значение элемента ‘= A8FF, то перед нами номер строки (2) Если A8FF ‘ значение элемента ‘= F6FF, то перед нами byte-code (3) Если F6FF ‘ значение элемента ‘= FFFF, то перед нами word-code (4)Вот, собственно, и все. Напомню, что под операндом понимается и nat-операнд, и флаг, и константа, и чтобы верно определить тип операнда, необходимо:
- а. определить, «чей» это операнд, т.е. определить команду-владельца этого операнда;
- по прототипу команды определить тип операнда.
Чтобы выйти из шока, вызванного прочтением этих пространных объяснений, скорее запутывающих, нежели объясняющих, разберем два примера, и, думаю, вопросов с пониманием построения GP-кода не должно остаться.
В качестве первого примера вполне подойдет задача про программистов-энтузиастов, которая предлагалась в предыдущей части статьи (Часть 2. Операнды в GP-коде (окончание)).
Усложним условие. Попробуем восстановить исходный код программы полностью, а не только область параметров.
Вот GP-код, который нам надлежит превратить в нечто потребное:
Воспользуемся результатами, которые были получены нами при решении предыдущей задачи. Нам понадобится Таблица Секций:
И Таблица Операндов (напомню, имена параметров - переменных придумали сами):
А теперь непосредственно решение. По Таблице Секций определяем, что Секция Кода начинается по смещению 0x88 от начала файла и ее размер составляет 0x14 байт. Вот она:
Разбираем по двухбайтовым элементам, руководствуясь приведенными выше диапазонами значений:
С пониманием преобразования номеров строк проблем быть не должно – мы это подробно обсуждали в первой части статьи.
А вот с командами мы знакомы шапочно. Поэтому необходимы небольшие комментарии.
При компиляции, исходный код Natural-программы превращается в GP-модуль, секция кода которого содержит ассемблероподобные инструкции. Это важно! Выражения Natural-программы не просто кодируются, а транслируются в выражения языка более низкого уровня (вспомните, в первой части статьи говорилось о том, что GP – это сокращенное Generated Program – сгенерированная программа). Причем, у этого языка свой самостоятельный синтаксис, более близкий к синтаксису ассемблера, нежели к синтаксису Natural. Команды этого языка имеют свой опкод, свою мнемонику, свою логику (см. Таб. 3.3.).
Но вернемся с небес на землю, точнее, к разбору Секции кода, а еще точнее – к элементу под номером пять (См. Таб.3.2.). Судя по значению элемента (0xFFF0), это команда. По Таб.3.3. определяем, что это ничто иное, как команда ADD. Читаем комментарий: «Прибавить arg3 к arg2 и поместить в arg1». Все аргументы – nat-операнды, точнее ссылки на nat-операнды. В нашем случае arg1 – это Временная переменная (элемент №4), arg2 – это parm2 (элемент №3), arg3 – это parm1 (элемент №2) (см. Таб.3.1). Таким образом, получаем:
С этим, вроде бы все понятно. Идем дальше. На очереди элемент под номером восемь. (См. Таб.3.2.). Судя по значению элемента (0xFFEC), это опкод. По Таб.3.3. определяем, что это ничто иное, как команда MULT. Читаем комментарий: «Умножить arg3 на arg2 и поместить в arg1». В нашем случае arg1 – это parm4 (элемент №7), arg2 – это parm3 (элемент №6), arg3 – это ... Если честно, не очень понятно, что делать с arg3, потому что элемент №5 – это разобранная нами ранее команда ADD. Правда, непонятность исчезает, если представить, что операнды перед использованием командами, помещаются в некий стек, и отсчет операндов надо вести не от номера элемента команды, а от вершины стека. (Эта гипотеза может показаться притянутой за уши, но, тем не менее, она работает, и не только в этом примере). Тогда arg3 – это Временная переменная! Таким образом, получаем:Код (Text):
Временная переменная = parm1 + parm2 Было бы неплохо избавиться от Временной переменной, т.к. в исходном коде она не объявлялась (иначе она была бы нормальной локальной переменной). Значит, изначально арифметическое выражение выглядело так:Код (Text):
parm4 = Временная переменная * parm3 Код (Text):
parm4 = (parm1 + parm2) * parm3Т.е. восстановленный исходный код программы должен выглядеть так:
Код (Text):
define data parameter 1 parm1 (n10.2) 1 parm2 (f4) by value 1 parm3 (I4) by value result 1 parm4 (n20.7) end-define parm4 := (parm1 + parm2) * parm3 ENDИ это работает! Мало того, сравнение содержимого старого файла и нового (полученного после компиляции восстановленного исходного кода) показывает, что отличаются только поле TimeStamp заголовков файлов (но мы договорились, что не будем обращать на это поле внимание) и … номера строк инструкций в Секции кода.
А дело вот в чем. На объявление 4 параметров уходит 6 строк исходного коде (4 – на само объявление параметров, 2 – на открытие и закрытие блока объявлений). Таким образом, первое программное выражение не может находиться в строке, меньшей, чем 0070 (Natural нумерует строки автоматически с шагом в 10). Тем не менее, в разбираемой нами программе выражение находится в строке 0050. Наиболее вероятный вариант – это использование параметрической области данных, в которой и объявлены параметры. Но это уже нюансы для тонких ценителей Natural.
В качестве второго примера разберем такой GP-код.
На всем, что неоднократно разжевано, не останавливаемся.
Секция Кода:
Элементы кода секций:
Логика разбора совершенно такая же, как и в предыдущем примере: перебираем последовательно все элементы секции, пристально вглядываясь в их значение. Если это команда, то обращаемся к Таб.3.3. По прототипу команды определяем тип операнда (операнды могут располагаться и до команды и после!). Таб.3.4 поможет нам не запутаться в ссылках на nat-операнды. В очередной раз теоретически подковались, а теперь - в путь!
С идентификацией первого элемента проблем быть не должно – это номер строки. Второй и третий элементы – точно операнды, но пока непонятно какие. Четвертый элемент – команда. По Таб.3.3 видим, что это старый знакомый MOVE, а раз так, то второй и третий элементы – ссылки на nat-операнды. Отсюда следует, что перед нами - простое присвоение значения 2 переменной lvar1 (См. Таб.3.4).
Ok, с этим разобрались, идем дальше.
А дальше перед нами команда FOR3_R, которая ничего не делает, а просто предупреждает, что настало время морально приготовиться к разбору цикла FOR. Эта команда имеет два постфиксных параметра. Назначение первого (ближайшего к команде) – указать, где находится безусловный переход на конец цикла, назначение второго – указать на начало программного кода, который должен выполняться в цикле (тело цикла) . В качестве указателей используется относительное смещение.
Поупражняемся в арифметике. FOR3_R - элемент №5. Значение его первого аргумента 18 (элемент № 6), второго аргумента - 2(элемент № 7). Отыскиваем конец цикла FOR по первому аргументу.
6 (номер элемента) + 18 (значение первого аргумента) = 24
Смотрим, 22 элемент – ENDLOOP, что вполне подходит на роль идентификатора конца цикла.
Отыскиваем тело цикла по второму аргументу:
7 (номер элемента) + 2 (значение второго аргумента) = 9
Смотрим, 9 элемент – номер строки, что тоже вполне подходит на роль начала тела цикла (отметим для себя, что номер строки совпадает с номером строки в первом элементе).
Будем считать, что разбор пятого - седьмого элементов закончен.
Восьмой элемент – байтовая команда JMP, соответствующая безусловному «прыжку» на 4 элемента в сторону увеличения номера элемента, т.е. на 12 элемент.
Девятый элемент – номер строки, ничего нового.
Десятый элемент – операнд, пока непонятно какой.
Одиннадцатый элемент – судя по значению, это байтовая команда, а судя по Таб.3.2, это команда INC. Прототип использования INC говорит о том, что десятый элемент – ссылка nat-операнд, а это никто иной, как старый знакомец lvar1. Таким образом, последовательность из десятого и одиннадцатого элементов увеличивает значение lvar1 на 3 (3 – постфиксный байтовый параметр) .
Двенадцатый и тринадцатый элементы – операнды, пока непонятно какие.
Четырнадцатый элемент – команда JGT_R с двумя префиксными параметрами – ссылками на nat-операнды и одним постфиксным параметром – константой. Отсюда, двенадцатый элемент – ссылка опять-таки на lvar1, тринадцатый элемент – ссылка на nat-константу, равную 10. Таким образом, последовательность с двенадцатого элемента по четырнадцатый, соответствует «прыжку» на 9 элементов в сторону увеличения номера элемента (т.е. на элемент 24 - ENDLOOP), если lvar1 > 10.
Шестнадцатый элемент – номер строки, не останавливаемся.
Последовательность с семнадцатого элемента по двадцатый соответствует вычитанию 1 из lvar1 (если вы внимательно читали предыдущие рассуждения, с интерпретацией этой последовательности вопросов возникнуть не должно).
Двадцать первый элемент – номер строки.
Последовательность с двадцать второго элемента по двадцать третий – безусловный переход на 14 элементов в сторону уменьшения номеров элементов.
Последовательность с двадцать четвертого элемента по двадцать пятый – идентификатор завершения цикла. Как и команда FOR3_R, ENDLOOP ничего не делает, а просто сигнализирует, что цикл завершается (настоящее «зацикливание» осуществляется предыдущим переходом).
Двадцать шестой элемент – NOP. Ну, NOP, он и в Африке NOP.
Двадцать седьмой элемент – номер строки.
Последовательность с двадцать восьмого элемента по тридцатый соответствует присвоению переменной lvar2 переменной lvar1.
Тридцать первый элемент – номер строки.
Тридцать второй элемент – старый знакомый END.
По-крупному, картина ясна – есть цикл, создаваемый nat-командой FOR, внутри этого цикла что-то происходит, по выходу из цикла.
Осталось разобраться с параметрами цикла и телом цикла, что совсем несложно.
Начнем с конца цикла, точнее, с безусловного перехода с отрицательным параметром (элемент 22). Как мы уже видели, он направляет на элемент 9, соответствующий номеру строки (причем, такому же, что и номер строки FOR3_R). Далее идет увеличение переменной lvar1 на 3. Причем, такое увеличение происходит на каждой итерации за исключением самой первой! Уж очень это похоже на параметр STEP nat-команды FOR! И еще сильнее убеждает в нашей правоте последующий условный переход (элемент 14), указывающий за цикл (мы уже отмечали, что элемент 24, на который указывает условный переход, соответствует команде ENDLOOP, которая является декларативной). Т.е. этот условный переход – условие выхода из цикла. Если наши рассуждения верны, то интерпретация четвертого элемента (команда MOVE) проста до безобразия – это инициализация переменной, управляющей выполнением цикла.
Тело цикла состоит из одной-единственной команды SUB (элемент 20). Других команд между условием на выход из цикла и «зацикливающим» переходом нет.
Суммируя сведения, полученные запредельной дедукцией, и переводя их на язык NATURAL, уверенно пишем:
Код (Text):
0010 define data local 0020 1 lvar1(n2) 0030 1 lvar2(n2) 0040 end-define 0050 for lvar1 from 2 to 10 step 3 0060 lvar1 := lvar1 - 1 0070 end-for 0080 move lvar1 to lvar2 0090 endВот и все!
Конечно, все эти танцы с бубном могут показаться инопланетянским лепетом. Однако при небольшом навыке все выглядит не так уж и страшно. По крайней мере, не страшнее, чем дизассемблерный листинг.
Проницательный читатель должен бы ухмыльнуться – мол, спасибо за пояснение, что, например, 0xFFF0 – это ничто иное, как команда ADD, но было бы неплохо пояснить, откуда это я взял!
Действительно, нет никакой официальной документации по GP-коду. Вся информация добывалась посредством проведения бесчисленного множества экспериментов и исследования natparse.dll. Дело в том, что natparse.dll – это и компилятор, и интерпретатор в одном флаконе. Мало того, в этой же библиотеке находится функция, создающая отчеты компиляции, вполне понятные и заметно экономящие время кодокопателя. Про эти отчеты я уже как-то упоминал - речь идет о параметре копиляции Dump of Natural GP. У этих отчетов есть один недостаток – они формируются в процессе компиляции, т.е. при наличии исходника. Правда, если эту библиотеку хорошенько «попросить», то она способна выдать набор стандартных отчетов, достаточных как для исследования р-кода, так и для восстановления исходного кода в ручном режиме. Но это уже совсем другая история. © Konstantin
Исследование «промежуточного» кода на примере GP-кода языка NATURAL - Часть 3: Секция кода
Дата публикации 3 июл 2007