Не так давно я столкнулся с проблемой: структуры, полученные от устройства через 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