Байты и биты: порядок байтов (big-endian и little-endian)

Байты и биты: порядок байтов (big-endian и little-endian) Полезное

Порядок байтов (endianness) — это соглашение о том, в какой последовательности байты многобайтового числа лежат в памяти или передаются по сети. Есть два основных варианта: big-endian (старший байт первым) и little-endian (младший байт первым). От выбора зависит, как одно и то же число выглядит в дампе памяти и как его читать на другой машине.

Ниже разберу главное: чем big-endian отличается от little-endian, где какой порядок принят (сеть — big-endian, процессоры x86 — little-endian), как определить порядок прямо в коде и зачем нужны функции htons/ntohl. Весь код я прогнал и сверил вывод с текстом.

Бит, байт и откуда берется порядок байтов

Начнем с единиц. Бит (binary digit) — минимальная единица информации, принимает значение 0 или 1. Байт — группа из 8 битов, поэтому один байт хранит числа от 0 до 255.

Пока число помещается в один байт, никакого порядка байтов нет: байт один, спорить не о чем. Порядок появляется, когда число занимает несколько байтов — например, 4-байтовое целое uint32_t.

Возьмем число 0x12345678 (шестнадцатеричная запись, каждые две цифры — один байт). В нем четыре байта: 12, 34, 56, 78. Байт 12 — самый старший (most significant byte, MSB), байт 78 — самый младший (least significant byte, LSB). Вопрос порядка байтов — какой из них записать в память первым.

Big-endian и little-endian: в чем разница

Различие только в том, с какого байта — старшего или младшего — начинается запись.

Big-endian (прямой порядок) — первым идет старший байт (MSB). Число 0x12345678 ложится в память как 12 34 56 78, то есть как мы привыкли писать числа слева направо.

Little-endian (обратный порядок) — первым идет младший байт (LSB). То же число ложится как 78 56 34 12 — «задом наперед» относительно привычной записи.

Признак Big-endian Little-endian
Первый байт в памяти старший (MSB) младший (LSB)
0x12345678 в памяти 12 34 56 78 78 56 34 12
Другое название network byte order, Motorola Intel byte order
Читается человеком как обычная запись числа в обратном порядке

Названия пришли из «Путешествий Гулливера»: там спорили, с какого конца разбивать яйцо — с тупого (big end) или острого (little end). В 1980 году Дэнни Коэн перенес эту метафору на спор о порядке байтов в статье «On Holy Wars and a Plea for Peace».

У little-endian есть практическое удобство: младший байт всегда лежит по младшему адресу. Поэтому при чтении числа как более короткого типа (например, взять младший байт от int) адрес не меняется — берем первый байт, и это работает независимо от того, 2-, 4- или 8-байтовое число.

Где какой порядок встречается

Однозначно «правильного» порядка нет — это соглашение. Но по областям он распределен вполне устойчиво.

Контекст Порядок
Сеть (TCP/IP, заголовки пакетов) big-endian (network byte order)
Процессоры x86 и x86-64 (Intel, AMD) little-endian
ARM (Android, iOS, большинство Linux) обычно little-endian
Классические SPARC, Motorola 68000, IBM z big-endian
Формат PNG big-endian
Форматы BMP, USB, GUID-заголовок GPT little-endian

Ключевое противоречие для разработчика: сеть работает в big-endian, а массовые процессоры x86 — в little-endian. Значит, при отправке многобайтовых чисел по сети их порядок приходится преобразовывать, иначе принимающая сторона прочитает мусор.

ARM формально bi-endian — порядок можно переключать, — но на практике в Android, iOS и типичных Linux-сборках используется little-endian. Существовал и исторический «смешанный» порядок (middle-endian) в PDP-11, сегодня это экзотика.

Как проверить порядок байтов в коде

Проще всего посмотреть, как число реально разложилось по байтам. В Python это делает метод int.to_bytes, где порядок задается явно:

import sys

n = 0x12345678
print("порядок этой машины:", sys.byteorder)
print("big-endian:   ", n.to_bytes(4, "big").hex(" "))
print("little-endian:", n.to_bytes(4, "little").hex(" "))

Вывод (на процессоре x86/ARM):

порядок этой машины: little
big-endian:    12 34 56 78
little-endian: 78 56 34 12

sys.byteorder возвращает порядок текущей машины ('little' на x86 и типичном ARM). Метод to_bytes(4, "big") раскладывает число в 4 байта в прямом порядке, to_bytes(4, "little") — в обратном. Обратное преобразование — int.from_bytes(data, "big").

В C порядок машины определяют «на живом числе»: кладут в память двухбайтовое число 1 и смотрят, что лежит в первом байте:

#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t x = 0x12345678;
    unsigned char *p = (unsigned char *)&x;
    printf("байты в памяти: %02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);

    uint16_t one = 1;
    if (*(unsigned char *)&one == 1)
        printf("порядок: little-endian\n");
    else
        printf("порядок: big-endian\n");
    return 0;
}

Вывод (на x86/ARM):

байты в памяти: 78 56 34 12
порядок: little-endian

Указатель p смотрит на первый байт числа x. Если первым лежит младший байт (78), машина little-endian. Трюк с uint16_t one = 1: у числа 1 единица — в младшем байте, поэтому первый байт равен 1 только на little-endian.

htons, htonl, ntohs, ntohl: перевод для сети

Чтобы не писать проверку порядка вручную в каждом сетевом приложении, в C есть стандартные функции из <arpa/inet.h>. Они переводят числа между порядком хоста (host) и порядком сети (network, всегда big-endian):

  • htons — host to network short (16 бит);
  • htonl — host to network long (32 бита);
  • ntohs, ntohl — обратно, из сетевого порядка в порядок хоста.
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>

int main(void) {
    uint16_t port = 0x1234;
    uint32_t addr = 0x12345678;
    printf("htons(0x1234)     = 0x%04x\n", htons(port));
    printf("htonl(0x12345678) = 0x%08x\n", htonl(addr));
    printf("ntohs(0x3412)     = 0x%04x\n", ntohs(0x3412));
    return 0;
}

Вывод (на x86/ARM, little-endian):

htons(0x1234)     = 0x3412
htonl(0x12345678) = 0x78563412
ntohs(0x3412)     = 0x1234

Здесь виден смысл функций: на little-endian хосте htons переставляет байты (0x1234 становится 0x3412), приводя число к сетевому big-endian. На big-endian хосте те же функции ничего не меняют — число уже в нужном порядке. Именно поэтому код с htons/htonl переносим: он делает правильное преобразование независимо от архитектуры, и его нужно применять к номерам портов и адресам перед отправкой, а ntohs/ntohl — к принятым данным.

FAQ

Как быстро запомнить, что есть что? Big-endian — «большой конец первым»: старший (самый весомый) байт идет в начало, как в обычной записи числа. Little-endian — «маленький конец первым»: первым лежит младший байт.

Влияет ли порядок байтов на строки и отдельные байты? Нет. Порядок байтов касается только многобайтовых чисел. Массив байтов, строка в однобайтовой кодировке или отдельный char лежат одинаково при любом порядке — переставлять там нечего.

Что такое bi-endian процессор? Это процессор (например, ARM, PowerPC), у которого порядок байтов можно переключить настройкой. На практике конкретная операционная система фиксирует один режим — для ARM в мобильных и десктопных системах это обычно little-endian.

Выводы

  • Порядок байтов (endianness) — соглашение о последовательности байтов многобайтового числа; для одного байта его не существует.
  • Big-endian: первым идет старший байт (0x12345678 -> 12 34 56 78); little-endian: первым младший (78 56 34 12).
  • Сеть (TCP/IP) — big-endian (network byte order), процессоры x86/x86-64 и обычно ARM — little-endian; отсюда необходимость преобразований при передаче по сети.
  • Проверить порядок машины: в Python — sys.byteorder и int.to_bytes; в C — через указатель на первый байт числа.
  • Для сети применяйте htons/htonl к исходящим числам и ntohs/ntohl к входящим — код останется переносимым между архитектурами.

Где это пригодится на практике

Порядок байтов всплывает в сетевом программировании, при разборе бинарных форматов файлов, чтении данных с датчиков и микроконтроллеров, отладке дампов памяти и межплатформенной сериализации. Ошибка с порядком дает не падение, а тихо «перевернутое» число — такие баги ловить сложнее всего, поэтому понимать endianness полезно любому системному разработчику.

Освойте тему на практике

Разобрать работу с памятью, бинарными данными и низкоуровневыми деталями на практике помогает курс Системное программирование: на нем разбирают представление данных, работу с C и взаимодействие с операционной системой.

Освойте тему на практике

Посмотреть, как преподаватели объясняют такие темы, и задать свои вопросы можно на открытых уроках Otus — они бесплатные и проходят регулярно.

Смежные темы: Все о системах счисления в информатике, Сокет: определение и особенности, Протоколы данных: описание и разновидности.

OTUS Журнал