Порядок следования байт (endianness) Печать
Добавил(а) microsin   

Понятие Byte order, или порядок следования байт (или endianness) относится к очередности размещения в памяти многобайтовых величин (обычно целых чисел и чисел с плавающей точкой; хотя числа с плавающей точкой не используются в Linux kernel, они могут работать в пользовательских программах), как это поддерживается аппаратурой процессора. Соответственно бывает 2 варианта порядка байт - big endian и little endian. Big endian это такой порядок байт, когда самый значимый по значению байт числа (most significant byte) сохранен в памяти первым по порядку (т. е. у него самый маленький абсолютный адрес байта, в сравнении с остальными байтами числа). Соответственно little endian это противоположный порядок байт, когда наименее значимый байт сохраняется в памяти первым.

Чтобы было понятнее, рассмотрим пример. 4-байтное целое число 0x01020304 будет сохранено в памяти системы big endian следующим образом:

Байт0 Байт1 Байт2 Байт3
0x01 0x02 0x03 0x04

Big endian всегда используется для так называемого сетевого порядка байт (network byte order), который применяется при кодировании адресов в сетевых протоколах.

Та же самая величина, которая будет храниться в памяти системы little endian, разместится в противоположном порядке:

Байт0 Байт1 Байт2 Байт3
0x04 0x03 0x02 0x01

Обычно при программировании можно не обращать внимания на endianness, то есть не важно, как процессор сохранит байты чисел в системе - big endian или little endian; ядро CPU просто загружает данные из памяти и сохраняет данные в память, и представляет данные в Вашей программе уже в правильном виде. Однако, когда нужно обмениваться данными с другой системой, обе системы должны учитывать формат хранения данных в памяти (endianness).

Linux kernel может быть либо big endian, либо little endian, в зависимости от архитектуры, в расчете на которую kernel скомпилировано. Ниже в таблице показан endianness для различных типов архитектур процессоров и протоколов.

Big Endian Little Endian Оба варианта
Архитектуры процессоров
AVR32
FR-V
H8300
PA-RISC
S390
Motorola 680x0 PowerPC
SPARC
Alpha
CRIS
Blackfin
Intel 64
IA-32 (x86) MN10300
AT91SAM7
Cortex, STM32
ARM
SuperH (sh)
M32R
MIPS
Xtensa


Протоколы
TCP/IP USB  

Примечание: процессор ARM может быть либо с архитектурой big endian, либо little endian, в зависимости от типа применяемого чипа, однако чаще всего это big endian. Архитектура PowerPC может быть сконфигурирована для работы либо в режиме big endian, либо little endian, но в Linux используется только big endian.

[Почему следует беспокоиться об endianness]

Как уже упоминалось, порядок байт на низком уровне в системе полностью прозрачен для программиста (про endianness можно часто забыть). Однако могут быть проблемы, когда происходит обмен данными с другой системой, поскольку эта другая система может ожидать появления данных в блоке памяти в противоположном порядке.

Например, если заранее нельзя предсказать тип системы на каком-то дальнем окончании сетевого соединения, сетевые протоколы должны заранее определить порядок байт, используемый для хранения многобайтных величин в заголовках пакетов. В этом случае порядок байт называют сетевым порядком байт (network byte order), и для протокола TCP/IP это будет big endian. Таким образом, отправляющая пакеты система конвертирует данные из локального порядка хранения байт в сетевой. После этого принимающая система преобразует данные из сетевого порядка байт в локальный. На практике, когда есть жесткие требования к быстродействию и заранее известно, что локальный порядок байт такой же, как сетевой, операция конверсии отбрасывается в целях оптимизации.

Другой хороший пример - протокол USB, у которого порядок байт для многобайтных величин little endian.

[Как программно определить endianness]

Можно написать простую программу, которая будет определять порядок байт в имеющейся системе.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
union {
   int i;
   char c[sizeof(int)];
} foo;
 
foo.i = 1;
if (foo.c[0] == 1)
   printf("Little endian\n");
else
   printf("Big endian\n");

Строки 1..4 определяют переменную foo, к которой можно обращаться либо как к числу типа int (тип int почти всегда состоит из нескольких байт) или как к массиву символов characters. На строке 6 переменная инициализируется целым значением 1, так что как минимум один байт в многобайтном числе станет равен 1 (наименее значащий байт), а все остальные значащие байты будут нулями. Если байт 0 массива наименее значимый, то он станет равным 1, и это означает, что система little endian. Если байт 0 массива самый значимый байт, то он будет нулем, и значит система big endian.

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

[Идентификаторы типов]

Следующие идентификаторы соответствуют типам u16, u32 и u64, за исключением случаев, когда они определены с поразрядным (bitwise) атрибутом, который вводят для ограничения применения их как целых чисел. Bitwise-атрибут используется утилитой sparse, чтобы гарантировать, что переменная преобразована в локальный тип процессора перед тем, как над переменной выполнятся другие (небезопасные, unsafe) операции.

Следующие типы можно применять для endian-зависимых переменных, после подключения header-файла linux/kernel.h.

__le16
__le32
__le64
 
__be16
__be32
__be64

[Макросы для преобразований]

Имеется множество макросов для преобразования порядка байт, используемого текущим процессором, в порядок либо big, либо little endian. Дополнительно для каждого типа конверсии имеются отдельные макросы для 16-, 32- и 64-разрядных значений. Имена макросов кодируют исходный и целевой порядок байт значения, так что по имени сразу понятно, что каждый макрос делает.

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

Следующие макросы вернут значение после конвертации. Обратите внимание, что заголовочный файл linux/kernel.h является заголовком, который должен быть подключен к файлам исходного кода, где макросы используются, но это не тот файл заголовка, где макросы реально определены.

#include < linux/kernel.h >
 
__u16 le16_to_cpu(const __le16);
__u32 le32_to_cpu(const __le32);
__u64 le64_to_cpu(const __le64);
 
__le16 cpu_to_le16(const __u16);
__le32 cpu_to_le32(const __u32);
__le64 cpu_to_le64(const __u64);
 
__u16 be16_to_cpu(const __be16);
__u32 be32_to_cpu(const __be32);
__u64 be64_to_cpu(const __be64);
 
__be16 cpu_to_be16(const __u16);
__be32 cpu_to_be32(const __u32);
__be64 cpu_to_be64(const __u64);

Следующие макросы такие же, как и предыдущие, отличие только в том, что параметр макроса - это указатель на преобразуемую величину. Обратите внимание, что имена этих макросов такие же, только добавлен суффикс "p" (от слова pointer) в конце каждого имени.

#include < linux/kernel.h >
 
__u16 le16_to_cpup(const __le16 *);
__u32 le32_to_cpup(const __le32 *);
__u64 le64_to_cpup(const __le64 *);
 
__le16 cpu_to_le16p(const __u16 *);
__le32 cpu_to_le32p(const __u32 *);
__le64 cpu_to_le64p(const __u64 *);
 
__u16 be16_to_cpup(const __be16 *);
__u32 be32_to_cpup(const __be32 *);
__u64 be64_to_cpup(const __be64 *);
 
__be16 cpu_to_be16p(const __u16 *);
__be32 cpu_to_be32p(const __u32 *);
__be64 cpu_to_be64p(const __u64 *);

Следующие макросы делают то же самое, что и предыдущие, но здесь место расположения исходной величины и преобразованной величины совпадают. Обратите внимание, что имена этих макросов такие же, только добавлен суффикс "s" (от латинской фразы in situ, что обозначает "в том же месте") в конце каждого имени.

#include < linux/kernel.h >
 
void le16_to_cpus(__u16 *);
void le32_to_cpus(__u32 *);
void le64_to_cpus(__u64 *);
 
void cpu_to_le16s(__u16 *);
void cpu_to_le32s(__u32 *);
void cpu_to_le64s(__u64 *);
 
void be16_to_cpus(__u16 *);
void be32_to_cpus(__u32 *);
void be64_to_cpus(__u64 *);
 
void cpu_to_be16s(__u16 *);
void cpu_to_be32s(__u32 *);
void cpu_to_be64s(__u64 *);

Следующие макросы предоставляют алиасы для имен функций, которые обычно применяются для преобразования порядка байт в коде сетевых приложений. Первые два макроса используются для преобразования из локального в сетевой порядок байт. Остальные два предоставляют обратное преобразование. Буквы "s" и "l" в конце имен в этом случае означают short (16-битное значение) и long (32-битное значение).

#include < linux/kernel.h >
 
#define htons(x) cpu_to_be16(x)
#define htonl(x) cpu_to_be32(x)
#define ntohs(x) be16_to_cpu(x)
#define ntohl(x) be32_to_cpu(x)

Как отмечалось ранее, сетевой порядок байт всегда big endian, и реализация этих макросов гарантирует корректное использование порядка байт сетевым хостом.

[Отличия между шинами BE-32 и BE-8]

Различия между обработкой шин данных Word-Invariant, или BE-32, и Byte-Invariant, или BE-8, следующее (BE означает Big Endian):

● В системе BE-32, Word-Invariant, представление 32-битного доступа по шине к слову (word access) является одинаковым в сравнении с доступом LE (Little Endian) к одному и тому же адресу слова. Однако представление байтового доступа шины (byte access) и доступа к половине слова (half-word access) различается.
● В BE-8, Byte Invariant, система представляет байтовый доступ одинаковым в сравнении с доступом LE к одному и тому же байтовому адресу.

Причем в обоих реализациях big-endian доступа BE-32 и BE-8 самый малый байтовый адрес соответствует самому значимому байту.

Таблица ниже показывает эффект доступа LE, BE-8 и BE-32 на 64-разрядной шине. Базовая форма для доступа к байту столбцов LE и BE-8 одинаковая, и также она одинаковая для доступа к слову столбцов LE и BE-32.

Примечание: в обоих случаях BE-8 и BE-32, доступ к байту по адресу 0 (самый младший адрес в системе) соответствует самому старшему байту доступа к слову, поэтому соответствует описанию big-endian.

Биты
шины
данных
Байтовый доступ
Доступ к половине слова
Доступ к слову
LE BE8 BE32 LE BE8 BE32 LE BE8 BE32
63:56 A7 A7 A4 A6:MS A6:LS A4:MS A4:MS A4:LS A4:MS
55:48 A6 A6 A5 A6:LS A6:MS A4:LS A4:MS-1 A4:LS+1 A4:MS-1
47:40 A5 A5 A6 A4:MS A4:LS A6:MS A4:LS+1 A4:MS-1 A4:LS+1
39:32 A4 A4 A7 A4:LS A4:MS A6:LS A4:LS A4:MS A4:LS
31:24 A3 A3 A0 A2:MS A2:LS A0:MS A0:MS A0:LS A0:MS
23:16 A2 A2 A1 A2:LS A2:MS A0:LS A0:MS-1 A0:LS+1 A0:MS-1
15:8 A1 A1 A2 A0:MS A0:LS A2:MS A0:LS+1 A0:MS-1 A0:LS+1
7:0 A0 A0 A3 A0:LS A0:MS A2:LS A0:LS A0:MS A0:LS

Пояснения к таблице:

A< Num > Доступ к байту к address[2:0] = Num.
A< Num >:< Byte > Байт < Byte > доступа к слову / половине слова к address[2:0]=Num.
< Byte > : MS Самый значимый байт.
MS-1 Следующий по значимости байт.
LS+1 Следующий по минимуму значимости байт.
LS Наименее значимый байт.

[Ссылки]

1. Byte Order site:bruceblinn.com.
2Макросы для реверсирования порядка байт.
3. Differences between BE-32 and BE-8 buses site:developer.arm.com.