Поиск по этому блогу

26 сентября 2012

История о неверном использовании pragma pack

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

В частности, в нем было много объявлений структур вроде такой:

#pragma pack(1)
struct header
{
...
};

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

Я сказал okay и, доверившись опыту прошлых разработчиков, принял на веру такой синтаксис.

Некоторое время все было хорошо, но в какой-то момент проект буквально посыпался.

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

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

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

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

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

В выходные решил, то дальше так нельзя, и решил довольно радикально изменить некоторые элементы архитектуры, что и сделал в понедельник. Победа! Ошибка ушла, и проект стал развиваться дальше.

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

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

И действительно, что за дурацкий синтаксис упаковки структур? Как компилятор будет интерпретировать pragma pack например перед объявлением функции, да и вообще, мне как-то с самого начала показалось странным использовать для упаковки структур нечто, начинающееся с шарпа/диеза -- ведь кажется есть __attribute__ для структур? И почему прежние разработчики писали так, и проект нормально работал?

Но больше ошибке скрываться было просто негде, и я полез в документацию на тему pragma pack. И вот оно: эта директива действует не только на следующую за ней структуру, но и вообще до конца файла, а так как в проекте она была написана в хедере, то вообще на весь транслируемый модуль. Таким образом, в одном модуле этот хедер подключался, а в другом нет (либо подключался выше объявления структуры):

модуль1.cpp:
#include "header.h" // объявление структуры header
// ...
header h;
// ...
func( h ); // реализация func -- в модуль2.cpp

модуль2.cpp:
#include "pragma.h" // хедер, содержащий "#pragma pack(1)"
#include "header.h" // объявление структуры header
// ...
void func( header& ) { ... }

Правильным решением (с использованием pragma pack) является такое:
#pragma pack(push, 1)
struct header
{
...
};
#pragma pack(pop)

В начале мы сохраняем (push) текущее значение опции упаковывать/не_упаковывать, затем включаем упаковку (1), и в конце восстанавливаем старое значение (pop).

Подобная ситуация описана здесь, но там все еще хуже: pragma pack(1) было написано в хедере сторонней библиотеки.

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

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

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

Второе. Ошибка компилятора -- это последнее что можно предполагать.

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

Желаю всем Совершенного кода!

Комментариев нет:

Отправить комментарий