Embedded HandbookПриложения
Все разделы › XV. Язык C глубже
STM32F103, STM32F407 · уровень средний · 11 мин чтения
Открыть в интерактивном справочнике

126. Данные в памяти: выравнивание, упаковка, битовые поля, порядок байт

Структура в Си — не пакет на линии и не регистр, хотя очень хочется считать её тем и другим. Компилятор вставляет между полями пустые байты, раскладывает битовые поля по своему усмотрению, а число в памяти лежит младшим байтом вперёд. Прошивка, которая отправляет структуру «как есть» по UART или читает пакет приведением указателя, работает, пока не сменится компилятор, ядро или собеседник. Раздел показывает раскладку на опыте и то, во что обходится каждый способ на Cortex-M3 и на M0.

Как это называется в документации

Оригинал Перевод
alignment выравнивание: адрес кратен размеру
padding заполнение: пустые байты между полями
packed structure упакованная структура, без заполнения
unaligned access невыровненный доступ
bit-field битовое поле
endianness порядок байт
little-endian / big-endian младшим байтом вперёд / старшим вперёд
network byte order сетевой порядок — старшим вперёд
serialization сериализация: запись в поток байтов
byte swap (REV) переворот байтов

126.1 Выравнивание и заполнение

Поля type, value, crc в трёх раскладках: с заполнением — 12 байт, от больших к малым — 8, упакованная — 7

Каждый тип хочет лежать по адресу, кратному своему размеру: uint16_t — по чётному, uint32_t — по кратному четырём. Поэтому компилятор вставляет пустые байты. Опыт — три раскладки одних и тех же полей:

Структура Размер, байт Смещения полей
plain: type, value, crc 12 0, 4, 8
ordered: value, crc, type 8 0, 4, 6
wire, упакованная 7 0, 1, 5

После однобайтового type три байта пустые — value начинается с 4. В конце ещё два: размер структуры кратен самому строгому выравниванию, чтобы в массиве следующая структура тоже была выровнена. Поля от больших к малым экономят треть памяти — на массивах из сотен записей это килобайты ОЗУ.


126.2 Заполнение — это настоящие байты

Пустые байты не нули, а то, что было в памяти раньше. В опыте структура лежала на памяти, заполненной 0xAA:

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

Пять байт из двенадцати — мусор. Отправка структуры целиком выдаёт их в канал, а сравнение структур через memcmp врёт. Это ещё и утечка данных: в заполнении остаются куски чужих переменных.


126.3 Упаковка

__attribute__((packed)) убирает заполнение: 7 байт, как в пакете. Цена — невыровненные поля. Что с ними делает компилятор, видно в объектном файле:

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

Компилятор справился сам, потому что знал: поле упаковано. Опасно другое — приведение указателя *(uint32_t *)(buf + 1): компилятор считает адрес выровненным, ставит обычный LDR, и на M0 это HardFault. На M3/M4 то же приведение работает, но LDRD и LDM невыровненными не бывают и там.

Так же опасен адрес упакованного поля: uint32_t *p = &w->value; — дальше компилятор уже не знает, что адрес может быть нечётным, и на M0 чтение *p даёт HardFault. gcc предупреждает об этом (-Waddress-of-packed-member); поле передают по значению или копируют через memcpy.


126.4 Порядок байт

Cortex-M и x86 — little-endian: число 0x12345678 лежит в памяти как 78 56 34 12. Сетевые протоколы, Modbus и многие датчики передают старшим байтом вперёд: 12 34 56 78. Путать нельзя, а менять местами руками — долго и с ошибками.

Надёжный способ — собирать и разбирать число побайтно, явно указывая порядок; тогда код одинаков на любой машине:

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

Опасение, что это медленно, напрасно: чтение «старший вперёд» armclang на Cortex-M3 узнал целиком и собрал в две команды — LDR и переворот REV. На M0 вышло 11 команд, но без риска невыровненного доступа.

0x12345678: в памяти Cortex-M — младшим байтом вперёд, в сети и Modbus — старшим


126.5 Переворот байтов

__builtin_bswap32 на всех трёх ядрах — одна команда REV; в CMSIS то же — __REV(). Для 16 бит — REV16. Эти функции нужны, когда данные уже лежат в выровненном слове и порядок надо просто развернуть.


126.6 Битовые поля

Битовые поля выглядят как готовое описание регистра:

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

gcc, clang и компиляторы ARM разложили их от младшего бита: enable = 1, mode = 2, prescaler = 17 дали слово 0x8D. Но порядок полей, их выравнивание и ширину обращения стандарт отдаёт реализации. Для регистров периферии это значит:

Поэтому заголовки CMSIS описывают регистры масками и сдвигами (раздел 3а), а битовые поля годятся для своих данных — упаковать флаги в памяти.


126.7 Сериализация пакетов

Правило для любых данных, покидающих прибор:

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

memcpy на M3 — одна LDR, на M0 — побайтное чтение: компилятор выбирает сам, и это всегда правильно.


126.8 Модель данных в раскладке

Размеры полей — от модели данных (раздел 122). struct { char c; long l; } — 8 байт на Cortex-M и 16 на 64-битных Linux и macOS: long там 8 байт с выравниванием 8. На 64-битной Windows long остаётся 4 байта, и та же структура — снова 8 байт: модель данных у каждой системы своя. Структура, записанная прибором в файл или отправленная программе на компьютере, может прочитаться неверно и с полями из stdint.h: он задаёт размеры, но не выравнивание и не порядок байт (126.4). struct { uint32_t a; uint64_t b; } на Cortex-M — 16 байт, b с адреса 8, а в 32-битной программе под Linux на x86 — 12: uint64_t там выровнен по 4. Надёжна только побайтная запись (126.7), а расхождение ловит при сборке _Static_assert. Файл для платы начинается со статических утверждений о раскладке:

_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 Выравнивание буферов DMA и стека


126.10 Грабли

На M0 HardFault при разборе пакета. *(uint32_t *)(buf + 1) — невыровненный доступ. memcpy или побайтный разбор.

Получатель видит мусор в пакете. Отправлена структура с заполнением. Сериализовать поля побайтно.

Числа от датчика перевёрнуты. Датчик отдаёт старшим вперёд, прибор читает младшим. Побайтный разбор с явным порядком или REV.

Запись битового поля сбросила флаг в соседнем бите. Поле пишется чтением-изменением-записью всего слова. Для регистров — маски.

Файл, записанный прибором, не читается на компьютере. Разная модель данных: long 4 против 8 на Linux и macOS, uint64_t в структуре выровнен по 8 против 4 в 32-битной программе на x86. Поля — из stdint.h, запись — побайтная, раскладка — _Static_assert.


126.11 Итоги

Полные исходники

mem126.c — выравнивание, упаковка, битовые поля и порядок байт — опыт на трёх машинах.

/* Раздел 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 — раскладка данных на Cortex-M: размеры, смещения, упакованная структура и переворот байтов.

/* Раздел 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;
}

Дальше: раздел 127 «Препроцессор как инструмент» — X-макросы, _Generic и статические проверки.

Открыть в интерактивном справочнике