Исследование «промежуточного» кода на примере GP-кода языка NATURAL - Часть 2: Операнды — Архив WASM.RU

Все статьи

Исследование «промежуточного» кода на примере GP-кода языка NATURAL - Часть 2: Операнды — Архив WASM.RU

Как известно, операндами называют то, что участвует в операциях. Так в операции сложения операндами будут слагаемые, а в операции умножения - сомножители.

Применительно к GP-коду Natural, термин "операнд" объединяет такие элементы программного кода как константы, локальные переменные, глобальные переменные, объекты и т.д.

Согласитесь, разобравшись с тем, как и где описываются операнды, нам будет вполне по силам делать "косметический ремонт" программ (изменить формат, размер переменной, атрибут поля формы и т.д.). Также нам станут подвластными и параметры вызываемых подпрограмм. Это самое очевидное, но мало ли, что можно еще придумать!

Полагаю, самым разумным будет начать наше исследование с констант, т.к. во-первых, константы - они в Natural константы, а во-вторых, их легко разглядеть в hex-редакторе.

И еще небольшое отступление. Natural предоставляет возможность использовать два режима написания программ - Report mode (режим отчета, обычно для написания программ "на скорую руку"), Structure mode (структурный режим). Основное отличие (не вдаваясь в особые подробности) заключается в том, что в режиме отчета можно объявлять переменные в любом месте программы по мере необходимости, а в структурном режиме - только в начале программы в блоке, выделенном ключевыми словами Define Data и End-Define. Есть различия в синтаксисе, но нам они сейчас не интересны. Для GP-кода безразлично, в каком режиме написан исходный текст программы. Для большей наглядности далее в статье исходные тексты программ будут представлены в структурном режиме.

Константы

Предлагаю решительно отказаться от соблазна вывести каноническое "Hello world!" на экран и исследовать получившийся код! Основная причина заключается в том, что вывод на экран - это не самая простая операция (справедливо не только для Natural). Если же есть желание разбираться с буфером формата и другими важными элементами программы, не имея для этого достаточной базы - что ж, дело хозяйское. Вы навсегда останетесь в моей памяти…

Предлагаю пойти другим путем. Рассмотрим константы в двух ипостасях - объявление константы, и присвоение константы. В первом случае мы объявим переменную константой (т.е. такой переменной, значение которой далее программе нельзя будет менять), во втором - присвоение некой переменной (конечно, объявленной не как константа) некоего значения.

После компиляции получим следующий GP-код

Из заголовка нам пригодится только значение поля Number of TABDIR entries, которое находится по смещению 0x18.

Ого! А там-то 0x0B!!! Т.е. количество секций 11, вместо полюбившихся нам 6! Но не будем спешить, одиннадцать, так одиннадцать.

На очереди таблица секций.

Напомню, запись в таблице секций состоит из двух полей по 4 байта каждая - размер секции и выравнивание. Начало таблицы секций расположено всегда по смещению 0x38, а размер таблицы равен произведению длины записи в таблице (4 + 4 = 8) и количества записей в таблице (поле Number of TABDIR entries заголовка, т.е. 0x0B). Итого 0x58. Для удобства добавим расчетные столбцы с границами секций

С первыми шестью секциями мы разбирались в предыдущей части статьи. Напомню:

  1. Заголовок (фиксированная часть и таблица секций).
  2. Секция кода.
  3. Секция №3 (пока неопознанная)
  4. Секция констант
  5. Секция №5 (пока неопознанная)
  6. Секция переменных. Точнее, размер памяти, который необходимо зарезервировать интерпретатору для размещения переменных во время исполнении программы. Физически в файле не присутствует, потому границы секции не определены.

Секции 7,8,9,10 имеют нулевой размер, говоря проще, их в GP-коде нет, и появилась шестнадцатибайтовая одиннадцатая секция.

Секция №3

Очень похоже, что перед нами очередная таблица, а значит, прежде чем гадать, зачем же она понадобилась, потратим немного времени на то, чтобы разбить ее по записям. Сразу оговорюсь, что понятие "запись" здесь условно. Нам не важно на этом этапе, каким образом в действительности происходит запись или чтение этой секции, нам важно понять ее назначение, а потому мы вольны в своих действиях. Т.е. задача - разбить последовательность данных на составляющие, осязаемые, понятные нам, на составляющие, которые мы смогли бы интерпретировать.

Основной критерий - повторяемость.

Для начала, предположим, что длина записи в таблице равна значению поля Align для 3 секции в таблице секций (см. Таб.2.1 Вариант 1). Беглого взгляда достаточно, чтобы увидеть некую периодичность, или повторяемость.

Увеличиваем длину записи до 8 (см. Таб.2.1 Вариант 2). Уже что-то выстраивается! Однако если с первой половиной записей все выглядит довольно оптимистично, то вторая половина явно сбилась. Попробуем добиться большей повторяемости (см. Таб.2.1 Вариант 3)

Визуально все выглядит вполне пристойно. Попробуем интерпретировать получившееся. Обратите внимание на последние байты записей - 0x0A, 0x0D, 0x13 - это ни что иное, как размеры объявленных нами переменных! Что такое 02 не понятно, а вот 08 вполне может быть размером наших строковых констант (они действительно по 8 байт). Единственное, что настораживает - вообще-то строковых констант в программе было 4, а здесь всего 3. Но смотрим дальше. Если наши предположения верны и в последних байтах записей помещены размеры переменных (точнее, операндов), то надо бы отыскать формат переменных. Все наши объявленные переменные являются символьными переменными, константы - тоже символьные, и лучшим кандидатом на хранение формата является шестой байт записи (во всех записях, кроме первой имеет одинаковое значение 0x0A).

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

Пятый байт любопытен двумя свойствами:

  • тем, что для записей, в которых последние (восьмые) байты 0x0A, 0x0D, 0x13, т.е. объявленные нами явно переменные, имеет значение 05, в остальных же случаях (уж не для идентификации ли констант!) имеет значение 03;
  • явно влияет на поведение первого байта.

Рассмотрим второе свойство подробнее.

Наблюдаем, как изменяется первый байт только для записей с пятым байтом, равным 03 (т.е. записи 1,2,4,7).

Первый байт увеличивается точно на размер предыдущей. Тоже самое происходит и для записей с пятым байтом, равным 05 (записи 3,5,6).

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

По-крупному с третьей секцией понятно - это секция, содержащая описание операндов. Остался вопрос с пропавшей константой, к которому обязательно вернемся чуть позже.

Секция №4 (секция констант)

Содержимое секции представлено на рис 2.4.

Здесь мы легко обнаруживаем строковые константы, которые используются в нашей программе, а также последовательность 01,00 в начале секции.

Возникает два вопроса:

  1. что это за последовательность?
  2. куда пропала четвертая константа?
  3. как с этим безобразием работать?

Относительно первого вопроса, ответ простой - не знаю! Да и особо не интересовался. Известно лишь, что эта последовательность встречается и в "минимальной" программе.

Относительно второго вопроса - перед нами зачатки оптимизации! Вернитесь к исходному коду программы - там строковая константа "String01" используется 2 раза. Компилятор посчитал расточительным хранить две идентичных константы, в чем, безусловно, прав. Жаль, на этом оптимизирующие свойства компилятора практически заканчиваются. С другой стороны, нам, исследователям, это только на руку.

Относительно третьего вопроса - эта секция напрямую связана с секцией №3. Посудите сами - записи с описанием констант (т.е. записи 1,2,4,7 в Таб. 2.2, имеющие значение 03 в пятом байте) в точности соответствуют содержимому секции констант.

Секция №11

Содержимое секции представлено на Рис 2.5.

Здесь не все очевидно, но достаточно просто выясняется, поставив несколько экспериментов. Я не буду морочить вам голову этими экспериментами - они того не стоят (о том, что эксперименты надо планировать, а полученные результаты журнализировать, надеюсь, вы помните!).

Логика рассуждений для прогноза результатов может быть такой. Пока не было изысков в виде "const" и "init" при объявлении переменных, одиннадцатая секция не проявлялась. Опять же именно для этих переменных выросли четырехбайтовые аппендиксы. Поэтому вполне возможно, что перед нами секция атрибутов переменных - операндов (проще говоря, секция каких-то дополнительных свойств операндов). И если вы предположите, что пресловутый аппендикс в таблице операндов не что иное, как ссылка на соответствующую запись в секции атрибутов, то будете абсолютно правы! Вы также будете правы, если предположите, что запись помимо признака константы или инициализированной переменной содержит и ссылку на то, чем переменная инициализирована.

Эксперименты покажут, что (см. Рис 2.5) 0xA0 - признак константы, 0x80 - признак инициализированной переменной (стоит отметить, что это довольно характерные значения для всякого рода флагов), 02 - ссылка на последовательность "Srting01", а 07 - ссылка на последовательность "Srting02".

Буквально несколько слов об использовании ссылок в GP-коде.

В GP-коде для связи секций-таблиц не используется номер записи в таблице в качестве ссылки, т.к. длина записи в таблице не постоянна. Не используется также и относительный адрес на начало записи, а используется некий симбиоз - индекс секций, равный отношению относительного адреса начала записи к значению Align соответствующей секции. Оригинальное название индекса для каждой секции свое.

Чтобы картина описания констант в GP-коде стала более четкой (мы прошлись по ней лишь крупными мазками), пройдемся по предложенному примеру еще раз, и начнем с таблицы операндов.

Суммируя полученную информацию

Таблица операндов предназначена для хранения информации об операндах, имеет переменную длину записи (постоянная часть записи плюс, если есть, переменная часть записи).

Постоянная часть записи содержит следующую информацию:

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

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

С таблицей операндов, думаю, достаточно, переходим к таблице атрибутов.

Таблица атрибутов (секция №11) имеет тоже переменную длину. Постоянная часть содержит следующую информацию:

  • флаг (на самом деле, конечно, флаги, но давайте договоримся, что пока этого не знаем);
  • режим печати (о! а вот это отложим до лучших времен).

Если флаг имеет значение 0xA0 (константа) или 0x80 (инициализация переменной), то следующие 4 байта (переменная часть) - это ссылка значение, которое присвоено константе (в первом случае), либо которым инициализирована переменная (во втором случае). Ссылка на таблицу операндов!

Рассмотрим в качестве примера запись №5 в таблице операндов (Таб.2.4). Последовательность 0A 00 00 00 05 0A 04 0D 02 00 00 00 может быть интерпретирована следующим образом. Постоянная часть (8 байт) содержит информацию о локальной (0x05, 5-ый байт) символьной (0x0A, 6-ой байт) переменной размером 13 байт (0x0D, 8-ой байт), находящейся по смещению 0x0A (0x0000000A, 1-4-ые байты) в области локальных переменных. Это описание операнда дополняется информацией об атрибутах операнда (0x04, 7-ой байт) - переменной частью записи. Значение флага 0x04 указывает нам, что дополнительная информация находится в секции атрибутов (секция №11), а значение 02 (1-2-ые байты, переменная часть записи) указывает на вторую запись в таблице атрибутов. Вот как это считается. Как мы уже знаем (Рис. 2.3), значение Align для секции №11 (секции атрибутов) равно 4, а произведение 2 (1-2-ые байты, переменная часть записи) и 4 дает 8, т.е. запись, содержащая дополнительную информацию об атрибутах операнда, находится по смещению 8 от начала секции атрибутов, а это и есть вторая запись.

Возвратимся к нашему примеру и попробуем интерпретировать последовательность 00 80 00 00 07 00 00 00 (Таб.2.4), соответствующую второй записи в таблице атрибутов.

Постоянная часть (4 байта) показывает, что перед нами переменная, инициализированная (0x80, 2-ой байт) … а вот каким значением сейчас рассчитаем.

Значение Align (Рис. 2.3) для секции №3 (таблицы операндов) равно 4, а произведение 7 (1-2 байты, переменная часть записи) и 4 дает 28 (0x1C), т.е. переменная инициализирована операндом (!), информация о котором находится по смещению 28 (0x1C) от начала таблицы операндов. А это ни что иное, как 4 запись в этой таблице. Попробуем разобрать последовательность 0A 00 00 00 03 0A 00 08 , соответствующую смещению 28 (0x1C). Постоянная часть (8 байт) содержит информацию о константной (0x03, 5 байт) символьной (0x0A, 6 байт) последовательности размером 8 байт (0x08, 8 байт), находящейся по смещению 0x0A (0x0000000A, 1-4 байты) в области констант. Под область констант отведена секция №4. Отсчитываем 0x0A от начала области констант и получаем восьмибайтную последовательность "String02".

Что ж, вполне похоже на правду.

Более подробную информацию о структурах секций - таблиц и возможные значения некоторых полей можно найти в Приложении 2.

Секция кода

Что ж, настало время взглянуть в "святая святых" - в секцию кода!

Это вторая секция, и думаю, не составит большого труда по Рис 2.3 определить границы секции, а по Рис 2.2 взглянуть на содержимое.

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

Два первых байта записей - номера строк (об этом мы говорили в предыдущей части статьи), два последних - идентификаторы команд (об этом не трудно догадаться, тем более что D0 FF в третьей записи мы научились распознавать опять-таки в предыдущей части статьи). Очевидно, что между номером строки и FE FF (командой MOVE) в первых двух записях должны каким-то боком казаться переменные (что и куда пересылается). Этим "боком" оказывают те самые ссылки на операнды, которые нам уже встречались при разборе констант.

Таким образом, оказывается (можно воспользоваться колонкой Ссылка Таб.2.4) , что

  • 0x0E - ссылка на последовательность "String03"
  • 0x09 - ссылка на переменную (в тексте программы initVar02)
  • 0x02 - ссылка на последовательность "String01"
  • 0x0C - ссылка на переменную (в тексте программы someVar03)

Что ж, можно считать, что разбор секции завершен, и полученный результат вполне похож на правду.

С чем вас и поздравляю!

Вместо заключения

В этой части статьи мы рассмотрели принцип организации таблицы операндов в GP-коде на примере констант, благодаря чему можем достаточно уверенно восстановить область локальных переменных. Правда, пока без особых изысков в виде переопределений (redefine), массивов и т.д. Увы, имена переменных нам не доступны, если не выставлен специальный флажок в настройках компилятора, который отвечает за создание Symbol Table при компиляции. Но это не помешает нам восстановить не только логику программы, но и вполне работоспособный исходный текст программы. Впрочем, я слишком тороплюсь…

Мы прикоснулись также к великому таинству секции кода. Благодаря тому, что мы разобрались с таблицей операндов, это прикосновение не ударило по нашей нервной системе. Все оказалось дольно простым. Для тех, кто исследует что-то свое - настоятельно рекомендую начинать исследование секций программного кода лишь после того, как вам станет понятно, как идентифицируются переменные в коде (а в этом вам помогут константы!).

Уверен, что такая организация и секций и межсекционного взаимодействия вызывет много вопросов. Но давайте не будем спешить. Посмотрим, что будет дальше, возможно часть вопросов снимется сама собой!

Приложение 2

1. Секции в GP-коде2

2 Приведенное соответствие верно лишь для исполняемых программных модулей. Для неисполняемых соответствие будет немного другим.

2. Структура постоянной части записи Таблицы операндов (Operand Table)

3. Форматы операнда

4. Флаги операнда

2002-2013 (c) wasm.ru