Отражает ли структура структуры подструктуры?

Недавно мы обнаружили код, представленный на нашей кодовой базе, в следующих строках:

#pragma pack(push,1)
struct xyzzy {
    BITMAPINFOHEADER header;
    char plugh;
    long twisty;
} myVar;

Мой вопрос: упаковка распространяется только на непосредственную структуру или может повлиять на упаковку BITMAPINFOHEADER. Я не вижу, чтобы последний случай был очень полезным, поскольку он мог бы отличить структуру от того, что вы получили бы от вызовов Windows API, например. В качестве примера предположим, что структура:

typedef struct {
    char aChar;
    DWORD biSize;
} BITMAPINFOHEADER;

Эта структура будет значительно отличаться от упаковки одной, а не стандартной по умолчанию для Windows (32-разрядная в любом случае, может быть шестнадцать для 64-разрядных).

Является ли BITMAPINFOHEADER "защищенным" от упаковки в силу того, что он почти наверняка будет объявлен ранее? Если бы это было объявлено как часть внешней декларации, тогда она была бы подвергнута упаковке?

Ответ 1

Из соответствующей документации:

pack вступает в силу при первом объявлении struct, union или class после просмотра прагмы. pack не влияет на определения.

header - это определение члена, поэтому на него не влияет.

он был объявлен как часть внешней декларации, тогда он будет подпадать под упаковку?

Да, так как это будет объявление struct.

Кроме того, как отмечает Lightness Races in Orbit в комментарии, более убедительная формулировка может быть найдена непосредственно перед:

Чтобы упаковать класс, нужно поместить его элементы непосредственно друг в друга в памяти.

т.е. он ничего не говорит о том, что содержат те члены, которые могут быть данными и/или дополнением. Тот факт, что упакованность (как описано выше) прикреплена к типу, как представляется, усиливает это


Тем не менее, документация достаточно расплывчата, поэтому лучше проверить, что эта интерпретация верна; f.i; } %0Aint+*p_u(struct Bar_Packed *b) {%0A+++ return %26b->f.i; } %0Aint+*p_p(struct Bar_PackedPacked *b) {%0A+++ return %26b->f.i; }'),l:'5',n:'0',o:'C source #1',t:'0')),k:46.182846371347786,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cg72,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'0'),lang:___c,libs:!(),options:'-O3 -m32',source:1),l:'5',n:'0',o:'x86-64 gcc 7.2+(Editor+#1,+Compiler+#1)+C',t:'0')),k:38.08114379571297,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:output,i:(compiler:1,editor:1),l:'5',n:'0',o:'#1 with x86-64 gcc 7.2',t:'0')),k:15.736009832939242,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">gcc и f.i; } %0Aint+*p_u(struct Bar_Packed *b) {%0A+++ return %26b->f.i; } %0Aint+*p_p(struct Bar_PackedPacked *b) {%0A+++ return %26b->f.i; }'),l:'5',n:'0',o:'C++ source #1',t:'0')),k:46.182846371347786,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cl19_32,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'0'),lang:c++,libs:!(),options: /O2,source:1),l:'5',n:'0',o:'x86 MSVC+19 2017 RTW (Editor+#1,+Compiler+#1)+C++',t:'0')),k:38.08114379571297,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:output,i:(compiler:1,editor:1),l:'5',n:'0',o:'#1 with x86 MSVC+19 2017 RTW',t:'0')),k:15.736009832939242,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">VC++ ведут себя так, как ожидалось. Не то чтобы я был особенно удивлен - что-то другое могло бы разрушить хаос в системе типов (при указании на элемент упакованной структуры фактически будет указатель на нечто иное, чем его тип говорит 1).

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


Заметки

  1. Честно говоря, это немного x86-ориентированное представление; машины с более высокими требованиями к выравниванию будут возражать против того, что даже указатели на правильные, но несогласованные структуры не соответствуют кошерности: хотя смещения полей относительно данного указателя верны, они не являются на самом деле указателями, которые можно использовать так, как они есть.

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

Ответ 2

С должным уважением, я не уверен, достаточно ли я хорош, чтобы ответить на вопрос, заданный 578K (сейчас 656k, ошеломляющий) представителем. Но из того, что я видел, это относится только к непосредственной структуре.

Посмотрите на фрагмент ниже:

#include<stdio.h>

struct /*__attribute__((__packed__))*/ struct_Inner {
    char a;
    int b;
    char c;
};

struct __attribute__((__packed__)) struct_Outer {
    char a;
    int b;
    char c;
    struct struct_Inner stInner;
};

int main() 
{
   struct struct_Inner oInner;
   struct struct_Outer oOuter;
   printf("\n%d Bytes", sizeof(oInner));
   printf("\n%d Bytes", sizeof(oOuter));

   return 0;
}

Отпечатки,

12 Bytes
18 Bytes

Когда я упаковываю struct_Inner он печатает,

6 Bytes
12 Bytes

Этот код был скомпилирован с GCC-7.2.0.

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

Так,

Защищен ли BITMAPINFOHEADER от упаковки в силу того факта, что он почти наверняка будет объявлен ранее?

Я думаю да. Это будет полностью зависеть от способа объявления BITMAPINFOHEADER.

Ответ 3

Предполагая, что GCC (или Clang emulating GCC) вы можете найти некоторую релевантную информацию в Structure Layout Pragmas, где говорится, что наличие push сохраняет текущее состояние упаковки в стек состояний:

Для совместимости с компиляторами Microsoft Windows GCC поддерживает набор директив #pragma которые изменяют максимальное выравнивание элементов структур (кроме битовых полей нулевой ширины), объединений и классов, определенных впоследствии. Минимальное значение n всегда должно быть малой мощностью в два и указывает новое выравнивание в байтах.

  1. #pragma pack(n) просто устанавливает новое выравнивание.
  2. #pragma pack() устанавливает выравнивание по отношению к тому, которое было в действительности при запуске компиляции (см. также параметр командной строки -fpack-struct[=n] см. Параметры кода Gen).
  3. #pragma pack(push[,n]) подталкивает текущую настройку выравнивания во внутреннем стеке, а затем дополнительно устанавливает новое выравнивание.
  4. #pragma pack(pop) восстанавливает настройку выравнивания на том, что хранится в верхней части внутреннего стека (и удаляет эту запись стека). Обратите внимание, что #pragma pack([n]) не влияет на этот внутренний стек; таким образом, возможно иметь #pragma pack(push) за которым следуют несколько экземпляров #pragma pack(n) и завершаться одним #pragma pack(pop).

Таким образом, #pragma в добавленном коде также влияет на все последующие определения структуры, пока не будет отменена #pragma pack(pop). Я был бы обеспокоен этим.

В документации не говорится, что произойдет, если вы делаете #pragma pack(pop) когда во внутреннем стеке нет состояния. Скорее всего, он возвращается к настройке при запуске компиляции.

Ответ 4

Согласно ссылке GCC:

В следующем примере struct my_packed_struct тесно связаны друг с другом, но внутренний макет его члена не упакован - для этого необходимо, чтобы struct my_unpacked_struct также была упакована.

  struct my_unpacked_struct
   {
      char c;
      int i;
   };

  struct my_packed_struct __attribute__ ((__packed__))
    {
       char c;
       int  i;
       struct my_unpacked_struct s;
    };

Вы можете указать этот атрибут только для определения enum, struct или union, а не для typedef, который также не определяет перечисляемый тип, структуру или объединение.

Ответ 5

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

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

Рассматривать:

//Header A.h
typedef struct {
    char aChar;
    DWORD biSize;
} BITMAPINFOHEADER;


// File A.c
#include <A.h>

void doStuffToHeader(BITMAPINFOHEADER* h)
{
    // compute stuff based on data stored in h
    // ...
}


// File B.c
#include <A.h>

#pragma pack(push,1)
struct xyzzy {
    BITMAPINFOHEADER header;
    char plugh;
    long twisty;
} myVar;

void foo()
{
    doStuffToHeader(&myVar.header);
}

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

  • Прозрачно распакуйте подструктуру во временный вызов функции и повторно упакуйте результат позже.
  • Внутренне изменить тип поля заголовка в xyzzy на то, что указывает на то, что он теперь упакован и несовместим с обычным BITMAPINFOHEADER.

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