Структура памяти макета в C

У меня есть фон С#. Я очень новичок в языке низкого уровня, таком как C.

В С# struct памяти выкладывается компилятором по умолчанию. Компилятор может неупорядоченно переупорядочивать поля данных или добавлять дополнительные биты между полями. Итак, мне пришлось указать какой-то специальный атрибут, чтобы переопределить это поведение для точного макета.

AFAIK, C не переупорядочивает и не выравнивает структуру памяти struct по умолчанию. Однако я услышал там небольшое исключение, которое очень трудно найти.

Что такое поведение макета памяти C? Что нужно переупорядочить/выровнять, а что нет?

Ответ 1

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

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

Возможно, каждый удаленный современный компилятор реализует #pragma pack, который позволяет контролировать заполнение и оставляет его программисту в соответствии с ABI. (Это строго нестандартно.)

Из C99 §6.7.2.1:

12 Каждый член небитового поля структура или объект объединения выровнены в соответствии с реализацией соответствующий его типу.

13 Внутри объект структуры, небитовое поле членов и подразделений, в которых битовые поля имеют адреса, которые увеличение порядка, в котором они объявлены. Указатель на структуру объект, соответствующим образом преобразованный, указывает на его первоначальный член (или если этот член это бит-поле, а затем единица в который он проживает), и наоборот. Внутри объект структуры, но не начало.

Ответ 2

Это зависит от реализации, но на практике правило (при отсутствии пакета #pragma pack и т.п.):

  • Члены структуры хранятся в порядке их объявления. (Это требуется стандартом C99, как упоминалось здесь ранее.)
  • При необходимости перед каждым элементом структуры добавляется отступ, чтобы обеспечить правильное выравнивание.
  • Каждый примитивный тип T требует выравнивания sizeof(T) байтов.

Итак, учитывая следующую структуру:

struct ST
{
   char ch1;
   short s;
   char ch2;
   long long ll;
   int i;
};
  • ch1 со смещением 0
  • байт заполнения вставляется для выравнивания...
  • s по смещению 2
  • ch2 по смещению 4, сразу после s
  • 3 байта заполнения вставляются для выравнивания...
  • ll по смещению 8
  • i по смещению 16, сразу после
  • В конце добавляются 4 байта заполнения, так что общая структура кратна 8 байтам. Я проверил это в 64-битной системе: 32-битные системы могут позволять структурам иметь 4-байтовое выравнивание.

Таким образом, sizeof(ST) составляет 24.

Это может быть уменьшено до 16 байтов, переставляя элементы, чтобы избежать заполнения:

struct ST
{
   long long ll; // @ 0
   int i;        // @ 8
   short s;      // @ 12
   char ch1;     // @ 14
   char ch2;     // @ 15
} ST;

Ответ 3

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

Из статьи в Википедии:

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

От 6.54.8 Структурно-упаковочные прагмы документации GCC:

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

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

Некоторые цели, например i386 и powerpc, поддерживают ms_struct #pragma которая представляет структуру в виде документированного __attribute__ ((ms_struct)).

  1. #pragma ms_struct on включает макет для объявленных структур.
  2. #pragma ms_struct off отключает макет для объявленных структур.
  3. #pragma ms_struct reset возвращается к макету по умолчанию.

Ответ 4

В C структуры выложены почти точно так, как вы указываете в коде. Аналогично С# StructLayout.Sequential.

Единственное различие заключается в выравнивании элементов. Это никогда не переупорядочивает элементы данных в структуре, но может изменять размер структуры, вставляя "pad" байты в середине структуры. Причина этого заключается в том, чтобы убедиться, что каждый из членов начинается на границе (обычно 4 или 8 байтов).

Например:

struct mystruct {
   int a;
   short int b;
   char c;
};

Размер этой структуры обычно составляет 12 байтов (по 4 для каждого члена). Это связано с тем, что большинство компиляторов по умолчанию делают каждый член того же самого размера, что и самый большой в структуре. Таким образом, char займет 4 байта вместо одного. Но очень важно отметить, что sizeof (mystruct:: c) будет по-прежнему на 1, но sizeof (mystruct) будет 12.

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

Метод изменения этого поведения очень зависит от компилятора, в языке нет ничего, указывающего, как это должно обрабатываться. В MSVC вы должны использовать #pragma pack(1), чтобы отключить выравнивание (1 говорит, выравнивает все по 1 байт-границам). В GCC вы должны использовать __attribute__((packed)) в определении структуры. Обратитесь к документации для вашего компилятора, чтобы узнать, что она делает по умолчанию и как изменить это поведение.