Порядок байтов

Предпосылки: Двоичная система и байты, Целые числа.

Кодирование текста

Число 0x00000001 занимает четыре байта. В памяти x86-процессора оно хранится как 01 00 00 00, а в сетевом пакете — как 00 00 00 01. Те же байты, зеркальный порядок. Не ошибка — два разных соглашения.

Задача: несколько байтов — один порядок

Один байт — однозначен: 8 бит, значение от 0 до 255. Но 32-битное целое число занимает 4 байта. Память адресуется побайтно — каждый байт лежит по своему адресу. Как разложить 4 байта по 4 последовательным адресам?

Возьмём число 0x01020304. Его четыре байта: 01, 02, 03, 04. Старший байт (most significant byte, MSB) — 01, младший (least significant byte, LSB) — 04. Два варианта размещения в памяти, начиная с адреса 100:

Big-endian (старший байт первый):

  Адрес:  | 100 | 101 | 102 | 103 |
  Байт:   |  01 |  02 |  03 |  04 |
           MSB                  LSB

Little-endian (младший байт первый):

  Адрес:  | 100 | 101 | 102 | 103 |
  Байт:   |  04 |  03 |  02 |  01 |
           LSB                  MSB

Одно и то же число — разная раскладка по адресам. Оба варианта однозначны и равнозначны, пока все участники придерживаются одного соглашения. Проблемы начинаются, когда не придерживаются.

Big-endian: старший байт по младшему адресу

В big-endian порядке старший байт лежит по наименьшему адресу. 0x01020304 записывается как 01 02 03 04 — слева направо, от старшего к младшему. Это совпадает с тем, как человек записывает числа: в 1234 первая цифра — самая значимая.

Происхождение термина: Дэнни Коэн (Danny Cohen) в статье «On Holy Wars and a Plea for Peace» (1980) заимствовал названия из «Путешествий Гулливера» Джонатана Свифта. В романе лилипуты воевали из-за того, с какого конца разбивать яйцо. Big-Endians разбивали с тупого (big) конца. Big-endian — порядок, в котором «большой конец» (старший байт) идёт первым.

Big-endian — стандарт для сетевых протоколов. RFC 1700 фиксирует его как network byte order (сетевой порядок байтов). TCP/IP-заголовки, номера портов, IP-адреса — всё записывается big-endian.

За пределами сети big-endian был родным порядком для нескольких процессорных архитектур: Motorola 68000, SPARC, IBM z/Architecture (мейнфреймы). На этих платформах дамп памяти читается так же, как hex-запись числа.

Little-endian: младший байт по младшему адресу

В little-endian порядке младший байт лежит по наименьшему адресу. 0x01020304 записывается как 04 03 02 01. Непривычно для человека, но удобно для процессора.

Почему x86 (Intel/AMD) выбрал little-endian — две практические причины.

Расширение без сдвига. 16-битное число 0x0042 лежит в памяти как 42 00 (адреса 100, 101). Если расширить его до 32 бит — 0x00000042 — в little-endian результат: 42 00 00 00 (адреса 100, 101, 102, 103). Байт 42 остался по тому же адресу 100 — добавились только старшие нулевые байты по более высоким адресам. В big-endian пришлось бы сдвигать все существующие байты, чтобы освободить место для старших в начале.

Младший байт всегда по базовому адресу. Проверка «число чётное?» — это проверка младшего бита, который находится в младшем байте. В little-endian младший байт всегда по адресу начала числа, вне зависимости от того, 8-битное оно, 16-битное или 64-битное. Процессору не нужно знать полный размер числа, чтобы прочитать его младший байт.

Между двух лагерей

ARM — bi-endian: процессор может работать в любом режиме, выбор делается при конфигурации. На практике большинство ARM-систем (включая смартфоны на Android и iOS) работают в little-endian. Apple M1/M2/M3 — little-endian. Серверы Amazon Graviton (ARM) — тоже little-endian. Итог: на 2020-е годы little-endian фактически доминирует в потребительских и серверных процессорах. Big-endian остался в нишах: сетевое оборудование, мейнфреймы IBM, некоторые встраиваемые системы.

История знает и более экзотический вариант. Middle-endian — третья схема, не выжившая. PDP-11 (DEC, 1970-е) хранил 32-битное число 0x01020304 как 02 01 04 03. Внутри каждого 16-битного слова — little-endian, но сами слова расположены в big-endian порядке. На практике это означало, что ни big-endian, ни little-endian инструменты не могли корректно прочитать дамп памяти PDP-11 без специальной обработки. Путаницы хватило, чтобы убедить индустрию: два варианта — уже много, третий — точно лишний.

Где порядок байтов имеет значение

В пределах одного процессора порядок байтов невидим: процессор записывает и читает в одном соглашении, программа работает с числами, а не с байтами. Проблемы начинаются, когда байты пересекают границу между системами.

Сетевые протоколы. TCP/IP передаёт многобайтовые поля в big-endian. x86-машина, отправляя TCP-пакет, должна переставить байты номера порта и IP-адреса из своего little-endian в сетевой big-endian. Принимая пакет — обратно. Для этого существуют стандартные функции преобразования:

ФункцияРасшифровкаДействие
htonlhost to network long32-битное: из порядка хоста в сетевой
ntohlnetwork to host long32-битное: из сетевого в порядок хоста
htonshost to network short16-битное: из порядка хоста в сетевой
ntohsnetwork to host short16-битное: из сетевого в порядок хоста

На big-endian машине эти функции ничего не делают (порядок хоста совпадает с сетевым). На little-endian — переставляют байты. Код, использующий htonl/ntohl, работает корректно на обеих архитектурах — поэтому правило простое: всегда вызывать эти функции при работе с сетевыми данными, даже если «текущая машина big-endian и конвертировать нечего».

Пример: порт HTTP — 80 в десятичной, 0x0050 в hex. На x86 (little-endian) число 0x0050 лежит в памяти как 50 00. Но TCP-заголовок ожидает 00 50 (big-endian). Вызов htons(80) на x86 переставляет байты и возвращает значение, которое в памяти выглядит как 00 50 — именно то, что ожидает сетевой стек. Без вызова htons — порт будет прочитан на другом конце как 0x5000 = 20480.

Файловые форматы. Каждый бинарный формат фиксирует порядок байтов:

  • ELF (формат исполняемых файлов в Linux): заголовок содержит поле e_ident[EI_DATA] — значение 1 = little-endian, 2 = big-endian. Загрузчик знает, как интерпретировать все остальные числовые поля.
  • JPEG — всегда big-endian.
  • BMP — всегда little-endian (формат разработан для Windows на x86).
  • TIFF — первые два байта файла: II (Intel, little-endian) или MM (Motorola, big-endian).

Сериализация. Текстовые форматы (JSON, XML) записывают числа как строки цифр — порядок байтов не играет роли. Бинарные форматы (MessagePack, Protocol Buffers) фиксируют порядок в спецификации: Protocol Buffers использует little-endian для fixed-size полей, MessagePack — big-endian. Но при ручной записи чисел в бинарный файл — «просто скопировать байты из памяти» приведёт к тому, что файл будет нечитаем на машине с другим порядком. Любой бинарный протокол, не фиксирующий порядок байтов, — бомба замедленного действия: работает в тестах (обе стороны на одной архитектуре) и ломается в продакшене (другая сторона — другой процессор).

UTF-16 и BOM. Символ A (U+0041) в UTF-16 — два байта. Big-endian: 00 41. Little-endian: 41 00. BOM (U+FEFF) в начале файла разрешает неоднозначность: FE FF = big-endian, FF FE = little-endian. В UTF-8 каждый символ — поток одиночных байтов, и проблема порядка не возникает — это одно из преимуществ, сделавших UTF-8 доминирующей кодировкой. Подробнее — Кодирование текста.

Как распознать проблему порядка байтов

Типичный симптом: числа «в тысячи раз больше» или «в тысячи раз меньше» ожидаемых. Порт 80 превращается в 20480. Длина пакета 256 (0x0100) становится 1 (0x0001). Значения неслучайны — они получены перестановкой байтов.

Ещё один признак — работает на одной платформе, ломается на другой. Программа корректно общается с сервером на x86, но получает мусор при подключении к сетевому устройству на MIPS (big-endian). Диагностика: снять дамп байтов на обеих сторонах и сравнить порядок — если зеркальный, причина найдена.


На этом серия foundations/ завершается. Путь от переключателя с двумя состояниями оказался длинным: бит и байт — минимальные единицы, из которых строится всё. Целые числа — как из байтов получить отрицательные числа и не сломать сложение. Побитовые операции — прямая работа с отдельными битами. Числа с плавающей точкой — компромисс между диапазоном и точностью для дробей. Кодирование текста — как дать номер каждому символу каждой письменности. И наконец, порядок байтов — соглашение о том, как многобайтовые значения раскладываются в памяти.

Всё это — фундамент, на который опираются процессоры, сетевые протоколы, языки программирования и базы данных.

Sources


Кодирование текста