[{"content":"Знайома ситуація: спроєктували бінарний протокол, усе красиво, struct один-в-один як у специфікації. Шлемо по UART чи CAN, а приймач отримує кашу. Або: працює на Cortex-M4, а на M0+ вилітає в HardFault з \u0026ldquo;ну я ж нічого дивного не робив\u0026rdquo;.\nСпойлер: struct у C - це не \u0026ldquo;група змінних\u0026rdquo;, це конкретний малюнок байтів, і малюємо його не ми, а компілятор. Виводи нижче отримані з GCC 13.3 (x86-64), arm-none-eabi-gcc 13.2.1, pahole 1.25 і rustc 1.84; з іншими версіями деталі asm можуть відрізнятися.\n📝 TL;DR Компілятор вставляє між полями невидимі байти (padding), щоб кожне поле лежало за адресою, кратною його alignof. Порядок полів впливає на розмір. Сортуйте від більшого до меншого й перевіряйте через pahole. packed прибирає padding, але скидає вирівнювання всієї структури до 1: на Cortex-M0 побайтово читаються навіть поля на офсеті 0. packed, aligned(4) частково це лікує. HardFault на M0 дає не сам packed, а код, який \u0026ldquo;забуває\u0026rdquo; про невирівняність: каст uint8_t * =\u0026gt; uint32_t * або взяття адреси packed-поля. packed ≠ \u0026ldquo;готовий формат для дроту\u0026rdquo;: є ще порядок байтів і сміття в padding. Для протоколів серіалізуйте явно. Вирівнювання - питання ABI: double на i386 вирівнюється по 4, на ARM і x86-64 по 8. Вирівнюються навіть аргументи в регістрах. На Cortex-M7 з D-cache DMA-буфер вирівнюють по кеш-лінії (32 байти) і за адресою, і за розміром, інакше cache maintenance зачепить сусідні змінні. C++ додає прихованих байтів (vptr), Rust за замовчуванням сам переставляє поля. Різниця між мовами не у \u0026ldquo;вирівнюванні\u0026rdquo;, а в тому, що кожна з них гарантує. Залізо: навіщо взагалі вирівнювання Пам\u0026rsquo;ять - не нескінченна стрічка байтів, а набір комірок, до яких ходить шина певної ширини.\nОдин банк, 8-бітні комірки. Щоб прочитати 32-бітне число, потрібно 4 цикли: байт =\u0026gt; байт =\u0026gt; байт =\u0026gt; байт, а потім склеїти. Кілька паралельних банків (наприклад, 4 по байту). Якщо 32-бітне значення лежить на адресі, кратній 4, всі чотири банки віддають свій байт одночасно: 1 цикл. Якщо значення \u0026ldquo;розрізане\u0026rdquo; між двома сусідніми словами, потрібні два доступи плюс зсув і злиття. Звідси природне вирівнювання (natural alignment): значення типу T має лежати за адресою, для якої addr % alignof(T) == 0. Для скалярних типів alignof зазвичай дорівнює sizeof, але не завжди: останнє слово за ABI платформи (нижче побачимо, як double на i386 вирівнюється лише по 4). Структура мусить розкласти поля так, щоб ця умова виконувалась для кожного з них.\nНа 32-бітному ядрі вирівняне слово читається за один доступ до шини, а double - за два, бо це два слова. Невирівняний доступ коштує більше: на одних ядрах прихованими додатковими циклами, на інших (Cortex-M0/M0+) - HardFault.\nЕксперимент №1: вгадай sizeof Чотири структури. Спробуйте вгадати розміри на папері, перш ніж дивитись далі:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; typedef struct { uint8_t a; uint16_t b; } a_t; typedef struct { uint16_t a; uint8_t b; uint32_t c; } b_t; typedef struct { uint8_t a; double b; uint32_t c; } c_t; typedef struct { double a; uint32_t b; uint8_t c; } d_t; #define SHOW(T) printf(\u0026#34;%-4s sizeof=%2zu alignof=%zu\\n\u0026#34;, #T, sizeof(T), _Alignof(T)) int main(void) { SHOW(a_t); SHOW(b_t); SHOW(c_t); SHOW(d_t); printf(\u0026#34;c_t offsets: a=%zu b=%zu c=%zu\\n\u0026#34;, offsetof(c_t, a), offsetof(c_t, b), offsetof(c_t, c)); printf(\u0026#34;d_t offsets: a=%zu b=%zu c=%zu\\n\u0026#34;, offsetof(d_t, a), offsetof(d_t, b), offsetof(d_t, c)); return 0; } 1 2 3 4 5 6 7 gcc -std=c11 -Wall sizes.c -o sizes \u0026amp;\u0026amp; ./sizes a_t sizeof= 4 alignof=2 b_t sizeof= 8 alignof=4 c_t sizeof=24 alignof=8 d_t sizeof=16 alignof=8 c_t offsets: a=0 b=8 c=16 d_t offsets: a=0 b=8 c=12 Наївна сума полів проти реальності:\nstruct сума полів реально (x86-64, ARM EABI) alignof a_t 3 4 2 b_t 7 8 4 c_t 13 24 8 d_t 13 16 8 Колонка alignof - ключ до всього: вирівнювання структури дорівнює найбільшому вирівнюванню серед її полів.\na_t і b_t очевидні: між u8 та u16 один байт padding, між u8 і u32 теж.\nА от c_t зазвичай ламає мозок. Рахуємо: a (1 байт), 7 байт padding, b (8 байт, офсет 8), c (4 байти, офсет 16). Закінчуємо на 20. Чому ж 24?\nБо розмір структури мусить бути кратним її вирівнюванню (тут 8, від double). Інакше в масиві c_t arr[2] другий елемент поїде на офсет 20, і його double стане невирівняним. Тож у кінець додається ще 4 байти tail padding. Те саме правило працює рекурсивно для структур усередині структур.\n--- title: \"c_t - 24 байти\" config: packet: bitsPerRow: 8 --- packet-beta 0: \"a\" 1-7: \"_padding_\" 8-15: \"b (double)\" 16-19: \"c\" 20-23: \"_tail_\"d_t каже те саме з іншого боку: ті самі типи, але великий double стоїть першим, тож внутрішніх дірок немає, а хвостовий padding лише 3 байти (13 =\u0026gt; 16).\n--- title: \"d_t = 16 байтів: ті самі поля\" config: packet: bitsPerRow: 8 --- packet-beta 0-7: \"a (double)\" 8-11: \"b\" 12: \"c\" 13-15: \"_tail_\"Порядок полів = економія пам\u0026rsquo;яті, і безкоштовно. Практичне правило: сортуйте поля за спаданням розміру (точніше, за спаданням alignof).\n1 2 struct bad { uint8_t a; uint32_t b; uint16_t c; }; // 1 + 3 pad + 4 + 2 + 2 pad = 12 struct good { uint32_t b; uint16_t c; uint8_t a; }; // 4 + 2 + 1 + 1 pad = 8 Попросити компілятор сказати самому 1 2 3 4 5 6 gcc -Wpadded -c sizes.c -o /dev/null sizes.c:6:39: warning: padding struct to align \u0026#39;b\u0026#39; [-Wpadded] sizes.c:7:51: warning: padding struct to align \u0026#39;c\u0026#39; [-Wpadded] sizes.c:8:39: warning: padding struct to align \u0026#39;b\u0026#39; [-Wpadded] sizes.c:8:16: warning: padding struct size to alignment boundary with 4 bytes [-Wpadded] sizes.c:9:16: warning: padding struct size to alignment boundary with 3 bytes [-Wpadded] Бачимо і внутрішній padding, і хвостовий (4 байти для c_t, 3 для d_t). Для реального проєкту -Wpadded надто балакучий, щоб тримати його ввімкненим постійно, але для разової ревізії структур - те, що треба.\nЕксперимент №2: дивимось через pahole -Wpadded корисний, але найнаочніше дірки показує pahole (пакет dwarves). Він читає DWARF-інформацію з об\u0026rsquo;єктного файлу і малює структуру так, як вона лежить у пам\u0026rsquo;яті. Працює й з об\u0026rsquo;єктниками під ARM.\n1 2 3 4 5 6 #include \u0026lt;stdint.h\u0026gt; typedef struct { uint8_t a; double b; uint32_t c; } c_t; typedef struct { double a; uint32_t b; uint8_t c; } d_t; c_t gc; d_t gd; // хоча б по одній змінній кожного типу, див. граблі №2 1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -g -c sizes_arm.c -o sizes_arm.o 1 2 3 4 5 6 7 8 9 10 11 12 13 14 pahole -C c_t sizes_arm.o typedef struct { uint8_t a; /* 0 1 */ /* XXX 7 bytes hole, try to pack */ double b; /* 8 8 */ uint32_t c; /* 16 4 */ /* size: 24, cachelines: 1, members: 3 */ /* sum members: 13, holes: 1, sum holes: 7 */ /* padding: 4 */ /* last cacheline: 24 bytes */ } c_t; Усе, що ми вирахували вручну, тут написано прямо: діра 7 байт, хвостовий padding 4, 13 корисних байтів у 24. А ось d_t:\n1 2 3 4 5 6 7 8 9 10 pahole -C d_t sizes_arm.o typedef struct { double a; /* 0 8 */ uint32_t b; /* 8 4 */ uint8_t c; /* 12 1 */ /* size: 16, cachelines: 1, members: 3 */ /* padding: 3 */ /* last cacheline: 16 bytes */ } d_t; Жодних XXX hole, лише 3 байти в хвості.\nТри граблі, на які можна наступити 1. Голий pahole file.o може мовчати. Без жодної помилки, exit code 0. Причина: структури в прикладі анонімні (typedef struct { ... } c_t;), а голий виклик друкує лише структури з тегом. Формат DWARF тут ні до чого: з -gdwarf-4 поведінка та сама. Лікується двома способами:\n1 pahole -C c_t file.o # шукати за іменем typedef, або дати структурі тег: typedef struct c_s { ... } c_t; і тоді працює й голий pahole 2. Тип, який ніде не використано, у DWARF не потрапляє. GCC за замовчуванням викидає невикористані типи з debug-інформації. Якщо структура фігурує лише в sizeof, pahole скаже type 'X' not found. Або заведіть хоча б одну змінну цього типу (як gc і gd вище), або попросіть GCC нічого не викидати:\n1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -g -fno-eliminate-unused-debug-types -c file.c -o file.o 3. Потрібен -g. Без debug-інформації pahole немає з чого читати.\nЕксперимент №3: packed і заголовок прошивки Тепер реалістичний кейс: заголовок образу прошивки, який лежить у Flash і формат якого зафіксований байт-у-байт. Цей файл використовуємо в наступних двох експериментах, тож тут він повністю:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; typedef struct { uint32_t magic; uint16_t version; uint8_t flags; uint32_t crc; } fw_header_natural_t; typedef struct __attribute__((packed)) { uint32_t magic; uint16_t version; uint8_t flags; uint32_t crc; } fw_header_packed_t; typedef struct __attribute__((packed, aligned(4))) { uint32_t magic; uint16_t version; uint8_t flags; uint32_t crc; } fw_header_packed_a4_t; _Static_assert(sizeof(fw_header_natural_t) == 12, \u0026#34;natural: 12\u0026#34;); _Static_assert(offsetof(fw_header_natural_t, crc) == 8, \u0026#34;natural crc @ 8\u0026#34;); _Static_assert(sizeof(fw_header_packed_t) == 11, \u0026#34;packed: 11\u0026#34;); _Static_assert(offsetof(fw_header_packed_t, crc) == 7, \u0026#34;packed crc @ 7\u0026#34;); _Static_assert(sizeof(fw_header_packed_a4_t) == 12, \u0026#34;packed+aligned(4): 12\u0026#34;); // Читання невирівняного поля: компілятор ЗНАЄ, що воно packed uint32_t read_crc(const fw_header_packed_t *h) { return h-\u0026gt;crc; } // Вирівняне поле (офсет 0) у packed-структурі з alignof == 1 uint32_t read_magic(const fw_header_packed_t *h) { return h-\u0026gt;magic; } // Те саме, але структура packed + aligned(4) uint32_t read_magic_a4(const fw_header_packed_a4_t *h) { return h-\u0026gt;magic; } uint32_t read_crc_a4(const fw_header_packed_a4_t *h) { return h-\u0026gt;crc; } // Взяття адреси packed-поля: packed-ність губиться uint32_t crc_of(const uint32_t *p); // десь в іншому файлі: return *p; uint32_t read_via_ptr(const fw_header_packed_t *h) { return crc_of(\u0026amp;h-\u0026gt;crc); } У natural-варіанті між flags (офсет 6) і crc компілятор вставляє 1 байт padding, crc стає на офсет 8, розмір 12. У packed padding немає: crc на офсеті 7, розмір 11.\n--- title: \"fw_header_natural_t\" config: packet: bitsPerRow: 4 --- packet-beta 0-3: \"magic\" 4-5: \"version\" 6: \"flags\" 7: \"pad\" 8-11: \"crc\"--- title: \"fw_header_packed_t\" config: packet: bitsPerRow: 4 --- packet-beta 0-3: \"magic\" 4-5: \"version\" 6: \"flags\" 7-10: \"crc\"На другій схемі видно саме той випадок із розділу про банки пам\u0026rsquo;яті: crc починається в одному слові, а закінчується в іншому.\nЗверніть увагу на _Static_assert. Вони компілюються в нуль байтів, але якщо хтось через рік додасть поле чи змінить прапор компілятора, впаде збірка з людським повідомленням, а не прошивка в полі.\npacked виглядає як ідеальне рішення. Тепер про ціну.\nЕксперимент №4: скільки коштує packed Функція read_crc з fw.c читає невирівняне поле:\n1 uint32_t read_crc(const fw_header_packed_t *h) { return h-\u0026gt;crc; } Компілюємо під три конфігурації. У .s немає імен полів C, лише зсуви на кшталт [r0, #7], тому -fverbose-asm додає коментарі, а sed вирізає потрібну функцію:\n1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -S -fverbose-asm fw.c -o fw_m4.s 1 arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O2 -S -fverbose-asm fw.c -o fw_m0.s 1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -S -fverbose-asm -mno-unaligned-access fw.c -o fw_m4_nounal.s 1 sed -n \u0026#39;/^read_crc:/,/^\\s*bx\\s*lr/p\u0026#39; fw_m4.s # і так само для fw_m0.s, fw_m4_nounal.s Cortex-M4 (ARMv7-M, невирівняний доступ підтримується апаратно):\n1 2 3 read_crc: ldr r0, [r0, #7] @ unaligned bx lr Одна інструкція. GCC лише ставить коментар @ unaligned. Ціна прихована: ядро всередині робить кілька доступів до шини замість одного, тож це повільніше, але в асемблері цього не видно.\nCortex-M0 (ARMv6-M, апаратної підтримки немає):\n1 2 3 4 5 6 7 8 9 10 11 12 read_crc: ldrb r2, [r0, #8] ldrb r3, [r0, #7] lsls r2, r2, #8 orrs r2, r3 ldrb r3, [r0, #9] ldrb r0, [r0, #10] lsls r3, r3, #16 orrs r3, r2 lsls r0, r0, #24 orrs r0, r3 bx lr Тут уже видно, що відбувається: чотири ldrb (по байту), три зсуви, три злиття. Десять інструкцій замість однієї.\nCortex-M4 з -mno-unaligned-access: те саме, але з barrel shifter в orr 1 2 3 4 5 6 7 8 9 read_crc: ldrb r3, [r0, #7] ldrb r1, [r0, #8] ldrb r2, [r0, #9] ldrb r0, [r0, #10] orr r3, r3, r1, lsl #8 orr r3, r3, r2, lsl #16 orr r0, r3, r0, lsl #24 bx lr Thumb-2 уміє зсувати другий операнд прямо в orr, тому окремі lsls не потрібні: 7 інструкцій замість 10.\nКлючовий висновок: ціну packed визначає ядро. Компілятор знає, що поле невирівняне (бо ви самі написали packed), і підбирає безпечну послідовність для конкретної архітектури. Тож у цьому випадку HardFault не буде, буде лише повільніше й довше.\nСюрприз: побайтово читається навіть magic packed прибирає не лише padding між полями: він скидає alignof всієї структури до 1. Компілятор більше не може припускати, що сам вказівник h вирівняний, тому на M0 навіть magic на офсеті 0 збирається з байтів:\n1 2 3 4 5 6 7 8 9 10 11 12 read_magic: @ Cortex-M0, fw_header_packed_t ldrb r2, [r0, #1] ldrb r3, [r0] lsls r2, r2, #8 orrs r2, r3 ldrb r3, [r0, #2] ldrb r0, [r0, #3] lsls r3, r3, #16 orrs r3, r2 lsls r0, r0, #24 orrs r0, r3 bx lr Ліки - повернути структурі вирівнювання, не повертаючи padding між полями: __attribute__((packed, aligned(4))) (тип fw_header_packed_a4_t у fw.c). Тепер компілятор знає, що база вирівняна по 4, і поля на \u0026ldquo;чесних\u0026rdquo; офсетах читаються однією інструкцією навіть на M0 (коментарі в asm - мої):\n1 2 3 4 5 6 7 8 9 10 read_magic_a4: @ Cortex-M0 ldr r0, [r0] bx lr read_crc_a4: @ Cortex-M0 ldrb r3, [r0, #7] @ байт 7 окремо ldr r0, [r0, #8] @ слово 8..11 вирівняне lsls r0, r0, #8 @ байт 11 (tail padding) виштовхується orrs r0, r3 bx lr Навіть невирівняний crc подешевшав: компілятор читає вирівняне слово з офсету 8 і доклеює один байт. Легально це саме тому, що байт 11 належить структурі (tail padding).\nфункція M4 M4 -mno-unaligned-access M0 read_crc (packed) 1 7 10 read_magic (packed) 1 7 10 read_magic_a4 (packed + aligned(4)) 1 1 1 read_crc_a4 (packed + aligned(4)) 1 3 4 Кількість інструкцій без bx lr.\n📌 Важливо aligned(4) повертає tail padding: sizeof(fw_header_packed_a4_t) == 12, а не 11. Для заголовка у Flash, який ви самі розмічаєте, це зазвичай прийнятно. Для формату, де після 11-го байта одразу йде payload, - ні. Тому _Static_assert на розмір у fw.c стоїть і для цього типу.\nДе M0 справді падає HardFault на M0 приходить, коли компілятор не знає, що адреса невирівняна. Два типові способи йому \u0026ldquo;не сказати\u0026rdquo;: каст байтового буфера та взяття адреси packed-поля.\nКаст вказівника 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;string.h\u0026gt; // Компілятор НЕ знає, що адреса невирівняна: каст - це обіцянка uint32_t read_cast(const uint8_t *buf) { return *(const uint32_t *)(buf + 1); } // Коректно: memcpy uint32_t read_memcpy(const uint8_t *buf) { uint32_t v; memcpy(\u0026amp;v, buf + 1, sizeof v); return v; } // Коректно і з явним порядком байтів: little-endian з будь-якої адреси uint32_t get_u32le(const uint8_t *p) { return (uint32_t)p[0] | (uint32_t)p[1] \u0026lt;\u0026lt; 8 | (uint32_t)p[2] \u0026lt;\u0026lt; 16 | (uint32_t)p[3] \u0026lt;\u0026lt; 24; } 1 2 3 4 5 6 7 8 9 10 @ Cortex-M0 read_cast: adds r0, r0, #1 ldr r0, [r0] bx lr @ Cortex-M4 (будь-які прапори, навіть -mno-unaligned-access) read_cast: ldr r0, [r0, #1] bx lr Звичайний ldr. Ніякого @ unaligned, ніякої побайтової збірки: для компілятора каст uint8_t * =\u0026gt; uint32_t * означає обіцянку \u0026ldquo;тут точно вирівняно по 4\u0026rdquo; (а за стандартом C ще й порушує strict aliasing). Якщо buf + 1 не кратне 4, то на M0/M0+ це миттєвий HardFault, а на M3/M4/M7 - тихий повільний доступ (поки не ввімкнемо trap, див. нижче).\nЗвідки на M0 окремий adds? У Thumb-1 LDR із immediate-офсетом кодує зсув, кратний 4 (#0..#124), тому [r0, #1] просто не існує як інструкція. Сам набір інструкцій M0 нагадує, що слово мусить бути вирівняне.\nАдреса packed-поля Другий спосіб - передати адресу поля туди, де чекають звичайний вказівник:\n1 2 uint32_t crc_of(const uint32_t *p); // десь в іншому файлі: return *p; uint32_t read_via_ptr(const fw_header_packed_t *h) { return crc_of(\u0026amp;h-\u0026gt;crc); } Усередині crc_of немає жодної інформації про packed: там звичайний ldr r0, [r0], а приходить у нього h + 7. На M0 - HardFault. GCC про це попереджає за замовчуванням:\n1 fw.c:44:68: warning: taking address of packed member of \u0026#39;struct \u0026lt;anonymous\u0026gt;\u0026#39; may result in an unaligned pointer value [-Waddress-of-packed-member] 🚨 Обережно Це найчастіший реальний сценарій: структура packed, а поле передають у CRC- чи parse-функцію за вказівником. Попередження -Waddress-of-packed-member варто тримати як помилку (-Werror=address-of-packed-member).\nЯк читати правильно У cast.c є ще два варіанти. Ось що з них робить компілятор:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ Cortex-M4 read_memcpy: ldr r0, [r0, #1] @ unaligned bx lr get_u32le: ldr r0, [r0] @ unaligned bx lr @ Cortex-M0, -O2 read_memcpy: push {lr} sub sp, sp, #12 adds r1, r0, #1 movs r2, #4 add r0, sp, #4 bl memcpy ldr r0, [sp, #4] add sp, sp, #12 pop {pc} get_u32le: @ 10 інструкцій, як у read_crc ... memcpy коректний скрізь, і на M4 згортається в один ldr. Але на M0 GCC 13 залишив справжній виклик бібліотечної memcpy. get_u32le зі зсувами коректний скрізь, на M4 GCC сам розпізнає патерн і теж згортає його в один ldr, а на M0 дає ту саму побайтову збірку без виклику. І головне: він фіксує порядок байтів, про що memcpy нічого не знає. Для читання з протокольних буферів це найкращий вибір.\nКомпілятор і санітайзер як сторожі -Wcast-align на ARM попереджає про каст uint8_t * =\u0026gt; uint32_t * (cast increases required alignment of target type). На x86 той самий прапор мовчить, бо архітектура терпить невирівняні доступи. Там потрібен -Wcast-align=strict. Якщо бізнес-логіка тестується на хості, -fsanitize=alignment ловить невирівняний доступ у рантаймі, навіть на x86: 1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;assert.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #include \u0026#34;cast.c\u0026#34; // так, це трішки hack-абельно int main(void) { _Alignas(4) const uint8_t buf[8] = { 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0, 0, 0 }; assert(get_u32le(buf + 1) == 0xDEADBEEF); assert(read_memcpy(buf) == 0xDEADBEEF); // на little-endian хості assert(read_cast(buf) == 0xDEADBEEF); // UBSan зупинить тут return 0; } 1 2 gcc -std=c11 -O1 -g -fsanitize=alignment -fno-sanitize-recover=alignment cast_test.c -o cast_test \u0026amp;\u0026amp; ./cast_test cast.c:6:49: runtime error: load of misaligned address 0x7ffe38e3a431 for type \u0026#39;const uint32_t\u0026#39;, which requires 4 byte alignment get_u32le і memcpy проходять, а read_cast зупиняє тест з точним рядком.\nЯк знайти невирівняні доступи на M3/M4/M7 На цих ядрах невирівняний доступ мовчки працює, тобто баги ховаються. Але trap можна ввімкнути:\n1 SCB-\u0026gt;CCR |= SCB_CCR_UNALIGN_TRP_Msk; // CMSIS Тепер кожен невирівняний LDR/STR дасть UsageFault (з прапорцем UNALIGNED в UFSR, а якщо UsageFault-обробник не ввімкнений, ескалюється в HardFault). Це не про \u0026ldquo;повільні місця\u0026rdquo;, а про всі місця, де ви покладаєтесь на апаратну терпимість. Корисно, щоб на M4 відловити код, який \u0026ldquo;випадково\u0026rdquo; зламається при міграції на M0.\nОдин нюанс: з trap\u0026rsquo;ом код із read_crc вище (ldr r0, [r0, #7] @ unaligned) почне падати, бо GCC за замовчуванням вважає невирівняний ldr допустимим. Тож разом із trap\u0026rsquo;ом додавайте -mno-unaligned-access: тоді компілятор для packed-полів сам перейде на побайтове читання. Підсумок: прапор компілятора + trap = чесна перевірка, що код переносимий на M0.\nМалі типи: LDRB, LDRSB, LDRH Байт можна прочитати з будь-якого офсету однією інструкцією, і він одразу опиняється в молодших бітах регістра: нічого зсувати чи маскувати вручну не треба.\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdint.h\u0026gt; typedef struct __attribute__((packed)) { uint8_t pad; // offset 0: лише щоб зсунути наступні поля uint8_t u8; // offset 1 int8_t s8; // offset 2 uint16_t u16; // offset 3: непарний, не кратний 2 } small_t; uint32_t read_u8 (const small_t *s) { return s-\u0026gt;u8; } int32_t read_s8 (const small_t *s) { return s-\u0026gt;s8; } uint32_t read_u16(const small_t *s) { return s-\u0026gt;u16; } 1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -S small_int.c -o small_int_m4.s 1 arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O2 -S small_int.c -o small_int_m0.s 1 2 3 4 5 6 7 8 9 10 @ Cortex-M4 read_u8: ldrb r0, [r0, #1] bx lr read_s8: ldrsb r0, [r0, #2] bx lr read_u16: ldrh r0, [r0, #3] @ unaligned bx lr 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ Cortex-M0 read_u8: ldrb r0, [r0, #1] bx lr read_s8: ldrb r0, [r0, #2] sxtb r0, r0 bx lr read_u16: ldrb r3, [r0, #3] ldrb r0, [r0, #4] lsls r0, r0, #8 orrs r0, r3 bx lr ldrb бере байт з непарного офсету й розширює нулями до 32 біт - на обох ядрах одна інструкція. Для знакового байта на M4 є ldrsb з immediate-офсетом, на M0 такої форми немає (у Thumb-1 LDRSB вміє лише регістровий офсет), тому ldrb + sxtb. uint16_t на офсеті 3 вже невирівняний: на M4 це ldrh з @ unaligned, на M0 - збирання з двох байтів. Порівняйте з архітектурами без байтових завантажень: там довелося б читати ціле слово, зсувати й маскувати \u0026amp; 0xFF. На ARM апаратура бере це на себе.\nPadding - це ще не все: не шліть struct в мережу Ви можете подумати: \u0026ldquo;Ага, packed, 11 байтів, шлю структуру як є\u0026rdquo;. Ні. Подивімося, що реально лежить у пам\u0026rsquo;яті звичайної (не-packed) структури і що має піти в протокол:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef struct { uint32_t magic; uint16_t version; uint8_t flags; uint32_t crc; } fw_header_natural_t; static void put_u32le(uint8_t *p, uint32_t v) { p[0] = v; p[1] = v \u0026gt;\u0026gt; 8; p[2] = v \u0026gt;\u0026gt; 16; p[3] = v \u0026gt;\u0026gt; 24; } static void put_u16le(uint8_t *p, uint16_t v) { p[0] = v; p[1] = v \u0026gt;\u0026gt; 8; } #define HDR_NET_SIZE 11u // розмір із специфікації протоколу, а не sizeof() static size_t hdr_serialize(uint8_t *out, const fw_header_natural_t *h) { put_u32le(out + 0, h-\u0026gt;magic); put_u16le(out + 4, h-\u0026gt;version); out[6] = h-\u0026gt;flags; put_u32le(out + 7, h-\u0026gt;crc); return HDR_NET_SIZE; } static void dump(const char *name, const void *p, size_t n) { printf(\u0026#34;%s\u0026#34;, name); for (size_t i = 0; i \u0026lt; n; i++) printf(\u0026#34;%02x \u0026#34;, ((const uint8_t *)p)[i]); printf(\u0026#34;\\n\u0026#34;); } int main(void) { fw_header_natural_t h; memset(\u0026amp;h, 0xCC, sizeof h); // імітуємо \u0026#34;сміття\u0026#34; на стеку h.magic = 0xA5A50001; h.version = 3; h.flags = 0x80; h.crc = 0xDEADBEEF; uint8_t wire[HDR_NET_SIZE]; hdr_serialize(wire, \u0026amp;h); dump(\u0026#34;у мережі: \u0026#34;, wire, sizeof wire); dump(\u0026#34;сира структура: \u0026#34;, \u0026amp;h, sizeof h); return 0; } 1 2 3 4 gcc -std=c11 -Wall -O2 ser.c -o ser \u0026amp;\u0026amp; ./ser у мережі: 01 00 a5 a5 03 00 80 ef be ad de сира структура: 01 00 a5 a5 03 00 80 cc ef be ad de # ^^ padding memset(..., 0xCC, ...) імітує стек: значення padding-байта стандартом не визначене, тож у реальному коді там буде те, що лежало на стеку до цього. Тобто сира структура в проводі - ще й витік вмісту пам\u0026rsquo;яті. Плюс її порядок байтів диктує архітектура відправника. Cortex-M (на практиці) і x86 - little-endian, але перший же big-endian партнер по мережі зламає все.\nТому правило таке: packed - для форматів, що лежать у Flash і читаються тим самим кодом (заголовки образів, конфіг-блоки). Усе, що йде назовні, серіалізуйте явно: зсувами, як put_u32le/get_u32le, у Rust - to_le_bytes()/from_le_bytes(). Так ви одночасно вирішуєте padding, endianness і вирівнювання.\n⚠️ Попередження packed не для MMIO-регістрів. Регістри периферії мають бути природно вирівняні й читатися своєю шириною, а packed може перетворити 32-бітний доступ на чотири байтові, які регістр не підтримує або на які реагує інакше (наприклад, read-to-clear).\nВирівнювання - питання ABI, а не мови Чому double у структурі вирівнюється по 8, хоча шина 32-бітна? Бо так сказав ABI: менше доступів до шини ціною більшого padding. Перевіримо: той самий sizes.c, але під 32-бітний x86:\n1 2 3 4 5 6 7 gcc -m32 -std=c11 sizes.c -o sizes32 \u0026amp;\u0026amp; ./sizes32 a_t sizeof= 4 alignof=2 b_t sizeof= 8 alignof=4 c_t sizeof=16 alignof=4 # \u0026lt;-- було 24 і 8 d_t sizeof=16 alignof=4 c_t offsets: a=0 b=4 c=12 d_t offsets: a=0 b=8 c=12 На i386 System V double усередині структури вирівнюється лише по 4, тож c_t стискається з 24 до 16 (b на офсеті 4, а не 8). На ARM EABI (AAPCS) та x86-64 вимога 8. Висновок: розмір структури не переноситься між платформами, і це ще одна причина ставити _Static_assert.\nА що з double на Cortex-M4? 1 2 3 4 5 6 7 8 9 10 typedef struct { unsigned char a; double b; } natural_t; typedef struct __attribute__((packed)) { unsigned char a; double b; } packed_t; natural_t gn; packed_t gp; // щоб типи потрапили в DWARF для pahole double read_natural(const natural_t *p) { return p-\u0026gt;b; } double read_packed (const packed_t *p) { return p-\u0026gt;b; } double add (double a, double b) { return a + b; } float addf(float a, float b) { return a + b; } 1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -O2 -S dbl.c -o dbl.s 1 2 3 4 5 6 7 8 9 read_natural: vldr.64 d0, [r0, #8] bx lr read_packed: ldr r2, [r0, #1] @ unaligned ldr r3, [r0, #5] @ unaligned vmov d0, r2, r3 bx lr Вирівняний double вантажиться однією vldr.64. Для невирівняного компілятор не може використати vldr (на невирівняній адресі вона дає UsageFault завжди, див. вище), і навіть ldrd не може. Тож бере два одиночних ldr і перекладає їх у FPU-регістр через vmov.\npahole показує ціну в байтах:\n1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -g -c dbl.c -o dbl.o 1 2 3 4 5 6 7 8 9 10 11 12 pahole -C natural_t dbl.o typedef struct { unsigned char a; /* 0 1 */ /* XXX 7 bytes hole, try to pack */ double b; /* 8 8 */ /* size: 16, cachelines: 1, members: 2 */ /* sum members: 9, holes: 1, sum holes: 7 */ /* last cacheline: 16 bytes */ } natural_t; 1 2 3 4 5 6 7 8 pahole -C packed_t dbl.o typedef struct { unsigned char a; /* 0 1 */ double b; /* 1 8 */ /* size: 9, cachelines: 1, members: 2 */ /* last cacheline: 9 bytes */ } __attribute__((__packed__)) packed_t; 💡 FPU на M4 - одинарної точності fpv4-sp-d16 вміє лише float. Функція add з dbl.c (a + b для double) компілюється у bl __aeabi_dadd, тобто в програмну емуляцію, а addf для float - в одну vadd.f32. Padding навколо double на цьому тлі дрібниця. Якщо точність дозволяє, беріть float.\nРегістри та стек теж вирівнюються Правила вирівнювання діють не лише в структурах. AAPCS передає перші аргументи в r0 - r3, і 64-бітне значення мусить зайняти парну пару регістрів:\n1 2 3 4 5 6 7 #include \u0026lt;stdint.h\u0026gt; uint64_t sink(uint32_t a, uint64_t b); // визначення - в іншому файлі uint64_t call(void) { return sink(0x11111111u, 0x2222222233333333ull); } 1 arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -S args.c -o args.s Компілятор пише константи в десятковому вигляді, коментарі з hex і поясненнями - мої:\n1 2 3 4 5 call: mov r2, #858993459 @ 0x33333333 - молодша половина b mov r3, #572662306 @ 0x22222222 - старша половина b mov r0, #286331153 @ 0x11111111 - a b sink @ r1 не використано: це padding у регістрах a іде в r0, r1 пропускається, b іде в r2:r3. Порядок аргументів має значення так само, як порядок полів: sink(uint64_t b, uint32_t a) вліз би в r0 - r2 і залишив r3 вільним для ще одного аргументу. Якби аргументів не вистачило регістрів, на стеку 64-бітне значення так само лягло б на адресу, кратну 8.\nСам стек теж вирівняний: AAPCS вимагає, щоб SP був кратний 8 на межах публічних функцій, а Cortex-M при вході у виняток вирівнює кадр по 8 (CCR.STKALIGN; на M0 завжди). А от розміщення локальних змінних усередині кадру - справа компілятора: кілька char цілком можуть лежати поруч на непарних адресах, тому покладатися тут ні на що не варто.\nC++: те саме, плюс сюрпризи C++ повністю успадковує правила C, але додає власні джерела прихованого розміру. Усі приклади в одному файлі:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;cstdint\u0026gt; #include \u0026lt;type_traits\u0026gt; struct NoVirt { uint32_t a; }; struct WithVirt { uint32_t a; virtual void f() {} }; struct Empty {}; struct DerivedEmpty { Empty e; uint32_t x; }; // порожнє поле-член struct DerivedEBO : Empty { uint32_t x; }; // порожній базовий клас struct Logger {}; struct Widget { [[no_unique_address]] Logger log; uint32_t value; }; struct alignas(32) Dma { uint8_t b[64]; }; static_assert(std::is_standard_layout_v\u0026lt;NoVirt\u0026gt;); static_assert(!std::is_standard_layout_v\u0026lt;WithVirt\u0026gt;); int main() { WithVirt w; // щоб vtable точно потрапила в бінарник w.f(); std::printf(\u0026#34;NoVirt=%zu WithVirt=%zu\\n\u0026#34;, sizeof(NoVirt), sizeof(WithVirt)); std::printf(\u0026#34;member Empty=%zu base Empty (EBO)=%zu [[no_unique_address]]=%zu\\n\u0026#34;, sizeof(DerivedEmpty), sizeof(DerivedEBO), sizeof(Widget)); std::printf(\u0026#34;Dma align=%zu\\n\u0026#34;, alignof(Dma)); } 1 2 3 4 g++ -std=c++20 lay.cpp -o lay \u0026amp;\u0026amp; ./lay # x86-64 NoVirt=4 WithVirt=16 member Empty=8 base Empty (EBO)=4 [[no_unique_address]]=4 Dma align=32 1 2 3 4 g++ -m32 -std=c++20 lay.cpp -o lay32 \u0026amp;\u0026amp; ./lay32 # 32-бітний режим NoVirt=4 WithVirt=8 member Empty=8 base Empty (EBO)=4 [[no_unique_address]]=4 Dma align=32 1 2 3 nm -C lay | grep vtable # чия vtable потрапила в бінарник 0000000000003d88 V vtable for WithVirt U vtable for __cxxabiv1::__class_type_info@CXXABI_1.3 Що тут видно:\nvptr. Одна virtual-функція, і об\u0026rsquo;єкт розтовстів: на 32-бітному таргеті +4 байти (4 =\u0026gt; 8), на x86-64 аж до 16 через вирівнювання. nm показує, що vtable є лише у WithVirt (друга - службова, з RTTI). У масивах поліморфних об\u0026rsquo;єктів у RAM це відчутно, тому вбудований код часто воліє switch або таблиці функцій. EBO. Порожній базовий клас не займає місця (4), а порожнє поле-член займає (8, бо кожен об\u0026rsquo;єкт мусить мати власну адресу). [[no_unique_address]] (C++20) дає той самий ефект для полів-членів. alignas(32) - стандартний спосіб для DMA-буферів. У C довго було лише розширення компілятора (__attribute__((aligned)), __alignof__), _Alignas/_Alignof з\u0026rsquo;явились у C11, а в C23 alignas/alignof стали повноцінними ключовими словами, як у C++. Не standard-layout. static_assert у файлі перевіряє, що WithVirt не standard-layout: покладатися на порядок полів, offsetof чи сумісність із C для таких типів не можна. Для того, що йде у Flash чи в протокол, тримайте прості структури й ставте static_assert(std::is_standard_layout_v\u0026lt;T\u0026gt;). Rust: інша філософія У C/C++ порядок полів у пам\u0026rsquo;яті = порядок у коді. У Rust за замовчуванням (repr(Rust)) компілятору дозволено переставляти поля, щоб зекономити місце.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 use std::mem::{align_of, offset_of, size_of}; struct AutoOrder { a: u8, b: u32, c: u16 } #[repr(C)] struct CLayout { a: u8, b: u32, c: u16 } #[repr(C, packed)] struct FwHeader { magic: u32, version: u16, flags: u8, crc: u32 } #[repr(C, align(32))] struct DmaBuf([u8; 64]); // перевірка layout на етапі компіляції, аналог _Static_assert const _: () = assert!(size_of::\u0026lt;FwHeader\u0026gt;() == 11); const _: () = assert!(offset_of!(FwHeader, crc) == 7); fn main() { println!(\u0026#34;repr(Rust): size={} align={}\u0026#34;, size_of::\u0026lt;AutoOrder\u0026gt;(), align_of::\u0026lt;AutoOrder\u0026gt;()); println!(\u0026#34; offsets: a={} b={} c={}\u0026#34;, offset_of!(AutoOrder, a), offset_of!(AutoOrder, b), offset_of!(AutoOrder, c)); println!(\u0026#34;repr(C): size={} align={}\u0026#34;, size_of::\u0026lt;CLayout\u0026gt;(), align_of::\u0026lt;CLayout\u0026gt;()); println!(\u0026#34;packed: size={} align={}\u0026#34;, size_of::\u0026lt;FwHeader\u0026gt;(), align_of::\u0026lt;FwHeader\u0026gt;()); println!(\u0026#34;DmaBuf: size={} align={}\u0026#34;, size_of::\u0026lt;DmaBuf\u0026gt;(), align_of::\u0026lt;DmaBuf\u0026gt;()); println!(\u0026#34;\u0026amp;u32={} Option\u0026lt;\u0026amp;u32\u0026gt;={}\u0026#34;, size_of::\u0026lt;\u0026amp;u32\u0026gt;(), size_of::\u0026lt;Option\u0026lt;\u0026amp;u32\u0026gt;\u0026gt;()); let h = FwHeader { magic: 0xA5A5_0001, version: 3, flags: 0, crc: 0xDEAD_BEEF }; let crc = h.crc; // копія значення: ОК println!(\u0026#34;crc = {:#x}, bytes LE = {:02x?}\u0026#34;, crc, crc.to_le_bytes()); } 1 2 3 4 5 6 7 8 rustc -O lay.rs -o lay \u0026amp;\u0026amp; ./lay # offset_of! потребує Rust 1.77+ repr(Rust): size=8 align=4 offsets: a=6 b=0 c=4 repr(C): size=12 align=4 packed: size=11 align=1 DmaBuf: size=64 align=32 \u0026amp;u32=8 Option\u0026lt;\u0026amp;u32\u0026gt;=8 crc = 0xdeadbeef, bytes LE = [ef, be, ad, de] Ті самі u8, u32, u16: у C це 12 байт, а в repr(Rust) компілятор переставив поля (b на 0, c на 4, a на 6) й отримав 8, рівно як наша \u0026ldquo;відсортована\u0026rdquo; struct good у C. Звільнення від людського фактора, але є застереження: layout repr(Rust) не гарантований і може змінитися між версіями компілятора. Тому для всього, що виходить за межі вашого Rust-коду (FFI, регістри, Flash, мережа), потрібен явний #[repr(C)] або #[repr(C, packed)].\nconst _: () = assert!(...) у файлі - Rust-аналог _Static_assert: невдала перевірка зупиняє компіляцію.\nLayout кожного типу від компілятора: -Z print-type-sizes (nightly) 1 2 3 4 5 6 7 8 9 10 11 12 rustc +nightly -Z print-type-sizes -O lay.rs # з cargo: cargo +nightly rustc -- -Z print-type-sizes print-type-size type: `CLayout`: 12 bytes, alignment: 4 bytes print-type-size field `.a`: 1 bytes print-type-size padding: 3 bytes print-type-size field `.b`: 4 bytes, alignment: 4 bytes print-type-size field `.c`: 2 bytes print-type-size end padding: 2 bytes print-type-size type: `AutoOrder`: 8 bytes, alignment: 4 bytes print-type-size field `.b`: 4 bytes print-type-size field `.c`: 2 bytes print-type-size field `.a`: 1 bytes print-type-size end padding: 1 bytes Це найближчий аналог pahole для Rust: видно і переставлені поля, і padding.\nКрім того:\nNiche optimization. Option\u0026lt;\u0026amp;u32\u0026gt; займає стільки ж, скільки \u0026amp;u32 (8 на 64-бітному хості): нульовий вказівник для \u0026amp;T заборонений, тож None кодується ним, а окремий тег не потрібен. Для вирівнювання DMA-буферів: #[repr(C, align(32))] на обгортці над масивом (DmaBuf у файлі). Посилання на поле packed-структури. Rust не пустить у UB: 1 let r = \u0026amp;h.crc; // посилання на невирівняне поле 1 2 3 4 error[E0793]: reference to packed field is unaligned = note: packed structs are only aligned by one byte, and many modern architectures penalize unaligned field accesses = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced) = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers) Це та сама пастка, що й read_via_ptr у C, але в Rust вона hard error, а не попередження. Читайте поле копією (let crc = h.crc;), як у прикладі вище.\nЗведена таблиця C C++ Rust (repr(Rust)) Rust (repr(C)) Порядок полів як у коді як у коді (база =\u0026gt; похідний) не гарантований, компілятор оптимізує як у коді Padding природне вирівнювання те саме мінімізується автоматично як у C Packed __attribute__((packed)) / #pragma pack те саме #[repr(packed)] #[repr(C, packed)] Вирівнювання для DMA aligned(N) / _Alignas (C11), alignas (C23) alignas(N) #[repr(align(N))] #[repr(C, align(N))] Прихований розмір немає vptr, бази немає немає Перевірка в compile time _Static_assert static_assert const _: () = assert!(...) + offset_of! те саме Інструменти pahole, -Wpadded, -Wcast-align те саме + nm -C для vtable -Z print-type-sizes те саме Придатність для FFI/Flash так лише standard-layout ні так Золоті правила розробника Сортуйте поля від більшого до меншого і перевіряйте результат у pahole -C \u0026lt;тип\u0026gt; (або -Wpadded). Не рахуйте в голові. Фіксуйте розмір і зсуви через _Static_assert / static_assert / const assert!. Це страховка від сюрпризів при зміні платформи, компілятора чи прапорів. packed лише для форматів, що лежать у Flash. Не для MMIO, не \u0026ldquo;щоб влізло\u0026rdquo; в кожну структуру. Якщо формат дозволяє, додайте aligned(4), щоб не платити побайтовим читанням за вирівняні поля. Усе, що йде в мережу, серіалізуйте явно. packed не лікує ні endianness, ні сміття в padding. Не беріть адрес поля packed-структури й не кастуйте байтовий буфер у uint32_t *. Для читання з буфера є get_u32le-подібні функції або memcpy. Тримайте -Wcast-align і -Waddress-of-packed-member увімкненими, на хості - -fsanitize=alignment. Пам\u0026rsquo;ятайте про ядро. На M0/M0+ нечесний невирівняний ldr = HardFault. На M3/M4/M7 це мовчки повільно (а для ldrd/ldm/vldr - теж fault), ловиться через UNALIGN_TRP разом із -mno-unaligned-access. У Rust усе, що виходить за межі Rust-коду, має #[repr(C)]. repr(Rust) лишайте для внутрішніх структур. ","date":"2026-10-21T20:12:12+03:00","image":"/post/struct-memory-layout/cover.png","permalink":"/uk/post/struct-memory-layout/","title":"Memory layout: чому sizeof(struct) ≠ сума полів, і до чого тут HardFault"},{"content":"Колись, у старі добрі часи, писав трохи технічних статей на платформі Blogger - було прикольно, й часом корисно повернутися в часі й повторити якийсь сетап.\nІ ось @Schistos почав свої сетапи описувати подібним чином, накрило ностальгією й пішов досліджувати тему, що зараз актуальне. Ну й знайшов кілька проєктів з генерації статичних сайтів, проте найбільше до душі припав Hugo, ось ви на нього зараз й дивитеся 😉\nЯк це працює? 📝 Примітка Hugo - це статичний генератор сайтів. Ви пишете контент у Markdown-файлах, а Hugo перетворює їх на готові HTML/CSS/JS сторінки під час збірки. Це забезпечує максимальну швидкість завантаження та безпеку, оскільки на сервері немає бази даних чи активного бекенду.\nЦе зручно тримати в git-репозиторії й налаштувати CI/CD для збірки й публікації в GitHub/GitLab Pages, чим ми й займемося.\nВибір теми Для початку йдемо на themes.gohugo.io й обираємо те, що подобається. Я, після кількох годин перегляду усіх можливих і неможливих тем, зупинив свій вибір на темі Stack. Йдемо на GitHub теми й у README.md бачимо, що автор створив шаблон репозиторію, який можна просто використати для швидкого розгортання, проте ми не шукаємо простих шляхів, тому будемо робити все вручну. Хостити я збираюся не на GitHub, а на GitLab, тому дещо доведеться адаптувати під себе у будь-якому разі.\nВстановлення Hugo Ставимо Hugo і Git:\n1 sudo pacman -Sy hugo git І перевіряємо чи все ок:\n1 2 hugo version hugo v0.165.0+extended+withdeploy linux/amd64 BuildDate=unknown Створюємо проєкт 1 hugo new project blog.hardfault.win Нам дає трішки підказок щодо того що робити далі. Глянемо що вже є:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cd blog.hardfault.win \u0026amp;\u0026amp; hugo server -D Watching for changes in archetypes, assets, content, data, i18n, layouts, static Watching for config changes in hugo.toml Start building sites … hugo v0.165.0+extended+withdeploy linux/amd64 BuildDate=unknown WARN found no layout file for \u0026#34;html\u0026#34; for kind \u0026#34;home\u0026#34;: You should create a template file which matches Hugo Layouts Lookup Rules for this combination. WARN found no layout file for \u0026#34;html\u0026#34; for kind \u0026#34;taxonomy\u0026#34;: You should create a template file which matches Hugo Layouts Lookup Rules for this combination. │ EN ──────────────────┼──── Pages │ 4 Paginator pages │ 0 Non-page files │ 0 Static files │ 0 Processed images │ 0 Aliases │ 0 Cleaned │ 0 Built in 2 ms Environment: \u0026#34;development\u0026#34; Serving pages from disk Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop Отже, бачимо список директорій, за якими Hugo відстежує зміни, і одразу ж попередження про те, що він не має шаблонів для необхідних сторінок - це треба виправити.\nВстановлюємо тему Hugo використовує Go modules для керування залежностями, тому перетворимо наш проєкт на Go модуль:\n1 2 3 4 hugo mod init blog.hardfault.win go: creating new go.mod: module blog.hardfault.win hugo: to add module requirements and sums: hugo mod tidy Переглянемо його:\n1 2 3 module blog.hardfault.win go 1.26.6 Далі - додаємо залежність від нашої теми:\n1 2 3 4 5 # ... [module] [[module.imports]] path = \u0026#34;github.com/CaiJimmy/hugo-theme-stack/v4\u0026#34; Перезапускаємо наш сервер:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 hugo server -D Watching for changes in archetypes, assets, content, data, i18n, layouts, static Watching for config changes in hugo.toml, go.mod Start building sites … hugo v0.165.0+extended+withdeploy linux/amd64 BuildDate=unknown │ EN ──────────────────┼──── Pages │ 8 Paginator pages │ 0 Non-page files │ 0 Static files │ 0 Processed images │ 0 Aliases │ 3 Cleaned │ 0 Built in 33 ms Environment: \u0026#34;development\u0026#34; Serving pages from disk Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop Warning-ів немає - можна переглянути у браузері:\nПоки не густо, але вже працює. Проглянемо поточний граф залежностей:\n1 2 hugo mod graph blog.hardfault.win github.com/CaiJimmy/hugo-theme-stack/v4@v4.0.3 Підлаштування теми під себе 💡 Порада Щоб зручніше переглядати вихідний код теми та вносити в нього правки (якщо це потрібно), використовуйте команду hugo mod vendor. Вона завантажить копію модуля в локальну директорію _vendor, що дозволить шукати по коду теми за допомогою IDE.\n1 hugo mod vendor Це створить директорію _vendor у нашому проєкті:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 tree -d _vendor -L 5 _vendor └── github.com └── CaiJimmy └── hugo-theme-stack └── v4 ├── archetypes ├── assets ├── config ├── data ├── i18n └── layouts 11 directories Скопіюємо й перглянемо конфіги з теми в проєкт:\n1 cp -r _vendor/github.com/CaiJimmy/hugo-theme-stack/v4/config . 1 2 3 4 5 6 7 tree config config └── _default ├── module.toml └── params.toml 2 directories, 2 files module.toml - тут тільки залежності від Hugo, а ось в params.toml знаходяться всі основні налаштування теми.\n📌 Важливо Щоб конфігурації не були розкидані по всьому проєкту, краще перемістити кореневий hugo.toml до інших конфігів у config/_default/ та розбити його на тематичні файли.\nПереміщуємо файл:\n1 mv hugo.toml config/_default/ І наводимо ладу: import із config.toml перемістимо в module.toml, змінимо імена і т.п. Отримуємо:\n1 2 3 baseURL = \u0026#39;https://hardfault.win/\u0026#39; locale = \u0026#39;en\u0026#39; title = \u0026#39;HardFault.win\u0026#39; 1 2 3 4 5 6 [hugoVersion] extended = true min = \u0026#34;0.157.0\u0026#34; [imports] path = \u0026#34;github.com/CaiJimmy/hugo-theme-stack/v4\u0026#34; Створимо конфіг для бокового меню:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 [[social]] identifier = \u0026#34;github\u0026#34; name = \u0026#34;GitHub\u0026#34; url = \u0026#34;https://github.com/elektro-NIK\u0026#34; [social.params] icon = \u0026#34;brand-github\u0026#34; [[social]] identifier = \u0026#34;gitlab\u0026#34; name = \u0026#34;GitLab\u0026#34; url = \u0026#34;https://gitlab.com/elektro.NIK\u0026#34; [social.params] icon = \u0026#34;brand-gitlab\u0026#34; [[social]] identifier = \u0026#34;linkedin\u0026#34; name = \u0026#34;LinkedIn\u0026#34; url = \u0026#34;https://www.linkedin.com/in/elektro-nik\u0026#34; [social.params] icon = \u0026#34;brand-linkedin\u0026#34; [[main]] identifier = \u0026#34;home\u0026#34; name = \u0026#34;Home\u0026#34; url = \u0026#34;/\u0026#34; weight = 10 [main.params] icon = \u0026#34;home\u0026#34; [[main]] identifier = \u0026#34;about\u0026#34; name = \u0026#34;About\u0026#34; url = \u0026#34;/about/\u0026#34; weight = 20 [main.params] icon = \u0026#34;user\u0026#34; [[main]] identifier = \u0026#34;archives\u0026#34; name = \u0026#34;Archives\u0026#34; url = \u0026#34;/archives/\u0026#34; weight = 30 [main.params] icon = \u0026#34;archives\u0026#34; [[main]] identifier = \u0026#34;search\u0026#34; name = \u0026#34;Search\u0026#34; url = \u0026#34;/search/\u0026#34; weight = 40 [main.params] icon = \u0026#34;search\u0026#34; [[main]] identifier = \u0026#34;links\u0026#34; name = \u0026#34;Links\u0026#34; url = \u0026#34;/links/\u0026#34; weight = 50 [main.params] icon = \u0026#34;link\u0026#34; І покладемо іконки, яких немає в темі:\n1 2 3 4 5 6 tree assets/icons assets/icons ├── brand-gitlab.svg └── brand-linkedin.svg 1 directory, 2 files Всі посилання ведуть на 404, проте це вже щось :) Додамо сторінки, щоб бачити їхній функціонал:\n1 hugo new content content/page/about/index.md 1 hugo new content content/page/archives/index.md 1 hugo new content content/page/links/index.md 1 hugo new content content/page/search/index.md Редагуємо файли:\n1 2 3 4 5 +++ date = \u0026#39;2026-01-01T00:00:00+00:00\u0026#39; draft = false title = \u0026#39;About\u0026#39; +++ 1 2 3 4 5 6 +++ date = \u0026#39;2026-01-01T00:00:00+00:00\u0026#39; draft = false title = \u0026#39;Archives\u0026#39; layout = \u0026#34;archives\u0026#34; +++ 1 2 3 4 5 +++ date = \u0026#39;2026-01-01T00:00:00+00:00\u0026#39; draft = false title = \u0026#39;Links\u0026#39; +++ 1 2 3 4 5 6 7 8 +++ date = \u0026#39;2026-01-01T00:00:00+00:00\u0026#39; draft = false title = \u0026#39;Search\u0026#39; type = \u0026#34;page\u0026#34; layout = \u0026#34;search\u0026#34; outputs = [\u0026#34;html\u0026#34;, \u0026#34;json\u0026#34;] +++ Додамо аватар й favicon:\n1 2 3 4 5 6 tree assets/img assets/img ├── avatar.png └── favicon.png 1 directory, 2 files 1 2 3 4 5 6 7 8 9 10 11 12 13 # ... favicon = \u0026#34;img/favicon.png\u0026#34; # ... [sidebar] compact = false avatar = \u0026#34;img/avatar.png\u0026#34; emoji = \u0026#34;🦀\u0026#34; subtitle = \u0026#34;Cortex-M panics, but we don\u0026#39;t\u0026#34; # ... І створимо тестову публікацію:\n1 hugo new content content/post/hello_world/index.md 1 2 3 4 5 6 7 ## Hello World ### Test text Цим вже можна користуватися, а в наступній частині ми займемося підключенням багатомовності.\nШпаргалка по конфігурації Файл Призначення hugo.toml Основні налаштування сайту (baseURL, title, locale) params.toml Налаштування теми Stack (Sidebar, Footer, Cookies тощо) module.toml Керування версією Hugo та імпорт теми як модуля menu.toml Структура навігації та соцмереж у боковій панелі languages.toml Налаштування багатомовності (якщо використовується) ","date":"2026-08-27T22:24:40+03:00","image":"/post/hugo-1/Screenshot3.png","permalink":"/uk/post/hugo-1/","title":"Ставимо Hugo"}]