Навигация
Главная »  IBM 

Создание кода, независящего от порядка байтов, на языке C (исходники)


Источник: IBM developerWorks Россия
Харша С. Адига, инженер-программист, IBM

Введение

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

 
Порядок байтов
Этот термин означает, как хранит и использует байты система: порядок от старшего к младшему (запись начинается со старшего байта и заканчивается младшим, 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  #include   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  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 и других типов, но рассмотрение этих вопросов выходит за рамки данной статьи.

Заключение

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

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



 

 Создание кода, независящего от порядка байтов, на языке C (исходники).
 IBM остается мировым лидером в сегменте управления предприятием.
 Telelogic поможет оптимизировать процесс разработки.
 Приглашаем на cеминар по продуктам компании IBM Rational.
 IBM представила новые "облачные" сервисы для коллективной работы.


Главная »  IBM 

© 2018 Team.Furia.Ru.
Частичное копирование материалов разрешено.