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

Овладейте навыками создания кода, не зависящего от порядка байтов

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

Харша С. Адига, инженер-программист, IBM

Харша С. Адига (Harsha S. Adiga) работает в IBM Software Group в Бангалоре, Индия и активно участвует в различных рабочих группах и сообществах, занимающихся Linux и программным обеспечением с открытым исходным кодом.



05.12.2007

Введение

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

Порядок байтов

Этот термин означает, как хранит и использует байты система: порядок от старшего к младшему (запись начинается со старшего байта и заканчивается младшим, big endian), порядок от младшего к старшему (запись информации начинается с младшего и заканчивается старшим, little endian).

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

Хранение байтов в памяти

Мы будем оперировать 32-мя битами, т.е. четырьмя байтами. Целые числа или числа с плавающей точкой с обычной точностью записываются в 32 битах. Но поскольку каждый адрес в памяти может хранить только один байт, а не четыре, то 32-х битное число надо разбить на 4 байта. Например, предположим, что есть 32-х битное число, записанное как 12345678, которое является шестнадцатеричным. Поскольку каждая шестнадцатеричная цифра представляется четырьмя байтами, то необходимо восемь шестнадцатеричных чисел для представления рассматриваемого 32-х битного значения. Четыре байта это: 12, 34, 56, и 78. Есть два способа хранить это в памяти, как показано ниже.

  • От старшего к младшему: cтарший байт хранится в младшем адресе памяти, как показано ниже:

    Таблица 1. Хранение байтов от старшего к младшему (Big-endian)
    АдресЗначение
    100012
    100134
    100256
    100378
  • От младшего к старшему: ссылка на младший байт в младшем адресе памяти, как показано ниже:

    Таблица 2. Хранение байтов от младшего к старшему (little-endian)
    АдресЗначение
    100078
    100156
    100234
    100312

Обратите внимание на предыдущую таблицу - числа находятся в обратном порядке. Для запоминания порядков полезно следующее правило: младший байт записывается первым (порядок "от младшего к старшему" - 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)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=273329
ArticleTitle=Создание кода, независящего от порядка байтов, на языке C
publish-date=12052007