Порядок байтов
Предпосылки: Двоичная система и байты, Целые числа.
Число 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. Принимая пакет — обратно. Для этого существуют стандартные функции преобразования:
| Функция | Расшифровка | Действие |
|---|---|---|
htonl | host to network long | 32-битное: из порядка хоста в сетевой |
ntohl | network to host long | 32-битное: из сетевого в порядок хоста |
htons | host to network short | 16-битное: из порядка хоста в сетевой |
ntohs | network to host short | 16-битное: из сетевого в порядок хоста |
На 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
- Cohen, D., 1980, On Holy Wars and a Plea for Peace. USC/ISI. https://www.ietf.org/rfc/ien/ien137.txt
- RFC 1700, 1994, Assigned Numbers. https://www.rfc-editor.org/rfc/rfc1700
- Bryant, R., O’Hallaron, D., 2015, Computer Systems: A Programmer’s Perspective. 3rd ed. Pearson. Chapter 2.
- Petzold, C., 2000, Code: The Hidden Language of Computer Hardware and Software. Microsoft Press.