Проблемы упакованных структур

Не так давно я столкнулся с проблемой: структуры, полученные от устройства через USB-VCOM, содержали не те данные, которые я ожидал. Размер структуры в программе на Qt отличался от размера встроенной программы, скомпилированной в Keil uVision. Соответственно, доступ к членам структуры, а особенно к битовым полям оказывался некорректным.

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

Для иллюстрации проблемы рассмотрим несколько структур:

struct A {
    long int a;
    short int b;
    float c;
};
 
struct __attribute__((__packed__)) B {
    long int a;
    short int b;
    float c;
};

Структуры A и B одинаковы, но A будет выровнена компилятором по границам 32 бит. Это значит, что между b и c будет разрыв в 2 байта так, чтобы c имел другой физический адрес. Таким образом A будет на 2 байта длиннее (12 байт), чем сумма всех ее элементов (10 байт). Это выравнивание и размер разрывов может отличаться от компилятора к компилятору и от платформы к платформе.

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

Так называемые упакованные структуры, как структура B, очень часто используются в двоичных потоках данных между компьютерами и микроконтроллерами в последовательных интерфейсах, таких как USB или COM-порты. Из-за того, что каждый компилятор имеет свой собственный синтаксис для упакованных структур и не все компиляторы поддерживают их, их использование является не очень хорошей практикой. Но в сложных или длинных структурах это несравнимо удобно.

Есть еще одна проблема с упакованными структурами: процессоры ARM не могут загружать переменные с плавающей точкой, которые не выровнены по границе 4 байт. Таким образом, в примере структура B не может быть прочитана процессором ARM. Она не выдаст никакой ошибки, но непредсказуемое значение делает ее очень опасной. И нет никакого решения, кроме как изменить исходных код, например, скопировав байты из упакованной структуры во временную переменную типа с плавающей точкой перед использованием.

Возвращаясь к MinGW/gcc, эта проблема может решена добавлением компилятору опции -mno-ms-bitfields. В Qt это можно сделать, добавив в файл проекта QMAKE_CXXFLAGS+= -mno-ms-bitfields.

Более портируемое решение — это использовать директиву #pragma pack(1):

#pragma pack(1)
struct B {
    long int a;
    short int b;
    float c;
}
#pragma pack()

Это дает требуемый результат как в gcc, так и в Visual C.

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

if (sizeof(B) != 10) printf("Error, Pack structs don't work!");

поможет обнаружить неожиданное поведение компилятора и поможет портировать приложение.

Выводы

  • Избегайте использования «упакованных структур» и передачи двоичных данных, которые будут интерпретироваться как структуры, потому что они зависят от платформы и компилятора;
  • Если их использование неизбежно, внимательно тестируйте их поведение во время выполнения;
  • Если приложение будет использоваться на платформе ARM, держите в уме, что числа с плавающей точкой не могут быть доступны напрямую из памяти, не выровненной по границе 4 байт.

За основу взята статья по адресу: Programming: Problems using pack structs