Початок з найнижчого рівня
Як читати двійкові файли Grim Dawn
Байт — це не загадковий фрагмент машинної мови. Це маленька клітинка у відомому місці файла, яка може містити число від 0 до 255. Навчимося бачити такі числа, складати їх у більші значення й не губитися в позначках на кшталт 0x18.
file+0x00- 0x00–0x03 · поля профілю
- 0x04–0x0F · поля таблиці записів
- 0x10–0x17 · поля таблиці рядків
Зараз це лише послідовність чисел. Наприкінці статті перші чотири клітинки вже матимуть зрозумілий зміст.
Перед стартом
00 · Що ви зможете після цієї статті
Побачивши чотири байти у шістнадцятковому записі — 44 00 00 00, — ви не будете вгадувати, що вони означають. Ви поставите правильні запитання: де починається значення, скільки воно займає байтів і за яким правилом їх треба прочитати?
- відрізняти самі байти від способу, яким ми їх записали;
- читати багатобайтові цілі числа в little-endian;
- знаходити ділянку файла за зміщенням і довжиною;
- розуміти, чому без правил формату байти не мають одного «правильного» значення.
Одна клітинка
01 · Файл — це пронумерований ряд байтів
На диску файл не поділений на «заголовок», «записи» чи «назви». Для комп’ютера це послідовність маленьких чисел. Кожне таке число займає один байт (byte) і стоїть на певній позиції.
Біт — це одна двійкова позначка: 0 або 1. Байт складається з восьми бітів. Якщо тлумачити його як беззнакове ціле число, він може містити значення від 0 до 255. Але те саме число можна записати по-різному. Наприклад, десяткове 68 і шістнадцяткове 0x44 — це один і той самий байт.
file+0x00Позначка file+0x00 задає позицію першої клітинки. Кожна наступна стоїть на один байт далі, тому 9D лежить на позиції file+0x04.
Коротший запис
02 · Hex не змінює дані — лише робить їх видимими
Двійковий файл часто показують у шістнадцятковому записі (hex). У ньому є шістнадцять цифр: звичні 0–9 і літери A–F.
| HEX | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Десяткове | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| HEX | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Десяткове | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| HEX | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|
| Десяткове | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Дві 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- Беззнакове ціле число шириною 32 біти, тобто чотири байти.
LE- Little-endian: найменш значущий байт, тобто байт із множником 1, записаний першим.
Карта файла
04 · Зміщення відповідає на запитання «де?»
Зміщення (offset) — це відстань у байтах від визначеної точки відліку. У записі file+0x18 такою точкою є початок файла: треба пропустити 0x18, тобто 24 байти.
Заголовок синтетичного ARZ займає перші 24 байти. Одна з наступних секцій — таблиця записів (record table); її призначення розберемо в наступній статті. Поле record_table_offset містить 0x9D, тому ця таблиця починається на позиції 157.
| Ділянка | Діапазон | Байти |
|---|---|---|
| заголовок | 0x00..0x18 | 24 |
| стиснені дані | 0x18..0x9D | 133 |
| таблиця записів | 0x9D..0x115 | 120 |
| таблиця рядків | 0x115..0x1D3 | 190 |
| контрольні суми | 0x1D3..0x1E3 | 16 |
0x00..0x18: Початок входить у діапазон, кінець — ні. Тому це позиції від 0x00 до 0x17: рівно 24 байти, а не 25.
Зміщення саме по собі не називає секцію. Воно лише приводить до позиції. Що починається там і як це читати, визначають правила конкретного формату.
Від чисел до рядків
05 · Рядок — це байти плюс правило
Рядок (string) — це послідовність символів. У файлі він зберігається як байти без вбудованої позначки «я рядок». Формат має визначити, де рядок починається, скільки байтів займає і в якому кодуванні їх тлумачити.
ARZ збирає такі значення в таблицю рядків (string table) — нумерований список, спільний для всього файла. Деякі формати ставлять після рядка службовий нульовий байт NUL. В ARZ такого байта немає: перед кожним рядком стоїть чотирибайтова довжина, яка рахує саме його байти.
У цьому прикладі перетворити байти на знаки допомагає ASCII — таблиця відповідностей, у якій, наприклад, байт 0x43 означає літеру C.
Class у синтетичній таблиці · HEXentry+0x005роль значенняКількість наступних ASCII-байтів
entry+0x04Classроль значенняП’ять символів після застосування ASCII
У чотирьох перевірених ARZ-архівах усі рядки складаються з ASCII-байтів. Проте сам формат зберігає довжину, а не назву кодування. Це важлива межа: спостереження для поточного профілю не треба перетворювати на вічне правило для всіх можливих ARZ. Якщо трапляться байти вище 0x7F, їх слід зберегти й позначити кодування як невизначене, а не мовчки вгадувати його.
Читання без вгадування
06 · Безпечне читання починається з перевірки меж
Якщо заголовок каже «секція починається тут і має таку довжину», це ще не гарантія, що зазначені байти справді існують. Файл може бути обрізаним, пошкодженим або належати іншій версії.
- Перевірити очікувану структуру
Застосовуйте лише той профіль формату, структуру якого справді підтримуєте.
- Обчислити початок і кінець
Кінець секції обчислюємо як
зміщення + довжина. Додавання виконуємо з перевіркою переповнення. Якщо виникло переповнення або обчислений кінець опинився перед початком секції, результат некоректний і читати файл далі не можна. - Звірити з межами контейнера
Секція не може продовжуватися за кінцем файла.
- Прочитати рівно заявлену ділянку
Зайві або відсутні байти — це сигнал, а не дрібниця.
- Наявна ділянка
- Закінчується разом із файлом.
- Заявлена секція
- Продовжується за кінцем файла й виходить за його межі.
Якщо заявлена ділянка виходить за межі файла, подальше читання треба припинити. «Майже вистачило» не є валідним форматом.
Застосовуємо правила
07 · Прочитаймо перші чотири байти ARZ
Тепер переходимо від загальних правил байтового кодування до конкретного поточного профілю ARZ. Його перші чотири байти — це два little-endian числа шириною по два байти. Специфікація дає їм нейтральні структурні назви, бо оригінальні приватні назви невідомі:
file+0x002роль значенняРевізія сумісності формату
file+0x023роль значенняБіти можливостей заголовка
Поточний офіційний інструмент запису створює пару (2, 3). Офіційні програми читання відхиляють header_word_0, лише якщо значення менше за 2, тому це не точне magic-значення. Спостережену роль цього поля описуємо як ревізію сумісності формату.
Друге слово — не числова версія, а біти можливостей заголовка. 0x0001 бере участь у перевірці сумісності між шарами архіву, а 0x0002 позначає наявність 16-байтової завершальної секції з контрольними сумами (checksum footer). Значення двох станів першого біта й решти бітів не встановлено.
Якщо показати ті самі чотири байти як одне u32 LE, отримаємо 0x00030002, або десяткове 196610. Це лише упаковане представлення двох слів, а не одне семантичне значення версії.
Після читання
П’ять опорних думок
- Файл — це адресована послідовність байтів.
- Hex — компактний запис тих самих чисел.
- Багатобайтове число потребує ширини й порядку.
- Зміщення та довжина визначають ділянку файла.
- Значення з’являється лише після правила формату.