Gear CounselCompendium

Як читати двійкові файли Grim Dawn

Байт — це не загадковий фрагмент машинної мови. Це маленька клітинка у відомому місці файла, яка може містити число від 0 до 255. Навчимося бачити такі числа, складати їх у більші значення й не губитися в позначках на кшталт 0x18.

Перші 24 байти · HEX Синтетичний заголовок ARZ
file+0x00
  • 0x00–0x03 · поля профілю
  • 0x04–0x0F · поля таблиці записів
  • 0x10–0x17 · поля таблиці рядків

Зараз це лише послідовність чисел. Наприкінці статті перші чотири клітинки вже матимуть зрозумілий зміст.

00 · Що ви зможете після цієї статті

Побачивши чотири байти у шістнадцятковому записі — 44 00 00 00, — ви не будете вгадувати, що вони означають. Ви поставите правильні запитання: де починається значення, скільки воно займає байтів і за яким правилом їх треба прочитати?

  1. відрізняти самі байти від способу, яким ми їх записали;
  2. читати багатобайтові цілі числа в little-endian;
  3. знаходити ділянку файла за зміщенням і довжиною;
  4. розуміти, чому без правил формату байти не мають одного «правильного» значення.

01 · Файл — це пронумерований ряд байтів

На диску файл не поділений на «заголовок», «записи» чи «назви». Для комп’ютера це послідовність маленьких чисел. Кожне таке число займає один байт (byte) і стоїть на певній позиції.

Біт — це одна двійкова позначка: 0 або 1. Байт складається з восьми бітів. Якщо тлумачити його як беззнакове ціле число, він може містити значення від 0 до 255. Але те саме число можна записати по-різному. Наприклад, десяткове 68 і шістнадцяткове 0x44 — це один і той самий байт.

Вісім послідовних позицій файла · HEX
file+0x00

Позначка file+0x00 задає позицію першої клітинки. Кожна наступна стоїть на один байт далі, тому 9D лежить на позиції file+0x04.

02 · Hex не змінює дані — лише робить їх видимими

Двійковий файл часто показують у шістнадцятковому записі (hex). У ньому є шістнадцять цифр: звичні 0–9 і літери A–F.

Шістнадцять hex-цифр
HEX0123456789ABCDEF
Десяткове0123456789101112131415
HEX01234567
Десяткове01234567
HEX89ABCDEF
Десяткове89101112131415

Дві hex-цифри точно покривають один байт: від 0x00 до 0xFF. Тому hex зручний: межі байтів видно одразу, тоді як довгий ряд нулів і одиниць важко читати очима.

Одне число, три записи

Для окремих чисел у цій серії діє просте правило: запис із 0x — шістнадцятковий, а запис без 0x — десятковий. Тому 0x18 і 24 — те саме значення.

Перед послідовністю байтів ставимо позначку HEX. Вона стосується всієї послідовності: у записі HEX 44 00 00 00 кожна пара знаків — окремий шістнадцятковий байт.

Беззнакове ціле число від 0 до 255 вміщується в один байт. Значення 256 — уже ні: одному числу потрібно кілька байтів. Тут виникає нове питання — у якому порядку їх складати?

03 · Ті самі два байти можуть утворити різні числа

Спочатку пригадаємо звичайний десятковий запис. У числі 21 цифра 1 означає одну одиницю, а 2 — дві десятки. Тому 2 × 10 + 1 = 21: позиція цифри визначає, на що її множити.

З байтами принцип той самий, але сусідня позиція має множник 256, а не 10. Причина проста: один байт має 256 можливих значень — від 0 до 255. Тому у двобайтовому числі одну позицію множимо на 1, іншу — на 256.

У HEX-послідовності 01 02 байт 0x01 стоїть першим, а 0x02 — другим. Але сама послідовність не визначає, який із них треба множити на 1. Можливі два результати:

перший байт × 1
Другий байт отримує множник 256: 1 × 1 + 2 × 256 = 513.
перший байт × 256
Другий байт отримує множник 1: 1 × 256 + 2 × 1 = 258.

Обидва обчислення правильні. Саме формат має визначити, який множник отримує перший байт. Це правило називають порядком байтів (byte order).

ARZ використовує порядок від найменш значущого байта (little-endian / LE): першим записано байт із множником 1, наступним — із множником 256. Тому в ARZ HEX-послідовність 01 02 означає 513: перший байт множимо на 1, другий — на 256.

Три ненульові байти утворюють одне число
u32 число
u32
Беззнакове ціле число шириною 32 біти, тобто чотири байти.
LE
Little-endian: найменш значущий байт, тобто байт із множником 1, записаний першим.

04 · Зміщення відповідає на запитання «де?»

Зміщення (offset) — це відстань у байтах від визначеної точки відліку. У записі file+0x18 такою точкою є початок файла: треба пропустити 0x18, тобто 24 байти.

Заголовок синтетичного ARZ займає перші 24 байти. Одна з наступних секцій — таблиця записів (record table); її призначення розберемо в наступній статті. Поле record_table_offset містить 0x9D, тому ця таблиця починається на позиції 157.

Усі 483 байти синтетичного ARZ-X3
ДілянкаДіапазонБайти
заголовок0x00..0x1824
стиснені дані0x18..0x9D133
таблиця записів0x9D..0x115120
таблиця рядків0x115..0x1D3190
контрольні суми0x1D3..0x1E316

0x00..0x18: Початок входить у діапазон, кінець — ні. Тому це позиції від 0x00 до 0x17: рівно 24 байти, а не 25.

Зміщення саме по собі не називає секцію. Воно лише приводить до позиції. Що починається там і як це читати, визначають правила конкретного формату.

05 · Рядок — це байти плюс правило

Рядок (string) — це послідовність символів. У файлі він зберігається як байти без вбудованої позначки «я рядок». Формат має визначити, де рядок починається, скільки байтів займає і в якому кодуванні їх тлумачити.

ARZ збирає такі значення в таблицю рядків (string table) — нумерований список, спільний для всього файла. Деякі формати ставлять після рядка службовий нульовий байт NUL. В ARZ такого байта немає: перед кожним рядком стоїть чотирибайтова довжина, яка рахує саме його байти.

У цьому прикладі перетворити байти на знаки допомагає ASCII — таблиця відповідностей, у якій, наприклад, байт 0x43 означає літеру C.

Рядок Class у синтетичній таблиці · HEX
entry+0x00
u32 довжина
5

роль значенняКількість наступних ASCII-байтів

entry+0x04
ASCII рядок
Class

роль значенняП’ять символів після застосування ASCII

У чотирьох перевірених ARZ-архівах усі рядки складаються з ASCII-байтів. Проте сам формат зберігає довжину, а не назву кодування. Це важлива межа: спостереження для поточного профілю не треба перетворювати на вічне правило для всіх можливих ARZ. Якщо трапляться байти вище 0x7F, їх слід зберегти й позначити кодування як невизначене, а не мовчки вгадувати його.

06 · Безпечне читання починається з перевірки меж

Якщо заголовок каже «секція починається тут і має таку довжину», це ще не гарантія, що зазначені байти справді існують. Файл може бути обрізаним, пошкодженим або належати іншій версії.

  1. Перевірити очікувану структуру

    Застосовуйте лише той профіль формату, структуру якого справді підтримуєте.

  2. Обчислити початок і кінець

    Кінець секції обчислюємо як зміщення + довжина. Додавання виконуємо з перевіркою переповнення. Якщо виникло переповнення або обчислений кінець опинився перед початком секції, результат некоректний і читати файл далі не можна.

  3. Звірити з межами контейнера

    Секція не може продовжуватися за кінцем файла.

  4. Прочитати рівно заявлену ділянку

    Зайві або відсутні байти — це сигнал, а не дрібниця.

Перевірка межі файла
Наявна ділянка
Закінчується разом із файлом.
Заявлена секція
Продовжується за кінцем файла й виходить за його межі.

Якщо заявлена ділянка виходить за межі файла, подальше читання треба припинити. «Майже вистачило» не є валідним форматом.

07 · Прочитаймо перші чотири байти ARZ

Тепер переходимо від загальних правил байтового кодування до конкретного поточного профілю ARZ. Його перші чотири байти — це два little-endian числа шириною по два байти. Специфікація дає їм нейтральні структурні назви, бо оригінальні приватні назви невідомі:

file+0x00
u16 header_word_0
2

роль значенняРевізія сумісності формату

file+0x02
u16 header_word_1
3

роль значенняБіти можливостей заголовка

Поточний офіційний інструмент запису створює пару (2, 3). Офіційні програми читання відхиляють header_word_0, лише якщо значення менше за 2, тому це не точне magic-значення. Спостережену роль цього поля описуємо як ревізію сумісності формату.

Друге слово — не числова версія, а біти можливостей заголовка. 0x0001 бере участь у перевірці сумісності між шарами архіву, а 0x0002 позначає наявність 16-байтової завершальної секції з контрольними сумами (checksum footer). Значення двох станів першого біта й решти бітів не встановлено.

Якщо показати ті самі чотири байти як одне u32 LE, отримаємо 0x00030002, або десяткове 196610. Це лише упаковане представлення двох слів, а не одне семантичне значення версії.

П’ять опорних думок

  1. Файл — це адресована послідовність байтів.
  2. Hex — компактний запис тих самих чисел.
  3. Багатобайтове число потребує ширини й порядку.
  4. Зміщення та довжина визначають ділянку файла.
  5. Значення з’являється лише після правила формату.