От автора: бинарные операции в PHP – немного странные. Так как PHP с самого начала являлся шаблонным слоем для C-кода, в нем все еще много этих C-измов. Множество названий функции в точности отображают API C-уровня, даже если работают иногда немного по-разному. Например, PHP strlen напрямую устанавливают соответствие с STRLEN(3), и тому есть бесконечное множество примеров. Однако, как только дело доходит до работы с бинарными данными, все неожиданно сильно меняется.
Детали учебника
Тема: PHP
Сложность: продвинутая
Бинарные данные, говорите?
Что такое бинарные данные? Бинарный (двоичный) код на самом деле – всего лишь представление данных, а любые данные можно представить как нули и единицы. Говоря о бинарных данных, мы обычно подразумеваем представление данных, как последовательность битов. И обычно нам нужно закодировать некие данные для передачи в биты, а затем на другой стороне декодировать их. Бинарное представление – это просто эффективный формат передачи.
Чтобы кодировать и раскодировать, нам нужно как-то получить доступ к отдельным битам, а затем получить функции, способные конвертироваться из некоего существующего представления в упакованное и обратно. Один из способных к этому и обеспечиваемых языками программирования инструментов – побитовые операции.
Способ C
До того, как рассмотреть его работу в PHP, я хотел бы сначала изнутри рассмотреть, как C управляется с ним.
Хотя C является языком высокого уровня, он все еще очень близок к «железу». Внутри CPU и RAM данные хранятся, как последовательность битов. Следовательно, целые числа внутри C – тоже последовательность битов. Символ – тоже последовательность битов, а строка – массив символов.
Давайте рассмотрим пример:
1 2 3 |
char *hello = "Hello World"; printf("char: %c\n", hello[0]); printf("ascii: %i\n", hello[0]); |
мы обращаемся к первому символу H и печатаем два его представления. Первое – это представление символа (%c), второе – представление целого числа (%i). Представление символа – это H, представление целого числа – 72. Почему 72, спросите вы? Потому что десятичное число 72 представляет букву H в таблице ascii (Американского стандартного кода обмена информацией), что определяет набор знаков, который назначает каждому числу от 0 до 128 отдельное значение. Некоторые из них – управляющие символы, некоторые представляют числа, а некоторые – буквы.
Все в порядке. Данные – это просто данные, которые где-то хранятся, и нам нужно решить, как их интерпретировать.
PHP: как бы то ни было, вам не следует этого делать в PHP
Одна из основных причин, почему в PHP это так отличается – тот факт, что строка – это совсем иной тип. Давайте разберемся, что делает PHP:
1 2 3 |
$hello = "Hello World"; var_dump($hello[0]); var_dump(ord($hello[0])); |
Чтобы получить код ascii символа в PHP, вам нужно вызвать ord к символу (который на самом деле не символ, а строка из одного знака, так как здесь нет типа символа). Ord возвращает значение ascii символа.
В отличие от примера в C, здесь у нас имеется более одного представления данных. В C есть только единичное представление, у которого могут оказаться разные интерпретации. Число 72 в одно и то же время может быть символом H. PHP требует от нас конвертирования строк и значений ascii, сохраняя их оба в различных переменных с разными типами.
И это является основной головной болью при выполнении бинарного парсинга в PHP. Так как данные могут представляться как строка или число, вам необходимо знать, с чем из них вы имеете дело. А в зависимости от этого можно применять различные инструменты.
Опускаемся до уровня битов
Пока что мы видели, как получить доступ к отдельным байтам и как получить их значение ascii. Но пока нам это не очень пригодно. Для парсинга бинарных протоколов нам нужно получить доступ к отдельным байтам.
В качестве примера я использую заголовок пакета DNS. Заголовок состоит из 12 байтов. Эти байты разделены на 6 полей по 2 байта в каждом. Вот формат, определенный RFC 1035:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |
Все поля, кроме второго, должны читаться, как полные номера. Второе поле отличается, потому что в своих 2 байтах оно вмещает множество значений.
Давайте предположим, что у нас имеется пакет DNS, представленный строкой, и нам нужно сделать парсинг этой «бинарной строки» с помощью PHP. Выделение значений чисел делается легко. У PHP имеется функция unpack, позволяющая вам распаковать любую строку, разбивая ее в набор полей. Вам нужно сказать ей, какое количество байтов вам требуется в каждом из полей. Так как у нас по 16 битов в каждом поле, можно просто применить n, что определяется как unsigned short (всегда 16 bit, обратный порядок байтов). Unpack дает возможность повторения формата как образца путем прикрепления *, так что его можно распаковать, просто применив:
1 |
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); |
Она конвертирует строку байтов в 6 чисел, каждое из которых основано на двух байтах. Мы вызываем array_values, потому что значение, возвращаемое unpack – это 1-проиндексированный массив. Чтобы применить list, нам нужен 0-проиндексированный массив.
Вот данные заголовка DNS, представленные как шестнадцатеричные. Два символа соответствуют одному байту. Два байта – одно поле.
1 |
72 62 01 00 00 01 00 00 00 00 00 00 |
Это значит, что значения равны:
id – это 0x7262, что соответствует 0111 0010 0110 0010 в бинарном исчислении, 29282 в десятичном.
fields – это 0x0100, что соответствует 0000 0001 0000 0000 в бинарном.
qdCount – это 0x0001, что соответствует 0000 0000 0000 0001 в бинарном, 1 в десятичном.
anCount, nsCount и arCount – это 0.
Теперь давайте рассмотрим расширение, которое fields (размещает) переменную в содержимые ею значения. Для этого нельзя применять распаковывание unpack, потому что unpack имеет дело только с целыми байтами. Но можно использовать значение, полученное нами от декодирования с n, и извлечь из него байты с помощью побитовых операторов.
Побитовые операторы
Существует множество побитовых операторов, работающих с бинарной интерпретацией целых чисел PHP.
& — это побитовый AND
| — это побитовый OR
^ — это побитовый XOR
~ — это NOT, что означает, что он инвертирует все биты
« — это сдвиг влево
» — это сдвиг вправо
Основное применение & — это битовая маска. Битовая маска дает вам возможность возвращать определенные биты в исходное состояние. Это помогает отметить только нужные вам биты и игнорировать все остальные.
Мы определили, что значение fields – это число, представляющее 0000 0001 0000 0000. Мы обработаем это значение справа налево. Первое суб-поле – это rcode, и оно имеет длину в 4 бита. Значит, нужно проигнорировать все, кроме последних 4 битов. Это можно сделать, применив битовую маску:
1 2 3 |
value: 0000 0001 0000 0000 bitmask: 0000 0000 0000 1111 result of & op: 0000 0000 0000 0000 |
Оператор & устанавливает те биты 1 как в значение, так и в битовую маску. Так как в данном случае совпадений нет, результатом будет 0. В коде PHP та же операция выглядит так:
1 |
$rcode = $fields & bindec('1111'); |
Примечание: Мы применяем bindec, чтобы получить целое число, представляющее бинарное 1111, потому что побитовые операторы работают с числами. Со времен PHP 5.4 стало возможным писать 0b1111, PHP автоматически конвертирует его в значение целого числа 15.
Теперь нужно получить следующее значение – z. Можно таким же образом применить битовую маску, но теперь у нас возникла новая проблема. Значение, о котором мы беспокоимся, имеет несколько лишних битов справа. Точнее, 4 бита от rcode. Их можно установить на 0, применив битовую маску, но это означает, что там имеется несколько ненужных нам 0.
Решение проблемы кроется в побитовом сдвиге. Можно взять все число в бинарном виде и сдвинуть его влево или вправо. Сдвиг вправо уничтожает крайние справа биты, так как они сдвигаются «за край». В этом случае нам нужно сдвинуть их вправо, и сделать так 4 раза.
1 2 |
value: 0000 0001 0000 0000 result of >> 4: 0000 0001 0000 |
Теперь можно применять к этому значению битовую маску, чтобы выделить последние 3 бита для получения значения z.
1 2 3 4 |
value: 0000 0001 0000 0000 result of >> 4: 0000 0001 0000 bitmask: 0000 0000 0000 0111 result of & op: 0000 0000 0000 |
И то же самое в коде PHP:
1 |
$z = ($fields >> 4) & bindec('111'); |
Применять эту методику можно снова и снова, чтобы произвести разбор всего заголовка. В итоге получится вот что:
1 2 3 4 5 6 7 8 9 10 |
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); $rcode = $fields & bindec('1111'); $z = ($fields >> 4) & bindec('111'); $ra = ($fields >> 7) & 1; $rd = ($fields >> 8 ) & 1; $tc = ($fields >> 9) & 1; $aa = ($fields >> 10) & 1; $opcode = ($fields >> 11) & bindec('1111'); $qr = ($fields >> 15) & 1; |
Вот так делается парсинг бинарных данных в PHP.
Резюме
У PHP есть разные способы представления бинарных данных.
Для конвертации из «бинарной строки» в целое число применяйте unpack.
Чтобы получить доступ к отдельным битам этого целого числа, используйте побитовые операторы.
Для дальнейшего чтения
PHP: Побитовые операторы (PHP: Bitwise Operators)
RFC 1035: Доменные имена – применение и спецификация (RFC 1035: Domain Names — Implementation and specification)
github.com/reactphp/react/blob/master/src/React/Dns/Protocol/Parser.php — Исходный код: React\Dns\Protocol\Parser (Source code: React\Dns\Protocol\Parser)
Источник: //igor.io/
Редакция: Команда webformyself.
Комментарии (2)