(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

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

Харша С. Адига, инженер-программист, 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 <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 и других типов, но рассмотрение этих вопросов выходит за рамки данной статьи.

Заключение

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

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



 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 09.12.2007 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Quest Software. TOAD Xpert Edition
ABBYY Lingvo x6 Английская Домашняя версия, электронный ключ
TeeGrid VCL/FMX Source Code single license
JIRA Software Commercial (Cloud) Standard 10 Users
Microsoft 365 Apps for business (corporate)
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Программирование в AutoCAD
СУБД Oracle "с нуля"
Новые материалы
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100