Что pragma pack в себе таит

Сегодня коллега порвал три бубна. Искал страшный и ужасный баг, который приводил к тому, что в отладочной сборке срабатаывал run-time check на heap corruption. Порвав последний бубен, коллега воззвал к нашему гуру отладки, одним взглядом исцеляющему stack overflow и access violation. Гуру пришел со своими бубнами. Когда и у него бубны кончились, они зачем-то стали просить бубен взаймы у меня.

Вкратце, происходило следующее. Есть примерно такой класс

class Response
{
#pragma pack(push, 1)
struct Header
{
int blah;
unsighed short blahblah;
unsigned char vesrion;
}
#pragma pack(pop)
public:
 Response();
 void firstMethod();
 virtual void anotherMethod();
private:
 Header m_header;
 std::vector<char> m_data1;
 std::vector<char> m_data2;
}

Это часть парсера одного абстрактного протокола в вакууме, что объясняет использование #pragma pack(1).

Итак, отладочный рантайм повадился выдавать страшное предупреждение о повреждении кучи при удалении объекта такого класса. Интересно, что в релизе никаких побочных эффектов, характерных для heap corruption, не было (отладочные проверки там, понятное дело, отключены, но хороший расстрел памяти шилом в мешке не упрячешь, он всегда наружу выйдет) .

Ребята раскопали, как работает подобная проверка повреждения кучи. Всем известно, что отладочный рантайм MSVC инициализирует выделенную, но еще неинициализированную память значениями 0xCD. Но вот мне было интересно узнать, что в дополнении к этому, рантайм также расставляет заборы. То есть при выделении памяти под объект размера X рантайм размещает значение 0xFD по смещению X от начала объекта или, другими словами, сразу «за» объектом (тут подробнее). При удалении объекта рантайм проверяет, цел ли забор, то есть сохранилось ли значение 0xFD сразу за удаляемым объектом. Сообщение, выдаваемое в случае, если за объектом обнаружено нечто неожиданное, но не 0xFD, выглядит настолько устрашающе, что при его виде плачут даже лично знакомые с Александреску.

cdcdcdcd cdcdcdcd fdcdcdcd   //Allocated, not initialized
00abed01 6fa91001 fdcdcdcd   // Correctly initialized
00abed01 6fa91001 00cdcdcd   // BANG!

Первая строка наверху — память выделена, но не инициализирована (конструктор объекта еще не выполнился). Следующая строка — память инициализирована, объект сконструирован, замечаний нет. На третьей линии случился какой-то катаклизм и забор (0xFD) оказался продырявлен. Такое может произойти, к примеру, при buffer overrun. При удалении такого объекта отладочный рантайм поднимет вой.

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

Вернемся к нашим баранам. Buffer overrun мы отмели ввиду отсутствия такового (буфера). Для случайного расстрела памяти мина больно прицельно попадала в одну и ту же штакетину забора, поэтому вариант снайпера в соседнем потоке тоже был отметен. Осталось смотреть на создаваемый объект под отладчиком.

После бесчисленных экспериментов удалось установить, что несмотря на то, что sizeof(Response), вызванная в месте создания экземпляра объекта, выдавала 59 байтов, при пошаговой отладке код конструктора объекта ожидал, что объект имеет размер в 60 байтов. Как результат — последний член класса «выезжал» за пределы отведенной под отбъект памяти ровно на 1 байт, чем и рушил забор.

По правде сказать, хорошо поднаторевший в поисках багов Шрёдингера Штирлиц должен был бы насторожиться при виде оператора sizeof, возвращающего 59. Но в нашем случае причина чертовщины оказалась в том, что в разных единицах трансляции размер класса оказался разным! В итоге в месте использования код думал, что имеет дело с объектом размером в 59 байт, в то время как код конструктора полагал, что объект выровнен на границу 4 байт и посему имеет размер 60 байт.

Причина такого поведения оказалась проста. В одном из заголовков #pragma pack(push, 1) перед структурой поставили, но #pragma pack(pop) — забыли. Этот заголовок оказался включен в месте использования, и как результат — в этом *.cpp все структуры оказались выровнены на границу 1 байта. Багфикс заключался в поиске непарной прагмы и восстановлении статус-кво.

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

  1. «Весьма впечатляющий по своей простоте причины при запутанности спецэффектов баг. И что интересно, непонятно, как можно обезопаситься от такой ерунды в случаях, когда использования #pragma pack(1) избежать нельзя.»

    Use warnings, Luke

    http://msdn.microsoft.com/en-us/library/t4d0762d%28v=vs.80%29.aspx

  2. Он ловится при include заголовка, в котором нет парного #pragma pack(pop).
    У меня на тестовом проекте в VS2008 — warning воспроизвёлся.

    «вызванный апгрейдом проекта с 2005 до 2010»

    Я предпочитаю использовать CMake — он и для разных VS проект генерирует, и Makefile для nix’ов.

    • Вот в том-то и прикол, что не воспроизводится. придется проводить вскрытие.

      • На всякий случай: у меня в тестовом проекте два файла — main.cpp и incl.h, main.cpp инклюдит incl.h — то есть один translation unit.
        В incl.h «утекает» pragma pack.

        Просто странно, что не воспроизводится — по идее эта проверка на уровне компилятора должна быть — что там могло отвалится?

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


Примечание - Вы можете использовать эти HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

14 посетителей онлайн
2 гостей, 12 bots, 0 зарегистрированных
Максимум сегодня:: 23 в 12:39 am UTC
В этом месяце: 57 в 04-16-2021 08:12 am UTC
В этом году: 57 в 04-16-2021 08:12 am UTC
За все время: 332 в 11-22-2019 03:23 am UTC