 | Уровень сложности: средний Харша С. Адига, инженер-программист, IBM
05.12.2007 Архитектуры, процессоры, сетевые стеки и протоколы различных типов зависят от порядка в машинном коде, которым они пользуются. Эта статья объясняет влияние порядка байтов на программный код и как писать такой код, который будет независим от порядка байтов.
Введение
Для усвоения концепции порядка байтов (см. endianness), не обязательно разбираться в логических схемах организации памяти - нужно только понимать, что память представляет собой один большой массив. Массив содержит байты. Программисты используют адрес для указания на местоположение требуемого массива в памяти.
 |
Порядок байтов
Этот термин означает, как хранит и использует байты система: порядок от старшего к младшему (запись начинается со старшего байта и заканчивается младшим, big endian), порядок от младшего к старшему (запись информации начинается с младшего и заканчивается старшим, little endian).
|
|
Каждый адрес указывает на один элемент массива. Каждый элемент является обычно одним байтом. В некоторых конфигурациях памяти адрес хранит другую информацию кроме байта, однако такая реализация сейчас встречается достаточно редко, поэтому будем считать, что все адреса памяти хранят только байты.
Хранение байтов в памяти
Мы будем оперировать 32-мя битами, т.е. четырьмя байтами. Целые числа или числа с плавающей точкой с обычной точностью записываются в 32 битах. Но поскольку каждый адрес в памяти может хранить только один байт, а не четыре, то 32-х битное число надо разбить на 4 байта. Например, предположим, что есть 32-х битное число, записанное как 12345678, которое является шестнадцатеричным. Поскольку каждая шестнадцатеричная цифра представляется четырьмя байтами, то необходимо восемь шестнадцатеричных чисел для представления рассматриваемого 32-х битного значения. Четыре байта это: 12, 34, 56, и 78. Есть два способа хранить это в памяти, как показано ниже.
-
От старшего к младшему: cтарший байт хранится в младшем адресе памяти, как показано ниже:
Таблица 1. Хранение байтов от старшего к младшему (Big-endian)
| Адрес | Значение |
|---|
| 1000 | 12 | | 1001 | 34 | | 1002 | 56 | | 1003 | 78 |
-
От младшего к старшему: ссылка на младший байт в младшем адресе памяти, как показано ниже:
Таблица 2. Хранение байтов от младшего к старшему (little-endian)
| Адрес | Значение |
|---|
| 1000 | 78 | | 1001 | 56 | | 1002 | 34 | | 1003 | 12 |
Обратите внимание на предыдущую таблицу - числа находятся в обратном порядке. Для запоминания порядков полезно следующее правило: младший байт записывается первым (порядок "от младшего к старшему" - little-endian), старший байт записывается первым (порядок "от старшего к младшему" - big-endian).
Регистры и порядок байтов
Порядок байтов имеет значения только тогда, когда нужно разбить многобайтовую последовательность и записать полученные байты в последовательные участки памяти. Однако если имеется 32-х битный регистр, хранящий 32-х битное значение, то нет смысла говорить о порядке размещения байтов в памяти. Регистр не чувствителен к порядку "от старшего к младшему" или "от младшего к старшему"; регистр только хранит 32-х битное значение. Крайне правый бит является младшим, крайне левый бит является старшим.
Некоторые люди считают, что регистр имеет порядок "от старшего к младшему", потому что он записывает старшие байты в младшие адреса памяти.
Важность порядка байтов
Порядок байтов является атрибутом системы, который определяет, представляются ли целые числа слева-направо или справа-налево. В сегодняшнем мире виртуальных машин и гигагерцевых процессоров многие программисты задумываются, почему они должны учитывать вопрос, рассматриваемый в этой статье? К сожалению, необходимо учитывать порядок байтов каждый раз при разработке аппаратного или программного обеспечения. И, в общем-то, нет универсального правила о том, какой порядок байтов использовать.
Все процессоры должны быть разработаны с учетом либо порядка "от младшего к старшему" или "от старшего к младшему". Например, процессоры Intel® семейства 80x86 и их клоны используют "от младшего к старшему", в то время как процессоры Sun SPARC, Motorola 68K, и PowerPC® используют порядок "от старшего к младшему".
Почему порядок байт так важен? Представьте, что нужно записывать целочисленные значения в файл, и послать этот файл компьютеру, который использует противоположный порядок чтения байтов; этот компьютер прочтет записанное вам в файл значение неверно. Это происходит из-за разных порядков размещения байтов в памяти; в этом случае надо читать значение задом наперед для его корректности.
Порядок записи байт в память представляет собой большую проблему при пересылке через сеть. Еще раз повторю, что если послать число, созданное в одном порядке записи байтов, на компьютер, использующий противоположный порядок записи, то возникнет проблема. Ситуация усложняется еще больше, если неизвестен порядок записи байтов на удаленной машине.
Пример 1 показывает, к чему приводит программирование без учета различий в направлении записи байтов.
Пример 1. Программирование без учета различий в направлении записи байтов
#include <stdio.h>
#include <string.h>
int main (int argc, char* argv[]) {
FILE* fp;
/* Our example data structure */
struct {
char one[4];
int two;
char three[4];
} data;
/* Fill our structure with data */
strcpy (data.one, "foo");
data.two = 0x01234567;
strcpy (data.three, "bar");
/* Write it to a file */
fp = fopen ("output", "wb");
if (fp) {
fwrite (&data, sizeof (data), 1, fp);
fclose (fp);
}
}
|
Приведенный выше код корректно скомпилируется на любом компьютере. Однако выводимый результат будет разным из-за различных порядков записи байтов в память. Вывод программы после просмотра его утилитой hexdump, будет таким, как в примерах 2 и 3.
Пример 2. Вывод команды hexdump -C на компьютере с порядком "от старшего к меньшему"
00000000 66 6f 6f 00 12 34 56 78 62 61 72 00 |foo..4Vxbar.|
0000000c
|
Пример 3. Вывод команды hexdump -C с порядком записи "от младшего к старшему"
00000000 66 6f 6f 00 78 56 34 12 62 61 72 00 |foo.xV4.bar.|
0000000c
|
Влияние порядка хранения байт в памяти на программный код
Различие в порядке байтов далеко не всегда влияет на результат операций. Если выполняются побитовые операции или операции побитового сдвига на целочисленных значениях, то не надо учитывать порядок байтов в памяти. Компьютер упорядочивает многочисленные байты таким образом, что младший байт всегда остается самым младшим, старший байт всегда является старшим.
Естественно в таком случае задаться вопросом, могут ли строки сохраняться в каком-либо необычном порядке, характером для конкретного компьютера. Для ответа на этот вопрос, вернемся к основам организации массивов. Строка в языке C представляет собой массив символов. Каждый символ требует одного байта памяти, при условии, что символы отображаются в кодировке ASCII. В массиве адрес последовательных элементов возрастает. Таким образом, адрес &arr[i] меньше чем адрес &arr[i+1]. Хотя это и не является очевидным, если что-либо хранится в ячейках памяти, адреса которых последовательно увеличиваются, оно будет записано в файл в такой же возрастающей последовательности. При записи в файл обычно задается адрес в памяти и число байтов, которое нужно записать в файл, начиная с этого адреса.
Предположим, есть строка man. Будем считать, что m хранится по адресу 1000, a по адресу 1001, и n по адресу 1002. Символ конца строки \0 хранится по адресу 1003. Следуя из того, что строки в С являются массивами символов, к ним применяются правила работы с символами. В отличие от типов int или long, можно легко различать отдельные байты в строке. Индексация массивов используется для обеспечения доступа к байтам (символам) строки, нельзя просто обращаться к отдельным байтам типов int или long - для этого надо использовать указатели. Отдельные байты в int или long в большей или меньшей степени скрыты от программиста.
Теперь представим, что эту строку нужно записывать в файл при помощи метода типа write(). Ставим указатель на m и задаем число байтов, которое нужно записать в файл (в данном случае четыре). Метод write() обрабатывает байт за байтом и записывает их в файл начиная с m и заканчивая символом конца строки.
Итак, мы доказали, что порядок байтов не влияет на представление строк в языке С.
Порядок байтов имеет значение, когда вы используете приведение типов, которое зависит от конкретного порядка байтов. Пример такой ситуации показан в примере 4, но учтите, что существует много различных приведений типов, которые могут вызвать проблемы.
Пример 4. Принудительное задание порядка байтов
unsigned char endian[2] = {1, 0};
short x;
x = *(short *) endian;
|
Каким будет значение x? Давайте взглянем, что делает этот код. Мы создали двухбайтный массив, а затем преобразовали его к типу short. Используя массив, мы фактически принуждаем систему к использованию какого-либо конкретного порядка байтов; давайте посмотрим, как система обработает эти два байта.
В случае, если используется подход "от младшего к старшему", 0 и 1 интерпретируются задом наперед и будут представлены как 0,1. Так как старший байт 0, а младший байт 1, значение x будет равным 1.
С другой стороны, в системе с порядком "от старшего к младшему" старшим байтом будет 1 и значение переменной x будет равным 256.
Определения порядка байтов во время выполнения программы
Один из способов определить порядок байтов в системе - это проверить расположение в памяти предопределенной константы. Напомним, что размещение единицы (1) типа целочисленного 32-х битного числа будет следующим - 00 00 00 01 для порядка "от старшего к младшему" и 01 00 00 00 для порядка "от младшего к старшему". Взглянув на первый байт этой константы, можно указать порядок байтов у конкретной платформы, и затем предпринять наиболее эффективное в данной ситуации действие.
Пример 5 проверяет первый байт целого числа i для того, чтобы определить, является оно 0 или 1. Если оно равно 1, текущая платформа использует режим байтов в памяти "от младшего к старшему", а если 0 - то режим "от старшего к младшему".
Пример 5. Определение порядка байтов
const int i = 1;
#define is_bigendian() ( (*(char*)&i) == 0 )
int main(void) {
int val;
char *ptr;
ptr = (char*) &val;
val = 0x12345678;
if (is_bigendian()) {
printf( %X.%X.%X.%X\n", u.c[0], u.c[1], u.c[2], u.c[3]);
} else {
printf( %X.%X.%X.%X\n", u.c[3], u.c[2], u.c[1], u.c[0]);
}
exit(0);
}
|
Другой способ определить порядок байтов состоит в использовании символьных указателей на байты в числе типа int и затем проверить первый байт - является он 0 или 1. Пример 6 иллюстрирует этот способ.
Пример 6. Символьные указатели
#define LITTLE_ENDIAN 0
#define BIG_ENDIAN 1
int endian() {
int i = 1;
char *p = (char *)&i;
if (p[0] == 1)
return LITTLE_ENDIAN;
else
return BIG_ENDIAN;
}
|
Сетевой порядок байтов
Сетевые стеки и протоколы также должны определять свою последовательность байтов, иначе два узла сети с разным порядком байтов просто не смогут взаимодействовать. Это наиболее яркий пример влияния порядка байтов на программы. Все уровни протокола TCP/IP работают в режиме "от старшего к младшему". Любое 16-ти или 32-х битное значение внутри заголовков различных уровней (такое как IP-адрес, длина пакета, контрольная сумма) должны отсылаться и получаться так, чтобы старший байт был первым.
Порядок байтов "от старшего к младшему", используемый в протоколе TCP/IP, иногда еще называют сетевым порядком байтов. Даже если компьютеры в сети используют порядок "от младшего к старшему", многобайтные целочисленные значения для передачи их по сети должны быть преобразованы в сетевой порядок байтов, а затем еще раз преобразованы назад в порядок "от младшего к старшему" на принимающем компьютере.
Предположим, что нужно установить TCP-соединение с компьютером, чей IP-адрес равен 192.0.1.2. IPv4 использует уникальное 32-х битное целое число для идентификации каждого компьютера в сети. Разделенный точками IP-адрес должен быть представлен в качестве целочисленного значения.
Например, ПК на базе 80x86 связывается с сервером на базе SPARC через Интернет. Без каких-либо дополнительных действий со стороны пользователя процессор 80x86 может преобразовать 192.0.1.2 в целочисленное число с последовательностью байтов "от младшего к старшему", равное 0x020100C0, и передать байты в последовательности 02 01 00 C0. SPARC получит байты в порядке 02 01 00 C0, переведет байты в порядок "от старшего к младшему" 0x020100c0, и неправильно прочтет IP-адрес 2.1.0.192.
Если стек работает на процессоре, использующем порядок байтов "от младшего к старшему", он должен переупорядочить во время выполнения байты каждого многобайтного значения из заголовков уровней протокола TCP/IP. Если стек работает в режиме байтов "от большего к меньшему", то нет повода для беспокойств. Чтобы стек обладал переносимостью (работал на процессоре обоих типов), он должен уметь принимать решение о необходимости совершения переупорядочивания байтов во время компиляции.
Для осуществления подобного переупорядочивания сокеты предоставляют набор макросов для конвертирования байтов согласно последовательности хранения их на хосте и конвертированию байтов, при передачи их с хоста, в сетевой порядок байтов, как показано ниже.
-
htons()
- Переупорядочивает байты 16-ти битового беззнакового значения из порядка, используемого в текущем процессоре, в сетевой порядок байтов. Название макроса можно расшифровать как "host to network short" ("порядок в беззнаковом коротком числе преобразовать в сетевой порядок байтов").
-
htonl()
- Переупорядочивает байты 32-х битного беззнакового значения из порядка, используемого в текущем процессоре, в сетевой порядок байтов. Название макроса можно расшифровать как "host to network long" ("порядок в беззнаковом длинном числе преобразовать в сетевой порядок байтов").
-
ntohs()
- Переупорядочивает байты 16-ти битного беззнакового значения из сетевого порядка байтов в порядок байтов, используемый на текущем процессоре. Название макроса можно расшифровать как "network to host short" ("из сетевого порядка в порядок для используемого процессора, 16-ти битное число").
-
ntohl()
- Переупорядочивает байты 32-х байтного беззнакового значения из сетевого порядка в порядок байтов, используемый на текущем процессоре. Название макроса может быть расшифровано как "network to host long" ("из сетевого порядка в порядок для используемого процессора, 32-ти битное число").
Рассмотрим программу из примера 7.
Пример 7. Программа на языке С
#include <stdio.h>
main() {
int i;
long x = 0x112A380; /* Value to play with */
unsigned char *ptr = (char *) &x; /* Byte pointer */
/* Observe value in host byte order */
printf("x in hex: %x\n", x);
printf("x by bytes: ");
for (i=0; i < sizeof(long); i++)
printf("%x\t", ptr[i]);
printf("\n");
/* Observe value in network byte order */
x = htonl(x);
printf("\nAfter htonl()\n");
printf("x in hex: %x\n", x);
printf("x by bytes: ");
for (i=0; i < sizeof(long); i++)
printf("%x\t", ptr[i]);
printf("\n");
}
|
Эта программа показывает, как хранится переменная x типа long, хранящее значение 112A380 (шестнадцатеричное).
Когда эта программа выполняется на процессоре, использующем порядок байтов "от младшего к старшему", она выводит информацию как в примере 8.
Пример 8. Результат работы программы на процессоре с режимом "от младшего к старшему"
x in hex: 112a380
x by bytes: 80 a3 12 1
After htonl()
x in hex: 80a31201
x by bytes: 1 12 a3 80
|
Если посмотреть на отдельные байты x, то видно, что младший байт (0x80) находится по меньшему адресу. После этого вызвать htonl( ) для конвертирования в сетевой порядок байтов, то в результате старший байт (0x1) окажется по меньшему адресу. Естественно, что если распечатать значение переменной x после изменения ее порядка байтов, то получится бессмысленное число.
Пример 9 показывает результаты работы той же программы на процессоре с режимом "от старшего к младшему".
Пример 9. Результат работы программы на процессоре с режимом "от старшего к младшему"
x in hex: 112a380
x by bytes: 1 12 a3 80
After htonl()
x in hex: 112a380
x by bytes: 1 12 a3 80
|
Здесь видно, что самый старший байт (0x1) записан под меньшим адресом. Вызов htonl() для конвертирования в сетевой порядок не изменит ничего потому, что порядок байтов "от старшего к младшему".
Изменение на обратный порядка байтов
Теперь напишем программу, результаты работы которой не зависят от порядка байтов. Необходимо убедиться, что данные в файле записаны в верном порядке при записи в этот файл или чтении из него данных. Также следует избегать использования флагов условной компиляции и предусмотреть в коде программы возможностью автоматического определения порядок байтов, используемый на конкретном компьютере.
Создадим набор функций, которые автоматически меняет на обратный порядок байтов заданного параметра в зависимости от порядка байтов на компьютере.
Сначала нужно разобраться с параметром s типа short, разделив его два байта, а затем "склеить" их в обратном порядке. Как показано в примере 10 ниже, функция вернет реверсированное значение переменной типа short в случае, если процессор использует порядок байтов "от младшего к старшему". В противном случае функция оставит прежним значение переменной s.
Пример 10. Метод 1: Использование побитового сдвига и склеивания битов
short reverseShort (short s) {
unsigned char c1, c2;
if (is_bigendian()) {
return s;
} else {
c1 = s & 255;
c2 = (s >> 8) & 255;
return (c1 << 8) + c2;
}
}
|
В функции ниже преобразуется переменнаяю типа short, чтобы представить ее как массив символов, а затем создается новый массив, являющийся обратным первому в случае, если процессор использует режим "от младшего к старшему".
Пример 11. Метод 2: Использование указателей на символьный массив
short reverseShort (char *c) {
short s;
char *p = (char *)&s;
if (is_bigendian()) {
p[0] = c[0];
p[1] = c[1];
} else {
p[0] = c[1];
p[1] = c[0];
}
return s;
}
|
Теперь перейдем к типу int.
Пример 12. Метод 1: Использование побитового сдвига и склеивания байтов для типа int
int reverseInt (int i) {
unsigned char c1, c2, c3, c4;
if (is_bigendian()) {
return i;
} else {
c1 = i & 255;
c2 = (i >> 8) & 255;
c3 = (i >> 16) & 255;
c4 = (i >> 24) & 255;
return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}
|
Это в большей или меньшей степени соответствует тому, что мы раньше делали для изменения на обратный порядка для типа short, но для четырех байтов, а не двух.
Пример 13. Метод 2: Использование указателей на символьный массив для типа int
short reverseInt (char *c) {
int i;
char *p = (char *)&i;
if (is_bigendian()) {
p[0] = c[0];
p[1] = c[1];
p[2] = c[2];
p[3] = c[3];
} else {
p[0] = c[3];
p[1] = c[2];
p[2] = c[1];
p[3] = c[0];
}
return i;
}
|
То же самое мы делали для реверсирования переменной типа short, но здесь обрабатывались четыре байта.
Точно также можно менять на обратный порядок байтов для типов float, long, double и других типов, но рассмотрение этих вопросов выходит за рамки данной статьи.
Заключение
По большому счету, нет особого преимущества в использовании того или иного порядка байтов. Оба порядка распространены и используются в архитектуре многих ЭВМ. Процессоры, работающие в режиме "от младшего к старшему" используются на большинстве персональных компьютеров.
Проблемы с порядком байтов не затрагивают переменные, что хранятся только в одном байте, поскольку "байт" считается элементарной единицей хранения информации в памяти. С другой стороны многобайтные переменные зависят от порядка байтов , что следует иметь в виду при создании приложения.
Ресурсы Научиться
Получить продукты и технологии
-
IBM trial software: ознакомительные версии программного обеспечения для разработчиков, которые можно загрузить со страницы сообщества developerWorks.(EN)
Обсудить
Об авторе  | |  | Харша С. Адига (Harsha S. Adiga) работает в IBM Software Group в Бангалоре, Индия и активно участвует в различных рабочих группах и сообществах, занимающихся Linux и программным обеспечением с открытым исходным кодом. |
Выскажите мнение об этой странице
|  |