Embedded HandbookApps
All sections › XV. C in depth
STM32F103, STM32F407 · level intermediate · 11 min read
Open in the interactive handbook

126. Data in memory: alignment, packing, bit fields, byte order

A C structure is not a packet on the wire and not a register, however much you want to treat it as both. The compiler inserts empty bytes between the fields, lays out bit fields as it sees fit, and a number lies in memory least significant byte first. Firmware that sends a structure "as is" over UART or reads a packet by casting a pointer works until the compiler, the core or the other side changes. This section shows the layout by experiment and what each approach costs on Cortex-M3 and on M0.

Terms used in the documentation

Term Meaning
alignment the address is a multiple of the size
padding empty bytes between fields
packed structure a structure without padding
unaligned access an access to an address that is not aligned
bit-field a field a few bits wide inside a word
endianness the order of bytes in a number
little-endian / big-endian least significant byte first / most significant first
network byte order the network order — most significant byte first
serialization writing data into a byte stream
byte swap (REV) reversing the order of bytes

126.1 Alignment and padding

The fields type, value, crc in three layouts: with padding — 12 bytes, from large to small — 8, packed — 7

Every type wants to lie at an address that is a multiple of its size: uint16_t — at an even one, uint32_t — at a multiple of four. That is why the compiler inserts empty bytes. The experiment: three layouts of the same fields:

Structure Size, bytes Field offsets
plain: type, value, crc 12 0, 4, 8
ordered: value, crc, type 8 0, 4, 6
wire, packed 7 0, 1, 5

After the one-byte type there are three empty bytes — value starts at 4. At the end there are two more: the size of a structure is a multiple of its strictest alignment, so that in an array the next structure is aligned too. Fields ordered from large to small save a third of the memory — on arrays of hundreds of records that is kilobytes of RAM.


126.2 Padding is real bytes

The empty bytes are not zeros but whatever was in memory before. In the experiment the structure lay in memory filled with 0xAA:

01 AA AA AA 02 00 00 00 03 00 AA AA

Five bytes out of twelve are garbage. Sending the whole structure puts them on the channel, and comparing structures with memcmp gives wrong answers. It is also a data leak: pieces of other variables remain in the padding.


126.3 Packing

__attribute__((packed)) removes the padding: 7 bytes, as in the packet. The price is unaligned fields. What the compiler does with them can be seen in the object file:

uint32_t wire_value(const struct wire *w)
{
    return w->value;
}

The compiler coped on its own because it knew the field is packed. The danger is something else: a pointer cast *(uint32_t *)(buf + 1): the compiler assumes the address is aligned, emits a plain LDR, and on M0 that is a HardFault. On M3/M4 the same cast works, but LDRD and LDM cannot be unaligned even there.

The address of a packed field is just as dangerous: uint32_t *p = &w->value; — from then on the compiler no longer knows that the address may be odd, and on M0 reading *p gives a HardFault. gcc warns about this (-Waddress-of-packed-member); pass the field by value or copy it with memcpy.


126.4 Byte order

Cortex-M and x86 are little-endian: the number 0x12345678 lies in memory as 78 56 34 12. Network protocols, Modbus and many sensors send the most significant byte first: 12 34 56 78. You must not mix them up, and swapping bytes by hand is slow and error-prone.

The reliable way is to build and take apart a number byte by byte, stating the order explicitly; then the code is the same on any machine:

p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;

The fear that this is slow is unfounded: armclang recognised the whole "most significant first" read on Cortex-M3 and turned it into two instructions — LDR and the byte swap REV. On M0 it came out as 11 instructions, but with no risk of unaligned access.

0x12345678: in Cortex-M memory — least significant byte first, on the network and in Modbus — most significant first


126.5 Byte swap

__builtin_bswap32 on all three cores is one REV instruction; in CMSIS the same is __REV(). For 16 bits there is REV16. These functions are needed when the data already lies in an aligned word and the order simply has to be reversed.


126.6 Bit fields

Bit fields look like a ready-made description of a register:

uint32_t enable : 1;
uint32_t mode : 2;
uint32_t prescaler : 5;

gcc, clang and the ARM compilers laid them out from the least significant bit: enable = 1, mode = 2, prescaler = 17 gave the word 0x8D. But the order of the fields, their alignment and the access width are left to the implementation by the standard. For peripheral registers this means:

That is why CMSIS headers describe registers with masks and shifts (section 3а), and bit fields are fit for your own data — packing flags in memory.


126.7 Serializing packets

The rule for any data that leaves the device:

uint32_t v;
memcpy(&v, p, sizeof v);
return v;

memcpy on M3 is one LDR, on M0 it is a byte-by-byte read: the compiler chooses itself, and the choice is always correct.


126.8 The data model in the layout

The field sizes come from the data model (section 122). struct { char c; long l; } is 8 bytes on Cortex-M and 16 on 64-bit Linux and macOS: long there is 8 bytes with an alignment of 8. On 64-bit Windows long stays 4 bytes, and the same structure is 8 bytes again: every system has its own data model. A structure that the device writes to a file or sends to a program on a computer can be read wrongly even with fields from stdint.h: it sets the sizes, but not the alignment and not the byte order (126.4). struct { uint32_t a; uint64_t b; } on Cortex-M is 16 bytes, with b at address 8, and in a 32-bit program under Linux on x86 it is 12: uint64_t is aligned there to 4. Only byte-by-byte writing is reliable (126.7), and _Static_assert catches a mismatch at build time. The file for the board starts with static assertions about the layout:

_Static_assert(sizeof(struct plain) == 12 && offsetof(struct plain, value) == 4, "3 байта заполнения после type");
_Static_assert(sizeof(struct wire) == 7 && offsetof(struct wire, value) == 1, "упакованная: value с адреса 1");

126.9 Alignment of DMA buffers and of the stack


126.10 Pitfalls

HardFault on M0 while parsing a packet. *(uint32_t *)(buf + 1) is an unaligned access. Use memcpy or byte-by-byte parsing.

The receiver sees garbage in the packet. A structure with padding was sent. Serialize the fields byte by byte.

Numbers from a sensor are reversed. The sensor sends the most significant byte first, the device reads least significant first. Byte-by-byte parsing with an explicit order, or REV.

Writing a bit field cleared a flag in the neighbouring bit. The field is written by a read-modify-write of the whole word. For registers, use masks.

A file written by the device cannot be read on the computer. A different data model: long is 4 versus 8 on Linux and macOS, uint64_t in a structure is aligned to 8 versus 4 in a 32-bit program on x86. Fields from stdint.h, byte-by-byte writing, the layout checked with _Static_assert.


126.11 Summary

Full sources

mem126.c — alignment, packing, bit fields and byte order — the experiment on three machines.

/* Раздел 126: данные в памяти — выравнивание, упаковка, битовые поля, порядок байт.
 * Опыт на трёх машинах (gcc и clang x86-64, gcc для ARM в qemu). Печатает размеры,
 * смещения полей, байты в памяти и результат ручной сериализации. */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

struct plain {                 /* как лягут поля по умолчанию */
    uint8_t type;
    uint32_t value;
    uint16_t crc;
};

struct ordered {               /* те же поля, от больших к малым */
    uint32_t value;
    uint16_t crc;
    uint8_t type;
};

struct __attribute__((packed)) wire {    /* без заполнения — как в пакете */
    uint8_t type;
    uint32_t value;
    uint16_t crc;
};

struct with_long {
    char c;
    long l;                    /* long разной ширины у машин */
};

union reg {                    /* битовые поля поверх 32-битного слова */
    uint32_t word;
    struct {
        uint32_t enable : 1;
        uint32_t mode : 2;
        uint32_t prescaler : 5;
        uint32_t : 24;
    } bits;
};

/* Сериализация в порядке «старший байт первым» — одинакова на любой машине. */
static void put_be32(uint8_t *p, uint32_t v)
{
    p[0] = (uint8_t)(v >> 24);
    p[1] = (uint8_t)(v >> 16);
    p[2] = (uint8_t)(v >> 8);
    p[3] = (uint8_t)v;
}

static uint32_t get_le32(const uint8_t *p)
{
    return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}

static void hex(const char *key, const void *mem, size_t n)
{
    const uint8_t *b = mem;
    printf("%s=", key);
    for (size_t i = 0; i < n; i++)
        printf("%s%02X", i ? " " : "", b[i]);
    printf("\n");
}

int main(void)
{
    printf("plain=%zu,%zu,%zu,%zu\n", sizeof(struct plain), offsetof(struct plain, type),
           offsetof(struct plain, value), offsetof(struct plain, crc));
    printf("ordered=%zu,%zu,%zu,%zu\n", sizeof(struct ordered), offsetof(struct ordered, value),
           offsetof(struct ordered, crc), offsetof(struct ordered, type));
    printf("wire=%zu,%zu,%zu,%zu\n", sizeof(struct wire), offsetof(struct wire, type),
           offsetof(struct wire, value), offsetof(struct wire, crc));
    printf("with_long=%zu\n", sizeof(struct with_long));

    uint32_t v = 0x12345678u;
    hex("mem_u32", &v, sizeof v);                 /* порядок байт в памяти */
    uint8_t be[4];
    put_be32(be, v);
    hex("be_u32", be, sizeof be);
    static const uint8_t in[4] = { 0x78, 0x56, 0x34, 0x12 };
    printf("le_read=0x%08X\n", get_le32(in));
    printf("bswap=0x%08X\n", __builtin_bswap32(v));

    union reg r = { 0 };
    r.bits.enable = 1;
    r.bits.mode = 2;
    r.bits.prescaler = 17;
    printf("bitfield_word=0x%08X\n", r.word);    /* порядок полей задаёт реализация */

    struct plain p;
    memset(&p, 0xAA, sizeof p);                   /* заполнение между полями — тоже байты */
    p.type = 1;
    p.value = 2;
    p.crc = 3;
    hex("plain_bytes", &p, sizeof p);
    printf("done=1\n");
    return 0;
}

mem126_arm.c — data layout on Cortex-M: sizes, offsets, a packed structure and the byte swap.

/* Раздел 126: раскладка данных на Cortex-M — armclang для F103, F407 и M0.
 * Статические утверждения — размеры и смещения; функции — что делает компилятор
 * с упакованной структурой и с переворотом байтов. */
#include <stddef.h>
#include <stdint.h>
#include <string.h>

struct plain {
    uint8_t type;
    uint32_t value;
    uint16_t crc;
};

struct __attribute__((packed)) wire {
    uint8_t type;
    uint32_t value;
    uint16_t crc;
};

_Static_assert(sizeof(struct plain) == 12 && offsetof(struct plain, value) == 4, "3 байта заполнения после type");
_Static_assert(sizeof(struct wire) == 7 && offsetof(struct wire, value) == 1, "упакованная: value с адреса 1");
_Static_assert(sizeof(struct { char c; long l; }) == 8, "на Cortex-M long — 4 байта");

/* Поле упакованной структуры — невыровненный адрес. */
uint32_t wire_value(const struct wire *w)
{
    return w->value;
}

/* Надёжный разбор пакета: побайтно, порядок байт задан явно. */
uint32_t get_be32(const uint8_t *p)
{
    return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}

/* Переворот байтов: компилятор знает команду REV. */
uint32_t swap32(uint32_t v)
{
    return __builtin_bswap32(v);
}

/* То же через memcpy — выровнено или нет, компилятор решает сам. */
uint32_t load_u32(const uint8_t *p)
{
    uint32_t v;
    memcpy(&v, p, sizeof v);
    return v;
}

Next: section 127 "The preprocessor as a tool" — X-macros, _Generic and static checks.

Open in the interactive handbook