Почему бит endianness является проблемой в bitfields?


любой портативный код, который использует битовые поля, похоже, различает платформы с малым и большим концом. Смотрите объявление структуры iphdr в ядре linux для примера такого кода. Я не понимаю, почему бит endianness является проблемой вообще.

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

например, рассмотрим следующее поле: Вот, пишу d->f2 is просто компактный и читаемый способ сказать (i>>1) & (1<<4 - 1).

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

6 51

6 ответов:

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

неуказанному поведению

  • выравнивание адресуемого блока памяти, выделенного для хранения битового поля (6.7.2.1).

реализация-определено поведение

  • может ли битовое поле пересекать границу единицы хранения (6.7.2.1).
  • порядок распределения битовых полей внутри единицы измерения (6.7.2.1).

Big / little endian, конечно, также определяется реализацией. Это означает, что ваша структура может быть выделена следующими способами (предполагая 16 бит ints):

PADDING : 8
f1 : 1
f2 : 3
f3 : 4

or

PADDING : 8
f3 : 4
f2 : 3
f1 : 1

or

f1 : 1
f2 : 3
f3 : 4
PADDING : 8

or

f3 : 4
f2 : 3
f1 : 1
PADDING : 8

какой из них применяется? Сделайте предположение или прочитайте подробную внутреннюю документацию вашего компилятора. Добавьте сложность из 32-разрядных целых чисел, в большую или обратным порядком байтов, в этом. Затем добавьте тот факт, что компилятору разрешено добавлять любое количество заполнений байт в любом месте внутри вашего битового поля, потому что он рассматривается как структура (он не может добавить заполнение в самом начале структуры, но везде).

и тогда я даже не упомянул, что произойдет, если вы используете простой " int " как тип битового поля = поведение, определяемое реализацией, или если вы используете любой другой тип, кроме (без знака) int = поведение, определяемое реализацией.

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

единственным переносимым решением является использование битовых операторов вместо битовых полей. Сгенерированный машинный код будет точно такой же, но детерминированным. Битовые операторы на 100% переносимы на любом компиляторе C для любой системы.

насколько я понимаю, разряды носят исключительно создает компилятор

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

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

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

ISO / IEC 9899: 6.7.2.1 / 10

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

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

см. Также EXP11-C. Не применяйте операторы, ожидающие один тип к данным несовместимого типа.

доступ к битовому полю осуществляется с точки зрения операций над базовым типом. В Примере,unsigned int. Так что если у вас есть что-то типа:

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};

при доступе к полю b, компилятор обращается к целой unsigned int а затем сдвигает и маскирует соответствующий диапазон битов. (Ну, это не обязательно, но мы можем притвориться, что он делает.)

на большом endian, макет будет что-то вроде этого (самый значительный бит первый):

AAAABBBB BBBBCCCC

на little endian, макет будет выглядеть так:

BBBBAAAA CCCCBBBB

если вы хотите получить доступ к макету big endian из little endian или наоборот, вам придется выполнить дополнительную работу. Это увеличение переносимости приводит к снижению производительности, и поскольку макет структуры уже непереносим, разработчики языка пошли с более быстрой версией.

это делает много предположений. Также обратите внимание, что sizeof(struct x) == 4 на большинстве платформ.

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

чтобы отразить наиболее заметные моменты: Если вы используете это на одной платформе компилятора/HW в качестве только программного обеспечения, то endianness не будет проблемой. Если вы используете код или данные на нескольких платформах или должны соответствовать аппаратным битовым макетам, то это и проблема. И много профессиональное программное обеспечение является кросс-платформенным, поэтому он должен заботиться.

вот простейший пример: у меня есть код, который хранит числа в двоичном формате на диск. Если я не записываю и не читаю эти данные на диск сам явно байт за байтом, то это не будет то же самое значение, если читать из противоположной системы endian.

конкретный пример:

int16_t s = 4096; / / знаковое 16-разрядное число...

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

fread ((void*)&s, 2, fp); // чтение его с диска в двоичном виде...

Здесь Я считывайте его как 16-битное значение, а не как явные байты. Это означает, что если моя система соответствует endianness, хранящейся на диске, я получаю 4096, а если нет, я получаю 16 !!!!!

поэтому наиболее распространенным использованием endianness является массовая загрузка двоичных чисел, а затем выполнение bswap, если вы не совпадаете. В прошлом мы хранили данные на диске как big endian, потому что Intel был нечетным человеком и предоставлял высокоскоростные инструкции для обмена байтами. Сегодня корпорация Intel является настолько распространенной, что часто делают с обратным порядком байтов по умолчанию и своп, когда на большой системе с прямым порядком байтов.

более медленный, но эндианский нейтральный подход заключается в выполнении всех операций ввода-вывода байтами, т. е.:

uint_8 ubyte; int_8 типа sbyte; int16_t с; // читаем в нейтральных обратный путь

// давайте выберем little endian в качестве выбранного нами порядка байтов:

fread ((void*)&ubyte, 1, fp); / / читать только 1 байт за раз fread ((void*)&sbyte, 1, fp); / / читать только 1 байт за раз

/ / реконструировать s

s = ubyte | (sByte

обратите внимание, что это идентично коду, который вы бы написали, чтобы сделать endian swap, но вам больше не нужно проверять endianness. И вы можете использовать макросы, чтобы сделать это менее болезненно.

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

вот классический тест:

typedef union { uint_16 s; uint_8 b[2];} EndianTest_t;

EndianTest_t test = 4096;

Если (тест.b[0] = = 12) printf("обнаружен большой Эндиан!\n");

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