Por qué los structs tienen huecos
Cada tipo escalar tiene una alineación: en la mayoría de las ABI, un int de 4 bytes debe estar en una dirección divisible por 4, un double o un puntero de 8 bytes en una divisible por 8. El compilador inserta relleno antes de un miembro para alcanzar su alineación, y al final para que los arrays del struct mantengan alineado cada elemento (la alineación del struct es la de su miembro más alineado). Por eso struct { char c; int i; } ocupa 8 bytes, no 5, y struct { char a; long b; char c; } ocupa 24 mientras que { long b; char a; char c; } ocupa 16. Ordenar los miembros de mayor a menor alineación elimina la mayor parte del relleno; eso es justo lo que hace la sugerencia de reordenación.
Qué cambia entre ABIs
| Tipo | x86-64 SysV | AArch64 | i386 | ARM32 | MSVC x64 |
|---|---|---|---|---|---|
| long | 8 | 8 | 4 | 4 | 4 |
| pointer | 8 | 8 | 4 | 4 | 8 |
| double | 8, alineación 8 | 8, alineación 8 | 8, alineación 4 | 8, alineación 8 | 8, alineación 8 |
| long long | alineación 8 | alineación 8 | alineación 4 | alineación 8 | alineación 8 |
| long double | 16, alineación 16 | 16, alineación 16 | 12, alineación 4 | 8 | 8 |
| wchar_t | 4 | 4 | 4 | 4 | 2 |
| Reglas de campos de bits | Itanium: un campo se coloca en el siguiente bit salvo que cruce una ventana de sizeof(type) bytes; los campos sin nombre no afectan a la alineación (x86) o sí (ARM) | cada secuencia de campos del mismo tamaño recibe su propia unidad de almacenamiento; un cambio de tipo empieza una nueva | |||
El mismo código fuente puede dar disposiciones binarias distintas en distintos destinos, por eso los structs que viajan por la red o van a archivos deberían usar tipos de anchura fija, miembros de relleno explícitos y static_assert(sizeof(struct x) == 24).
Campos de bits
Los campos de bits adyacentes se empaquetan en la unidad de almacenamiento de su tipo declarado. En GCC y Clang, un campo va al siguiente bit libre salvo que cruzase una ventana de sizeof(type) bytes; entonces salta al siguiente límite de alineación. Un campo de anchura cero (int :0) obliga al siguiente campo a empezar una nueva unidad. MSVC abre una nueva unidad en cuanto cambia el tamaño del tipo, así que int a:8; char b:8; ocupa 5 bytes en MSVC y 2 en GCC. El orden de los bits dentro de la unidad sigue la endianness del destino (bits menos significativos primero en little-endian), y por eso los campos de bits son una mala forma de describir formatos de cable.
packed, aligned y #pragma pack
__attribute__((packed)) elimina todo el relleno (los miembros pueden quedar desalineados, lo que es lento en x86 y provoca un fallo en algunas configuraciones ARM). #pragma pack(n) limita la alineación de los miembros a n bytes. __attribute__((aligned(n))) y _Alignas(n) aumentan la alineación, normalmente a 16 para SIMD o 64 para una línea de caché. Todo eso se muestra en la disposición.
Preguntas
¿Qué precisión tiene?
El motor se prueba contra clang -fdump-record-layouts para x86-64, i386, AArch64, ARM32 y MSVC x64 en una serie de structs con campos de bits, empaquetado, uniones anidadas y campos de anchura cero. GCC y Clang coinciden en esas ABI. Los casos exóticos (tipos sobrealineados en campos de bits, __int128 en 32 bits, tipos vectoriales) compruébalos con el compilador.
¿Cómo obtengo los valores reales de mi compilador?
clang -Xclang -fdump-record-layouts -c archivo.c imprime cada struct; GCC: compila con -g y ejecuta pahole sobre el objeto, o usa offsetof() y sizeof() en un programa pequeño. Con static_assert(offsetof(struct s, x) == 8) congelas una disposición.
Dice que hay un tipo desconocido.
Los tipos de las cabeceras del sistema (size_t, uint32_t, time_t y la familia DWORD de Windows) están integrados; todo lo demás necesita su typedef encima del struct que lo usa. Un nombre desconocido se trata como int de 4 bytes y se marca.
¿Puede decodificar un struct de un volcado de memoria o un paquete?
Pega los bytes en la sección de decodificación. Enteros, floats, chars, punteros, arrays de char y campos de bits se muestran en el orden de bytes elegido; los structs anidados se listan pero no se despliegan (elige el struct interior para decodificarlo aparte).
¿Por qué la reordenación no ayuda con mi struct?
Si los miembros ya están ordenados por alineación o el struct tiene campos de bits o uniones anónimas (que no se reordenan automáticamente), la sugerencia se oculta. El relleno final es inevitable cuando la alineación del miembro mayor no divide la suma de los tamaños.