Почему в структурах есть дыры
У каждого скалярного типа есть выравнивание: в большинстве ABI 4-байтовый int должен лежать по адресу, кратному 4, а 8-байтовый double или указатель — по адресу, кратному 8. Компилятор вставляет заполнение перед членом, чтобы достичь его выравнивания, и в конце — чтобы в массивах структур каждый элемент оставался выровненным (выравнивание самой структуры равно выравниванию её самого требовательного члена). Поэтому struct { char c; int i; } занимает 8 байт, а не 5, а struct { char a; long b; char c; } — 24 байта, тогда как { long b; char a; char c; } — 16. Упорядочивание членов от наибольшего выравнивания к наименьшему убирает большую часть заполнения; именно это делает предложение переупорядочивания.
Чем различаются ABI
| Тип | x86-64 SysV | AArch64 | i386 | ARM32 | MSVC x64 |
|---|---|---|---|---|---|
| long | 8 | 8 | 4 | 4 | 4 |
| pointer | 8 | 8 | 4 | 4 | 8 |
| double | 8, выравнивание 8 | 8, выравнивание 8 | 8, выравнивание 4 | 8, выравнивание 8 | 8, выравнивание 8 |
| long long | выравнивание 8 | выравнивание 8 | выравнивание 4 | выравнивание 8 | выравнивание 8 |
| long double | 16, выравнивание 16 | 16, выравнивание 16 | 12, выравнивание 4 | 8 | 8 |
| wchar_t | 4 | 4 | 4 | 4 | 2 |
| правила битовых полей | Itanium: поле размещается со следующего бита, если только оно не пересекает окно в sizeof(type) байт; безымянные поля не влияют на выравнивание (x86) или влияют (ARM) | каждая серия полей одного размера получает собственную единицу хранения; смена типа начинает новую единицу | |||
Один и тот же исходный код может поэтому давать разную двоичную раскладку на разных целевых платформах, и именно поэтому структуры, уходящие в сеть или в файлы, должны использовать типы фиксированной ширины, явные члены-заполнители и static_assert(sizeof(struct x) == 24).
Битовые поля
Соседние битовые поля упаковываются в единицу хранения своего объявленного типа. В GCC и Clang поле размещается со следующего свободного бита, если только оно не пересекает окно в sizeof(type) байт — тогда оно переносится на следующую границу выравнивания; поле нулевой ширины (int :0) заставляет следующее поле начать новую единицу. MSVC выделяет новую единицу при каждой смене размера типа, поэтому int a:8; char b:8; занимает 5 байт в MSVC и 2 в GCC. Порядок битов внутри единицы следует порядку байтов целевой платформы (младшие биты первыми в little-endian), поэтому битовые поля — плохой способ описывать сетевые форматы.
packed, aligned и #pragma pack
__attribute__((packed)) убирает всё заполнение (члены могут оказаться невыровненными, что медленно на x86 и вызывает сбой в некоторых конфигурациях ARM). #pragma pack(n) ограничивает выравнивание членов n байтами. __attribute__((aligned(n))) и _Alignas(n) повышают выравнивание, обычно до 16 для SIMD или 64 для строки кэша. Всё это отображается в раскладке.
Вопросы
Насколько это точно?
Движок протестирован по clang -fdump-record-layouts для x86-64, i386, AArch64, ARM32 и MSVC x64 на наборе структур с битовыми полями, упаковкой, вложенными объединениями и полями нулевой ширины. GCC и Clang на этих ABI согласуются друг с другом. Экзотические случаи (типы с повышенным выравниванием в битовых полях, __int128 на 32 битах, векторные типы) проверяйте компилятором.
Как получить реальные значения от моего компилятора?
clang -Xclang -fdump-record-layouts -c file.c печатает каждую структуру; GCC: скомпилируйте с -g и запустите pahole на объектном файле либо используйте offsetof() и sizeof() в небольшой программе. Добавьте static_assert(offsetof(struct s, x) == 8), чтобы зафиксировать раскладку.
Пишет, что тип неизвестен.
Типы из системных заголовков (size_t, uint32_t, time_t и семейство DWORD в Windows) встроены; для всего остального нужно вставить его typedef выше структуры, которая его использует. Неизвестное имя считается 4-байтовым int и помечается.
Можно ли декодировать структуру из core dump или пакета?
Вставьте байты в раздел декодирования. Целые, float, char, указатели, массивы char и битовые поля показываются в выбранном порядке байтов; вложенные структуры перечисляются, но не раскрываются (выберите внутреннюю структуру, чтобы декодировать её отдельно).
Почему переупорядочивание не помогает моей структуре?
Если члены уже отсортированы по выравниванию или в структуре есть битовые поля либо анонимные объединения (они автоматически не переупорядочиваются), предложение скрыто. Заполнение в конце неизбежно, когда выравнивание самого большого члена не делит сумму размеров.