Почему битовая последовательность является проблемой в битовых полях?

Кажется, что любой переносимый код, использующий битовые поля, различает платформы с прямым и младшим порядком байтов. Посмотрите объявление struct iphdr в ядре Linux для примера такого кода. Я не понимаю, почему порядковый номер бит является проблемой вообще.

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

Например, рассмотрим следующее битовое поле:

struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
Здесь, написание d->f2 это просто компактный и читаемый способ сказать (i>>1) & (1<<4 - 1),

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

6 ответов

Решение

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

Неуточненное поведение

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

Поведение, определяемое реализацией

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

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

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" в качестве типа битового поля = поведение, определяемое реализацией, или если вы используете любой другой тип, кроме (unsigned) int = поведение, определяемое реализацией.

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

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

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

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

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

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

ИСО / МЭК 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 а затем сдвигает и маскирует соответствующий диапазон битов. (Ну, это не обязательно, но мы можем притворяться, что это так.)

В случае с прямым порядком байтов раскладка будет выглядеть примерно так (самый важный бит сначала):

AAAABBBB BBBBCCCC

На немного байтовой, макет будет выглядеть так:

BBBBAAAA CCCCBBBB

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

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

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

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

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

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

int16_t s = 4096; // 16-битное число со знаком...

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

fread ((void *) & s, 2, fp); // читаем с диска как бинарный файл...

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

Таким образом, наиболее распространенным использованием порядка байтов является массовая загрузка двоичных чисел, а затем выполнение bswap, если вы не соответствуете. Раньше мы хранили данные на диске с прямым порядком байтов, потому что Intel была странной фигурой и предоставляла высокоскоростные инструкции для обмена байтами. В настоящее время Intel настолько распространена, что часто делает Little Endian по умолчанию и переключается в системе с прямым порядком байтов.

Более медленный, но порядковый нейтральный подход состоит в том, чтобы делать ВСЕ ввода / вывода байтами, то есть:

uint_8 ubyte; int_8 sbyte; int16_t s; // читаем s в порядке байтов

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

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

// Реконструкция

s = убайт | (sByte << 8);

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

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

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

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

EndianTest_t test = 4096;

if (test.b [0] == 12) printf ("Обнаружен Big Endian!\n");

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

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

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

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

Так что имейте в виду ОБА уровни проблемы - байтовый порядок влияет на способность компьютера читать одно скалярное значение, например, число с плавающей запятой, в то время как компилятор (и аргументы сборки) влияют на способность программы читать в статистической структуре.

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

Другие вопросы по тегам