Как определить бинарный файл

Обновлено: 06.07.2024

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

Когда бинарный файл требуется просмотреть или переместить, содержимое файла переводится в формат, понятный человеку. Бинарный файл имеет расширение .bin. Прочитать его можно с помощью встроенной функции или модуля. В этом уроке мы разберём различные способы чтения бинарных файлов с помощью Python.

Подготовка

Перед тем, как начать урок, желательно создать один или несколько бинарных файлов, чтобы воспользоваться скриптом из примера. Ниже представлены два скрипта на Python, которые создадут два бинарника. Файл binary1.py создаёт string.bin, содержащий строковые данные, а binary2.py – number_list.bin со списком из числовых данных.

Binary1.py

Binary2.py

Считываем бинарный файл со строковыми данными в массив байтов

В Python существует множество способов прочитать бинарный файл. Можно прочитать определённое количество байтов или весь файл сразу.

Результат

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


Считываем бинарный файл со строковыми данными в массив

Следующий скрипт поможет нам прочитать бинарник number_list.bin, созданный нами ранее.

Бинарный файл содержит список с числовыми данными. Как и в предыдущем примере, функция open() открывает файл и читает из него данные. Затем из бинарника читаются первые 5 чисел и перед выводом объединяются в список.

Результат

После выполнения скрипта мы получим следующий результат. Бинарный файл содержит 7 чисел, первые 5 вывелись на консоль.


Читаем бинарный файл с помощью NumPy

В этой части мы поговорим о том, как создать бинарный файл и прочитать его с помощью массивов NumPy. Перед началом работы необходимо установить модуль NumPy командой в терминале или через ваш редактор Python, в котором вы будете писать программу.

Функция tofile() создаёт текстовый или бинарный файл, а fromfile() считывает данные из файла и создаёт массив.

Синтаксис tofile()

Первый аргумент обязательный – он принимает имя файла, путь или строку. Файл создастся, только если будет указан первый аргумент. Второй аргумент – необязательный, он используется для разделения элементов массива. Третий аргумент также необязателен, он отвечает за форматированный вывод содержимого файла.

Синтаксис fromfile()

Первый аргумент обязательный – он принимает имя файла, путь или строку. Содержимое файла будет прочитано, только если вы укажете имя файла. dtype определяет тип данных в возвращаемом массиве. Count задаёт число элементов массива. Sep – для разделения элементов текста или массива. Offset определяет позицию в файле, с которой начинается считывание. Последний аргумент нужен, чтобы создать массив, не являющийся массивом NumPy.

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

Результат

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


Заключение

Мы рассмотрели 3 разных способа чтения бинарных файлов. В первом примере мы получили содержимое файла в виде массива байтов, во втором и третьем – в виде списка.

неофициально большинство из нас понимает, что существуют "двоичные" файлы (объектные файлы, изображения, фильмы, исполняемые файлы, проприетарные форматы документов и т. д.) и "текстовые" файлы (исходный код, XML-файлы, HTML-файлы, электронная почта и т. д.).

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

тем не менее, существуют различные инструменты, которые работают с широким спектром файлов, и на практике вы хотите сделать что-то другое в зависимости от того, является ли файл "текстовым" или "двоичным". Примером этого является любой инструмент, который выводит данные на консоль. Простой "текст" будет выглядеть хорошо, и полезно. "двоичные" данные портят ваш терминал, и, как правило, не полезно смотреть. GNU grep по крайней мере использует это различие при определении того, следует ли выводить совпадения на консоль.

Итак, вопрос в том, как вы определяете, является ли файл "текстовым" или "двоичным"? И ограничивать дальше, как вы скажете на Linux, как файловая система? Я не знаю никаких метаданных файловой системы, которые указывают на " тип " файла, поэтому вопрос далее становится, по проверяя содержимое файла, как я могу определить, является ли он "текстовым" или "двоичным"? И для простоты, давайте ограничим "текст" для обозначения символов, которые могут быть напечатаны на консоли пользователя. И в частности как бы вы реализовать этого? (Я думал, что это подразумевалось на этом сайте, но я думаю, что это полезно, в общем, указать на существующий код, который делает это, я должен был указать), я действительно не после того, какие существующие программы я могу использовать для этого.

image

Если вас не пугает картинка выше, если вы знаете чем отличается big-endian от little-endian, если вам всегда было интересно как "устроены" бинарные файлы, значит эта статья для ВАС!

На Хабре уже было несколько статей про реверс инжинеринг бинарных форматов и про исследование структуры байткода .class файла:
Пул констант,
Java Bytecode Fundamentals,
Java байткод «Hello world»,
Hello World из байт-кода для JVM и т.д.
У исследователя возникает задача либо разобраться с неизвестным бинарным протоколом либо поковырять бинарную структуру на которую есть спецификация.

Мой интерес к бинарным форматам возник еще когда я был студентом и писал курсовую работу по разработке драйвера файловой системы Linux. Несколько лет спустя я читал лекции по основам Linux для экспертов-криминалистов — в давние времена Linux был в новинку и молодой специалист после ВУЗа мог поведать взрослым экспертам много нового. Рассказывая, как снять дамп с диска с помощью dd, а после подключить образ на другом компьютере для изучения, я понимал, что в образе диска лежит много интересной информации. Эту информацию можно было бы извлечь и без монтирования образа (ага, mount -o loop . ), если знать спецификацию на формат файловой системы и иметь соответствующие инструменты. К сожалению, у меня не было таких инструментов.

Мне был нужен универсальный механизм для описания бинарных структур и универсальный загрузчик. Загрузчик, используя описание, будет читать бинарные данные в память. Обычно приходиться иметь дело с числами, строками, массивами данных и составными структурами. С числами все просто — они имеют фиксированную длину — 1, 2, 4 или 8 байт и могут быть сразу отображены в типы данных, имеющиеся в языке. Например: byte, short, int, long для Java. Для числовых типов длиной более одного байта нужно предусмотреть маркер порядка байт (так называемое BigEndian/LittleEndiang представление).

Со строками сложнее — они могут быть в различных кодировках (ASCII, UNICODE), иметь фиксированную или переменную длину. Строку фиксированной длинны, можно считать как массив байт. Для строк с переменной длиной можно использовать два варианта записи — указывать в начале строки ее длину (Pascal или Length-prefixed strings) либо в конце строки ставить специальный знак, обозначающий конец строки. В качестве такого знака используют байт со значением ноль (так называемые null-terminated srings). Оба варианта имеют преимущества и недостатки, обсуждение которых выходит за рамки этой статьи. Если размер задается в начале, то при разработке формата нужно определиться с максимальной длиной строки: от этого зависит сколько байт мы должны выделить на маркер длины: 2 8 — 1 для одного байта, 2 16 — 1 для двух байт и т.д.

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

Нам необходимо каким-то образом описать структуру Java .class файла. В качестве результата хотелось бы иметь набор Java классов, где каждый класс содержит только поля, соответсвующие исселдуемой структуре данных и, возможно, вспомогательные методы для отображения объекта в человеко-читаемом виде при вызове toString() метода. Категорически не хотелось бы иметь внутри логику, отвечающую за чтение или запись файла.

Берем спецификациею виртуальной машины Java,
JVM Specification, Java SE 12 Edition.
Нас будет интересовать секция 4 "The class File Format".

Для того, чтобы определить какие поля в каком порядке загружать, введем аннотацию @FieldOrder(index=. ). Нам необходимо явно указывать порядок полей для загрузчика, поскольку спецификация не даем нам гарантии на то, в каком порядке они будут сохранены в бинарном файле.

Java .class файл начинается с 4 байт magic number, двух байт минорной версии Java и двух байт мажорной версии. Упакуем magic number в переменную int, а номер минорной и мажорной версии — в short:

Дальше в .class файле идет размер пула констант (двухбайтовая переменная) и сам пул констант. Введем аннотацию @ContainerSize для объявления размера массивов и списочных структур. Размер может быть фиксированный (будем задавать его через аттрибут value) либо иметь переменную длинну, определяемую прочитанной ранее переменной. В этом случае будем использовать "fieldName" аттрибут, который указывает из какой переменной будем считывать размер контейнера. В соответствии со спецификацией (секция 4.1,
"The ClassFile Structure"), реальный размер пула констант отличается на 1 от того значения,
которое записано в constant_pool_count:

Чтобы учесть такие коррекции, введем дополнительный аттрибут corrector в @ContainerSize аннотации.
Теперь мы можем добавить описание пула констант:

В случае более сложных вычислений, можно просто добавить get-метод, который вернет необходимое значение:

Каждый элемент в пуле констант представляет из себя либо описание соответствующей константы типа int, long, float, double, String, либо описание одной из составных частей Java класса — поля класса (fields), методы, сигнатуры методов и т.д. Под термином "контстанта" здесь подразумевается неименованое значение, используемое в коде:

Значение 100500 будет представленно в пуле констант как экземпляр CONSTANT_Integer. JVM спецификация для Java 12 определяет 17 типов, которые могут быть в пуле констант.

Constant type Tag
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_Dynamic 17
CONSTANT_InvokeDynamic 18
CONSTANT_Module 19
CONSTANT_Package 20

В нашей реализации создадим класс ConstantPoolItem в котором будет однобайтовое поле tag, определяющее какую именно структуру мы читаем в данный момент. На каждый элемент в таблице выше создадим Java класс, наследник ConstantPoolItem. Универсальный загрузчик бинарных файлов должен уметь определять какой именно класс-наследник должен быть использован на основании уже прочитанного тега
(в общем случае тег может быть переменной любого типа). Для этой цели определим интерфейс HasInheritor и реализуем этот интерфейс в классе ConstantPoolItem:

Универсальный загрузчик сам инстанцирует необходимый класс и продложит считывание. Единственное условие: индексы в классах-наследниках должны иметь сквозную нумерацию с родительским классом. Это означает что во всех классах-наследниках ConstantPoolItem, FieldOrder аннатация должна иметь индекс больше единицы, поскольку в родительском классе мы уже прочитали поле tag с номером "1".

После списка элементов пула констант в .class файле идет двухбайтовый идентификатор, определяющий детали данного класса — является ли класс аннотацией, интерфейсом, абстрактным классом, имеет ли флаг final и т.п. Далее следует двухбайтовый идентификатор (ссылка на элемент в пуле констант), определяющий данный класс. Этот идентификатор должен указывать на элемент с типом ClassInfo. Аналогичным образом определяется суперкласс для данного класса (то что указано после слова "extends" в определении класса). Для классов, не имеющих явно определенных суперклассов, в данном поле присутствует ссылка на класс Object.

В языке Java у любого класса может быть только один суперкласс, но количество
интерфейсов, которые реализует данный класс может быть несколько:

Каждый элемент в interfaceIndexList представляет ссылку на элемент в пуле констант (по указанному
инедксу должен находится элемент с типом ClassInfo).
Переменные класса (properties, fields) и методы представленны соответсвующими списками:

Последним элементом в описании Java .class файла является список аттрибутов класса. Здесь могут быть перечислены аттрибуты описывающие исходный файл, относящийся к классу, вложенные классы и т.д.

Java bytecode оперирует числовыми данными в big-endian представлении, будем это представление использовать по умолчанию. Для двоичных форматов с little-endian числами будем использовать LittleEndian аннотацию. Для строк, которые не имеют предопределенной длины, а
считываются до терминального символа (как C-like null-terminated строки) будем использовать
аннотацию @StringTerminator:

Иногда в нижележащие классы нужно пробросить информацию с более высокого уровня. Объект Method в methodList не имеет информации об имени класса, в котором он находится, более того объект-метод не содержит своего названия и списка параметров. Вся эта информация представленна в виде индексов на элементы в пуле констант. Для виртуальной машины этого достаточно, но нам хотелось бы реализовать методы toString(), чтобы они отображали информацию о методе в удобном для человека виде, а не в виде индексов на элементы в пуле констант. Для этого класс Method должен получить ссылку на ConstantPoolList и на переменную со значением thisClassIndex. Чтобы иметь возможность передавать ссылки на нижележащие уровни вложенности, будем использовать аннотацию Inject:

В текущем классе (ClassFile) будут вызываться getter методы для constantPoolList и thisClassIndex переменных, а в принимающем классе (в данном случае Method), будут вызваны setter методы (если они присутствуют).

Итак, у нас есть один интерфейс HasInheritor и пять аннотаций @FieldOrder, @ContainerSize, LittleEndian, Inject и @StringTerminator, которые позволяют описывать бинарные структуры на высоком уровне абстракции. Имея формальное описание, мы можем передать его универсальному загрузчику, который сможет инстанцировать описанную структуру, осуществить разбор бинарного файла и зачитать его в память.

В результате мы должны иметь возможность использовать такой код:

К сожалению, разработчики Java платформы немного перемудрили и для восьмибайтных значений в пуле
констант предусмотрели две ячейки, причем первая ячейка должна содержать значение, а вторая остается
пустой. Это касается long и double констант.


По всей видимости, разработчики Java хотели применить какую-то низкоуровневую оптимизацию, но позже
было признано, что это дизайнерское решение оказалось

Чтобы обработать эти специфичные случаи, добавим аннотацию @EntrySize, которую будем использовать,
чтобы пометить восьмибайтные константы:

Аттрибут value указывает на количество ячеек, которые будет занимать элемент, index — индекс элемета,
который содержит значение. классы LongInfo и DoubleInfo будут расширять класс EightByteNumberInfo.
Универсальный загрузчик нужно будет расширить фукционалом, поддерживающим аннотацию @EntrySize.

После загрузки класса ClassFileLoader'ом можно остановить отладчик и исследовать загруженный класс в инспекторе переменных в IDE.

image

Class file будет выглядеть вот так:

image

А Constant Pool так:

Для загрузки скомпилированного class файла воспользуйтесь загрузчиком annotate4j.classfile.loader.ClassFileLoader.

Большая часть кода была написана для Java 6, к современным версиям я адоптировал только constant pool. Сил и желания полностью реализовать загрузчик Java opcode'ов у меня не хватило, поэтому там только небольшие наработки в этой части.

Используя эту библиотеку (core часть) мне удалось зареверсить бинарный файл с данными Холтер мониторинга (ЭКГ исследование суточной активности сердца). С другой стороны, я не смог расшифровать бинарный протокол одной учетной системы, написанной на Delphi. Я не разобрался как передаются даты и иногда возникала ситуация, когда фактичиские данные не соответствовали структуре, построенной по предыдущим значениям.

Я пытался построить аналогично Java class файлу модель для ELF формата (запускаемый формат в Unix/Linux), но я не смог полностью понять спецификацию — она оказалась для меня слишком расплывчатой. Та же участь постигла JPEG и BMP форматы — все время натыкался на какие-то сложности с пониманием спецификации.

Файлом называют способ хранения информации на физическом устройстве. Файл - это понятие, которое применимо ко всему - от файла на диске до терминала.

Файл представляется потоком байтов.

В языке Си отсутствуют операторы для работы с файлами. Все необходимые действия выполняются с помощью функций, включенных в стандартную библиотеку < stdio.h >. Они позволяют работать с различными устройствами, такими, как диски, принтер, коммуникационные каналы и т.д.

В Си существует два типа файлов: текстовые (text) и двоичные (binary).


Текстовые и бинарные файлы

Текстовый файл — файл, содержащий текст, разбитый на строки при помощи некоторого разделяющего символа окончания строки или последовательности

в Unix — одиночный символ перевода строки;

в Microsoft Windows за символом перевода строки следует знак возврата каретки:

13 10 в десятичной системе счисления .

Двоичный (бинарный) файл — файл, из которого байты считываются и выводятся в «сыром» виде без какого-либо связывания (подстановки).

В чем отличия?

В чем разница между звуковым файлом и картинкой?

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

Текстовый файл интепретируется как текст, а бинарный, как набор двоичных чисел.

В текстовом файле символ "\n" переводится в "\r\n" при записи в файл.

При считывании производится обратная замена: "\r\n" "\n" .

С бинарными файлами этого не происходит.


Описание файла

Логическое имя ( дескриптор файла - int) – представляет собой указатель на файл, который используется операционной системой для поддержки операций с этим файлом.

Оно определяется так: FILE * fp;

FILE - имя типа, описанное в стандартном заголовочном файле <stdio.h>,

fp - указатель на файл.

Библиотечные функции, используемые при работе с файлами.

Прежде чем читать или записывать информацию в файл,

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

Прототип функции fopen():

FILE * fopen (cost char* name, cost char* mode );

В случае удачного открытия файла, функция fopen() возвращает дескриптор файла, иначе – константу NULL. Она определена в файле <stdio.h> и эквивалентно 0.

Функция fopen()

Обращение к функции fopen( ): fp = fopen(спецификация файла,

способ использования файла);

Спецификация файла (имя файла и путь к нему) может, например, иметь вид: "c:\\myprog.txt" - для файла myprog.txt на диске с:.

Рекомендуется использовать следующий способ открытия файла:

Функция freopen()

Функция freopen применяется для перенаправления файлов, обычно стандартных файлов stdin, stdout, stderr, stdaux, stdprn в файлы, определяемые пользователем.

FILE *freopen(const char *path, const char *mode, FILE *fp);

const char* mode

В некоторых операционных системах имеются различия в работе с текстовыми и бинарными файлами.

К таким системам относятся MS DOS и MS Windows.

В таких системах при открытии бинарного файла к строке mode следует добавлять букву " b ", а при открытии текстового файла – букву " t ".

Кроме того, при открытии можно разрешить выполнять как операции чтения, так и записи; для этого используется символ + .

Порядок букв в строке mode следующий: сначала идет одна из букв " r ", " w ", " a ", затем в произвольном порядке могут идти символы " b ", " t ", "+".

Буквы " b " и " t " можно использовать, даже если в операционной системе нет различий между бинарными и текстовыми файлами, в этом случае они просто игнорируются

В системе Unix различий между текстовыми и бинарными файлами нет.

Способ использования файла

r - открыть существующий файл для чтения;

w - создать новый файл для записи (если файл с указанным именем существует, то он будет переписан);

а - дополнить файл (открыть существующий файл для записи информации, начиная с конца файла, или создать файл,

если он не существует);

r+ - открыть существующий файл для чтения и записи; w + - создать новый файл для чтения и записи;

a + - дополнить или создать файл с возможностью чтения и записи;

rb - открыть двоичный файл для чтения; wb - создать двоичный файл для записи; аb - дополнить двоичный файл;

r+b - открыть двоичный файл для чтения и записи; w+b - создать двоичный файл для чтения и записи; а+b - дополнить двоичный файл с предоставлением возможности чтения и записи

Тут вы можете оставить комментарий к выбранному абзацу или сообщить об ошибке.

Читайте также: