Digs - Персональная территория

Авторский проект Артема Глазкова
? 
        Версия для печати (цвет)  

Идеи (разгрузка)
» Язык управления игрой
» Структуризация бинарных файлов

Использование материалов
Заметка #29
02 декабря 2007

Структуризация бинарных файлов


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

    Основное
    Отображение структур описывается на специальном Си-подобном языке. Отличие от приведенных выше программ состоит в отображении основного дампа файла не в шестнадцатеричном виде, а в виде, заданным в скрипте.
    Файл может быть большим, либо в нем могут быть записи, которые редко представляют большой интерес, поэтому в языке должна быть такая вещь как детализация (привет case-технологиям). Это значит, что в начальный момент некая единица файла может быть отображена, например, в сокращенном виде, а далее, если нужно, ее можно рассмотреть более подробно. Т.е. в принципе, изначальное отображение может быть в шестнадцатеричном виде с разбивкой по записям либо другим логическим структурам. Движение курсора должно подсвечивать фоном тот логический блок, внутри которого он находится и который может быть детализирован. Здесь два варианта: либо небольшое окошко должно располагаться сбоку от основной области просмотра, либо при нажатии какой-то кнопки должно открывать окошко с детализацией. В самой детализации также можно производить еще более мелкую детализацию.
    В качестве варианта, можно производить детализацию в этом же окне. Т.е. сокращение заменяется на свою детализацию. В настройках среды можно запоминать типы структур, которые отображать сразу в детализированном виде.
    Ну и естественно, было бы очень удобно, если бы все можно было редактировать. Т.е. при выделении определенного поля происходило появление редактора, раскрывающего списка с возможными значениями и прочее. Язык должен уметь отображать не только байтовые, двухбайтовые и т.д. поля, но и более сложные, например, составленные из различных бит. Поэтому должен присутствовать механизм получения и установки таких значений.
    Когда разрабатывал библиотеку поддержки xls формата, пришлось написать программу, отображающую BIFF-поток (внутренний формат xls). На рисунке пример дампа. Напоминаю, что для отображения нет никакой строгой формы вывода. В том числе для дампа, потому что он задается тем же скриптом и ничем от него не отличается. Т.е. термин «дамп» я использую только для удобства.

    Синтаксис
    Здесь все как обычно. Типы стандратные и прочие похожие, которые можно добавлять по мере надобности.

   char, byte, wchar, word, uint, ulong – без знаковые
   int, long - знаковые
   float, double, extended – с плавающей точкой
   str – строка (null terminated)
   wstr – UniCode строка

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

    project
    Структура «project» описывает, какие структуры являются основными и как их использовать. Пока на ум пришло два оператора:

project {
  files "*.xls" XLS "Книга Excel";
  resource BIFF "Excel BIFF";
}

   Первый оператор сообщает среде, что для обработки файлов с расширением «xls» следует использовать структуру «XLS». «Книга Excel» это просто заголовок, который может быть выведен в окне.
   Второй оператор задает ресурс c именем «Excel BIFF» и именем структуры «BIFF», которая такие ресурсы будет обрабатывать. Ресурс не является самостоятельной частью, а может использоваться как часть файла. Структура разбора файла может при необходимости сослаться на такой ресурс.

    Описание данных
    Описание структуры данных начинается с ключевого слова «struct». Основной единицей здесь является поле. Его объявление всегда влечет за собой чтение данных из потока и занесение их в данное поле. Поле может иметь простой тип, быть массивом или являться структурой. Объявления всегда происходят в нотации языка Си: сначала записывается тип, затем через пробел имя. Если по какой-то причине не нужно отображать поле, перед типом нужно поставить слово «hidden».
    При объявлении поля есть также возможность «разбросать» его на битовые цепочки, используя ключевое слова «fields». При этом содержимое цепочки не отображается на экране, но ее можно вывести потом с помощью свойства. Например:

hidden word pattern_colors fields pat_color 6-0,pat_back 13-7;

    Здесь «pat_color» и «pat_back» являются именами цепочек бит, по которым к ним можно обратиться в последствие.
    Свойства подобно полям имеют значение и типы, но при этом не считывают данных из потока, а предназначены для более удобного их представления. Свойства имеют несколько синтаксисов:

prop тип имя ref имя_поля_или_свойства [values имя_словаря];
prop boolbit имя ref имя_поля_или_свойства номер_бита;
prop тип имя read выражение write поле/свойство = выражение [values имя_словаря];

    В первом варианте свойство определяется как ссылка, позволяя накладывать новую структуру на уже имеющиеся данные. Например, на цепочку бит объявленную ранее. Если присутствует слово «values», то для отображения и установки поля или свойства используется словарь.
    Второй вариант: ключевое слово предназначено специально для выделения из данных одного бита. Здесь указывается, откуда берется информация, и на какой именно бит будет ссылаться свойство.
    В третьем варианте определяются операции для, соответственно, чтения и записи. Если ключевое слово «write» не указано, то свойство используется только для чтения.

    Имена
    Каждое поле/свойство/последовательность битов имеет два имени: для отображения на экране и для обращения из других частей скрипта. Последнее является идентификатором. Чтобы различать имена, первое при объявлении задается в кавычках. Если задается только второе имя, то оно же используется и для отображения (если нет директивы «\nolabel»). Если только первое, то из скрипта элемент будет недоступен. Чтобы задать сразу два имени сначала пишем первое, затем частицу «as», затем второе. Например:

"размер записи" as size

    Словарь
    Данная структура начинается со слова «dict», затем следует имя, которое в дальнейшем будем называть именем словаря. Содержимое структуры - это пары значений (значение, пробел, значение). Обращение к словарю происходит как вызов функции с данным именем. При этом ключом считается первое значение и возвращается второе. Для обратного преобразования перед именем словаря нужно поставить знак «~» (тильда).
    Ключевое слово «default» перед какой-то парой позволяет сделать значения этой пары значениями по умолчанию, которые возвращаются в случае, когда соответствие не было найдено. Очевидно, что пара по умолчанию должна быть единственной на словарь. При отсутствии в словаре ключевого слова «default» парой по умолчанию считается самая первая пара. Директива «noview» указывает, что данное значение не нужно отображать.

dict LINE_STYLES{
  default noview 0x00 "No line";
  0x01 "Thin";
  0x02 "Medium";
  0x03 "Dashed";
  0x04 "Dotted";
  0x05 "Thick";
  0x06 "Double";
  0x07 "Hair";
  0x08 "Medium dashed";
  0x09 "Thin dash-dotted";
  0x0A "Medium dash-dotted";
  0x0B "Thin dash-dot-dotted";
  0x0C "Medium dash-dot-dotted";
  0x0D "Slanted medium dash-dotted";
}

    Пример обращения к словарю:

hidden uint borderlines fields left_ls 3-0, right_ls 7-4;
prop str "Left line style" ref left_ls values LINE_STYLES;
prop str "Right line style" ref right_ls values LINE_STYLES;


    Детализация
    Детализация, как уже говорил ранее, позволяет задать для структуры полный и сокращенный вид. Синтаксис:

detail{
  Описание детализованной версии
}else{
  Описание сокращенной версии
}

    Если внутри первой скобки выяснилось, что детализация не требуется, то следует вставить оператор «nodetail», и тогда произойдет откат на начало структуры и запуск сокращенной версии.
    Совпадение размера данных для детализированной и сокращенной версии целиком ложится на составителя скрипта. В случае разницы размера есть два варианта:
    1. Для детализированной версии отдавать только ту часть потока, которая была прочитана сокращенной версией. Если размер детализации меньше, то оставшиеся байты игнорировать. Если больше, то либо подавать с заполнением конца пустым значением (например, нулем), либо разрешить читать дальше поток.
   2. Более медленное, но верное: пересчитывать размер и затем данные после detail-структуры также пересчитывать.

    Директивы
    Директивы влияют на вывод информации. Они начинаются с косой черты «\», за которой сразу без пробелов следует сама директива с параметрами. Всего три типа директив:
    1. Без параметров.
    2. Переключатели: директивы, заканчивающиеся символами «+» и «-».
    3. С параметрами: знак «:» используется как разделитель директивы и ее параметра(ов).
    Сами директивы могут использоваться в двух вариантах:
    1. после ключевого слова «set». Данное слово делает действие директив действительными на все последующие строки;
    2. использование в начале оператора дает распространение только на этот оператор. Следует так же отметить, что локальные установки имеют приоритет над глобальными.

    Пример директив оформления:
    \nobr – запрет перевода строки;
    \br – перевод строки;
    \nolabel – запрещает вывод метки. Без использования данной директивы метка используется всегда, когда есть имя поля в кавычках. При отсутствии директивы «\nobr» и имени в кавычках меткой считается второе имя (идентификатор);
    \pos:смещение – отображение данных, начиная с определенного столбца. Если строка уже заполнена дальше указанного смещения, то вывод производится дальше данного смещения.
    \b+ и \b - включить/выключить жирное начертание;
    \i+ и \i - включить/выключить наклон;
    \u+ и \u - включить/выключить подчеркивание;
    \c:rgb или \c:rrggbb - задание цвета текста.

    Директивы управления выводом. Они, как правило, добавляются перед массивом.
    \dump шестнадцатеричный вывод. Способ вывода (например, по байтам или словам) может задаваться в самой среде, а можно принудительно задать в директиве. Например: \dump:bytes;
    \text текст, который выводится совместно c шестнадцатеричным дампом. Если используется какая-то не стандартная кодировка, то можно ее указать. Например: \text:koi-8. Тогда будет осуществляться перекодировка.

    Позиция в файле.
    \offset:режим,позиция – задает новую позицию чтения из файла. Режим один из трех: «beg», «cur», «end». Соответственно, от начала файла (ресурса), от текущего места, от конца файла (ресурса). «Позиция» это смещение относительно точки, заданной режимом. Если в качестве позиции используется выражение, то его нужно заключить в круглые скобки. Напомню, указание «set» переводит считывание навсегда в новую позицию, а использование в качестве префикса распространяется только на текущий оператор.
    На примере PAK-файла

project{
  files "*.pak" PAK "Quake Pak-файл";
}

struct PAK_res{
  separator;
  char[56] "Название";
  long "Смещение";
  long "Размер";
}

struct PAK{
  char[4] ID;
  long "Смещение каталога" as offs;
  long "Кол-во записей" as rsize;
  \offset:beg,offs PAK_res[rsize];
}

    Директиву можно указывать перед ссылкой на структуру, поэтому в большинстве случаев ключевого слова «set» можно избежать. Если же такая ситуация возникнет, и при этом нужно будет возвратиться в исходную точку, то текущее положение можно запомнить в какой-нибудь переменной:

  set temp_pos = \offset

    Некоторые операторы
    halt – делает откат в начало главной структуры (той, что описана в project) и возвращает код, который указывает, что скрипт не может обработать данную структуру. Удобно использовать в случае, если неизвестен тип ресурсов, либо различные форматы файлов в силу некоторых обстоятельств имеют одинаковое расширение;
    rollback – делает откат текущей структуры;
    return – выход из структуры;

    Пример
    В качестве примера распишу часть скрипта, реализующего вывод как на приведенном в начале рисунке.

project {
  resource BIFF "Excel BIFF";
}

dict BIFF_TAGS {
  default 0x0000 "(unknown)" ;
  0x0908 "B0F";
  0x020B "INDEX";
  0x00E0 "XF";
  0x000A "EOF";
  ...
}

struct XF {
  ...
}

struct BIFF_RECORD{
  \pos:0 \c:F00 \nobr word code;
  \pos:6 \c:0F0 \nobr prop str codename read "("+BIFF_TAGS(code)+")";
  \pos:15 \nobr word datasize;
  detail{
    if (code=0x0085) {
      hidden word blocks;
      set BLOCK_COUNT = blocks + 1;
    }
    case (code) {
      0x00E0: XF;
      0x000A: {
        set BLOCK_COUNT = BLOCK_COUNT - 1;
        if (BLOCK_COUNT=0) {
          set STOP = true;
          return;
        }
        separator;
      }
    }else nodetail;
  }else \pos:20 \dump \text byte[datasize];
}

struct BIFF{
  set BLOCK_COUNT = 1;
  set STOP = false;
  while (not STOP) BIFF_RECORD;
}

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

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

    3. Если скрипт относительно простой, то можно попробовать реализовать механизм генерации библиотеки работы с файлами либо ресурсами, описанными данным скриптом. Т.е. загрузили скрипт, нажали кнопку, выбрали язык (Delphi, C++ и т.д.) и дали имя файлу, куда сохранить. Библиотека готова.
    Задачка достаточно сложная, но если ее реализовать, это скорее всего стало бы одной из важнейших составляющих программы. Напомню, что программа планировалась как помощник написания модулей поддержки определенных форматов. Т.е. в таком случае разработка свелась бы к описанию формата с последующей генерацией модуля.


© 2005-16, Powered By Digs (Написать письмо, vk)