18.4 C hordozhatósági audit és freestanding kitekintés

Egy C program akkor hordozható, ha minden feltételezése, amelyet a nyelvről, a platformról vagy a buildről tesz, ki van mondva, címkével el van látva és ellenőrizhető. Ez az egység azt a munkafolyamatot adja, amellyel ezt a listát összeállítod, és megmutatja, hol ér véget a hosted C: a freestanding tartományban. A cél nem az, hogy „mindenhol fusson”, hanem hogy pontosan tudd, hol fut, miért, és mi történik, ha a feltétel nem teljesül.

18.4.1 A hordozhatósági audit mint szerződés

A hordozhatósági audit verziózott dokumentum, amely a forráskódon kívül eső tulajdonságokra épülő állításokat gyűjti össze, mindegyiket címkével, ellenőrzési móddal és fallbackkel. Nem dokumentáció a dokumentáció kedvéért: a sorok egy része gépi ellenőrzéssé alakítható (#if, _Static_assert, configure-próba), a többi pedig a labornaplóba kerül. Az audit akkor jó, ha egy új targetre való portoláskor a kérdés nem az, hogy „mi romolhat el”, hanem hogy melyik sorok nem teljesülnek.

A címkézés a legfontosabb fegyelem, mert a négy kategória jogi ereje eltérő:

Az audittábla kötelező mezői: feltételezés, címke, ellenőrzés, fallback. Egy részleges példa:

Feltételezés Címke Ellenőrzés Fallback / teendő
CHAR_BIT == 8 ISO (limits.h) #if CHAR_BIT != 8 → #error bájt-alapú protokoll újraértelmezése, vagy a target kizárása
uint32_t létezik ISO, opcionális (stdint.h) #if !defined(UINT32_MAX) uint_least32_t, méretfüggetlen kód
sizeof(void*) == sizeof(long) ABI (LP64) sizeof mérés a laborban uintptr_t, vagy pointer→egész konverzió elkerülése
_Alignof(T) ismert ISO _Alignof, offsetof explicit padding, byte-wise szerializálás
little-endian tárolás ABI/platform __BYTE_ORDER__ vagy futásidejű próba bájtonkénti írás/olvasás
IEC 60559 float/double ISO, opcionális (Annex F) __STDC_IEC_559__ (C17) fixpontos vagy saját formátum
epoll elérhető Linux #ifdef __linux__ + header poll adapter
__builtin_bswap16 compiler extension __has_builtin shift-alapú megvalósítás

18.4.2 Nyelvi feltételezések: C bájt, egészek, pointermodell

A sizeof egysége a C bájt, amelynek bitszáma CHAR_BIT; a 8 bites oktett feltételezés, nem nyelvi tény. Mivel az unsigned char nem tartalmaz kitöltő bitet, UCHAR_MAX == 2^CHAR_BIT − 1 mindig teljesül, így a CHAR_BIT ellenőrzése preprocesszorral elvégezhető. Ha CHAR_BIT != 8, akkor a bájt-alapú hálózati protokoll, a uint8_t és a POSIX read()/write() oktett-szemantikája egyszerre esik ki: a POSIX oktettben számol, a C bájtban, és a kettő csak 8 bites bájtnál esik egybe. Ezért a hosted POSIX laborokban a 8 bitet a laborprofil rögzíti, nem a kód feltételezi.

Az exact-width típusok (int8_t, uint32_t, …) opcionálisak: csak akkor léteznek, ha van pontosan olyan szélességű, kitöltő bit nélküli típus. Kötelező viszont a int_leastN_t, int_fastN_t és intmax_t család, a intptr_t/uintptr_t pedig szintén opcionális. A helyes ellenőrzés nem a header létezésére, hanem a típusnév létezésére kérdez: #if defined(UINT8_MAX). A sizeof(int) == 4 állítás se nem ISO, se nem ABI-következmény — ez tipikusan mérés, amit a labornaplóban kell rögzíteni.

A pointeradatmodell (LP64, ILP32, LLP64) ABI-szintű fogalom. A sizeof(void*) == sizeof(long) állítás Linux x86-64-en igaz, Windowson nem; a pointer egésszé alakítása csak uintptr_t-vel és akkor is implementation-defined. A portolható kód nem a pointer értékére, hanem a pointer aránya/sorrendjére épít, és ahol lehet, elkerüli a konverziót.

18.4.3 Platform- és numerikus feltételezések: igazítás, endian, lebegőpont

Az igazítás lekérdezhető (_Alignof, offsetof, C23-ban alignof), de a struct layout soha nem szerializációs formátum: két architektúrán ugyanaz a struktúra más offsetekkel és paddinggel rendelkezhet, amit a korábbi layout-labor Layout- és protokolllabor két architektúrán (9.5) már rögzített. A címigazítás levezetése: legyen a>0a>0 az igazítás, n≥0n\ge 0; a legkisebb aa-val osztható m≥nm\ge n

m=⌊n+a−1a⌋a,m=\left\lfloor\frac{n+a-1}{a}\right\rfloor a,

és ha a=2ka=2^k, akkor m=(n+2k−1)&∼(2k−1)m=(n+2^k-1)\ \&\ \sim(2^k-1). Az előfeltétel, hogy n+a−1n+a-1 ne csorduljon túl: if (n > SIZE_MAX - (a - 1)) hibakezelés kell, különben a „round-up” csendben nullára teker.

Endianra az ISO C-nek nincs makrója. A __BYTE_ORDER__ GCC/Clang kiterjesztés, tehát az #if csak a rögzített compiler-profillal érvényes; hordozható megoldás a bájtonkénti (de) szerializálás. A union-on keresztüli type punning C-ben implementation-defined, C++-ban nem definiált viselkedés — a két nyelv eltérését a C és C++: azonos szintaxis, eltérő szemantika (18.3) rész tárgyalta, itt csak a következmény számít: az auditban ez nem lehet „ISO” sor.

Lebegőpontnál az IEC 60559 (Annex F) csak akkor áll, ha az implementáció definiálja a megfelelő makrót. C17-ben ez jellemzően __STDC_IEC_559__; a C23 ezt átdolgozta (a bináris és decimális formátumok külön makrót kaptak). A pontos makrónév és jelentés verziófüggő, ezért a konkrét draft és a tényleges compiler dokumentációja ellenőrizendő — ebben az egységben erre nem hivatkozom igazolt forrásként, és a labornaplóban kell rögzíteni.

18.4.4 Feature-test és fallback policy

A policy egyetlen helyen, egy portability.h-ban élő #if létra: minden feltételezés ott dől el, és a nem támogatott kombináció fordítási hiba, nem hajnali 3-kor jelentkező futásidejű meglepetés. A vizsgálandó makrók: __STDC_VERSION__, __STDC_HOSTED__, __STDC_NO_THREADS__, __STDC_NO_ATOMICS__, __STDC_NO_VLA__, valamint __has_include és __has_builtin. Utóbbiak C23-ban szabványosak, korábbi nyelvekben compiler-kiterjesztések, ezért defined(...) őr mögé kerülnek.

/* portability.h — illusztráció, a laborban fordítandó, nem lefuttatott példa */
#ifndef PORTABILITY_H
#define PORTABILITY_H

#include <limits.h>
#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
#  define PT_STATIC_ASSERT(cond, msg) static_assert(cond, msg)
#else
#  define PT_STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
#endif

#if CHAR_BIT != 8
#  error "Ez a fordítási profil 8 bites C bajtra epul (CHAR_BIT == 8)."
#endif

#if !defined(UINT8_MAX)
#  error "Nincs pontosan 8 bites unsigned egesz tipus ezen az implementacion."
#endif

PT_STATIC_ASSERT(sizeof(uint32_t) == 4, "uint32_t nem 4 C bajt");

static uint16_t pt_bswap16_portable(uint16_t v)
{
    return (uint16_t)((uint16_t)(v >> 8) | (uint16_t)(v << 8));
}

static uint16_t pt_bswap16(uint16_t v)
{
#if defined(__has_builtin)
#  if __has_builtin(__builtin_bswap16)
    return __builtin_bswap16(v);
#  else
    return pt_bswap16_portable(v);
#  endif
#else
    return pt_bswap16_portable(v);
#endif
}

#endif /* PORTABILITY_H */

A buildparancs a rögzített profillal (a verziókat a labornapló tartalmazza):

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -c portability.c -o portability.o
g++ -std=c++20 -Wall -Wextra -Wpedantic -x c++ -c portability.c -o portability_cpp.o

A második sor csak azt mutatja, hogy a header mindkét nyelvben fordul; a két nyelv nem közös nyelv, a PT_STATIC_ASSERT éppen a szintaktikai különbséget izolálja.

18.4.5 Freestanding: startup, MMIO, könyvtári korlátok

Freestanding az az implementációs mód, ahol __STDC_HOSTED__ == 0. A szabvány itt csak egy kötelező minimumot ír elő: a program indulása és leállása (a belépési pont neve, típusa, a leállás módja) implementation-defined, és a könyvtárból csak néhány header tartalma garantált. Ez alsó korlát, nem tilalom: a freestanding definíció nem mondja, hogy nincs operációs rendszer vagy további könyvtár. Freestanding módban fordul például a Linux-kernel (OS-en belül fut, kmalloc-kal és printk-val), és freestanding egy olyan embedded toolchain is, amely saját malloc-ot vagy minimál printf-et nyújt — ezek a platform szolgáltatásai, nem a szabvány garanciái, tehát az auditban platformtényként kell címkézni őket.

A kötelező freestanding headerek C17-ben: float.h, iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h, stddef.h, stdint.h, stdnoreturn.h. A C23 (N3096 munkatervezet, 4. szakasz) ezt a listát bővíti: a felsoroltak mind megmaradnak, új a stdbit.h, és a freestanding implementációnak a string.h nagy részét (a strdup, strndup, strcoll, strxfrm, strerror kivételével), valamint a stdlib.h-ból a memalignment függvényt is biztosítania kell. A C23 egyes neveket kulcsszóvá tett, ezért három kompatibilitási header tartalma kiürült vagy elavult — de a header maga megmaradt, a #include <stdbool.h> C23-ban is érvényes:

Header C17 tartalma C23 tartalma Megjegyzés
stdbool.h bool, true, false makró, __bool_true_false_are_defined csak az obsolescent __bool_true_false_are_defined bool, true, false C23-ban kulcsszó
stdalign.h alignas, alignof makró, __alignas_is_defined, __alignof_is_defined nincs tartalma („provides no content”) alignas, alignof C23-ban kulcsszó
stdnoreturn.h noreturn makró (_Noreturn-re) noreturn makró, a header és a makró obsolescent helyette [[noreturn]] attribútum; a _Noreturn is obsolescent
assert.h (hosted) static_assert makró a static_assert kulcsszó, a makró kikerült a thread_local is kulcsszó lett (C17-ben threads.h-makró)
stdbit.h — új, freestanding is bitszámláló és bájtsorrend-segédek

A táblázat a C23 munkatervezet 7. fejezetével és a C header inventory (46.4.1) leltárral egyezik. A labor GCC 13.3 (-std=c2x) és Clang 22 (-std=c23) mellett -ffreestanding módban lefordította azt a próbafájlt, amely mind a kilenc C17-es freestanding headert és a stdbit.h-t beemeli, és a bool, alignas, static_assert nevet header nélküli kulcsszóként, a noreturn-t a stdnoreturn.h makrójaként használja; -std=c17 alatt ugyanez a forrás a makrós úton fordult. A fejezet egyébként nem feltételezi a C23 teljes támogatását.

Egy konkrét, OS nélküli mikrokontrolleres célon (ez egy példa, nem a freestanding definíciója) a startup tipikusan: reset vektor → .data inicializált szakasz másolása ROM-ból RAM-ba → .bss nullázása → main. A main visszatérésének itt nincs értelme, a belépési pont neve és hívási konvenciója pedig platform- és toolchain-függő; ezt linker script és startup assembly rögzíti, nem a C forrás.

Az MMIO volatile pointerrel érhető el fix címen. A volatile azt biztosítja, hogy a compiler ne optimalizálja ki és ne rendezze át a hozzáféréseket egymáshoz képest; nem biztosít sorrendet más végrehajtó egységekkel szemben, ahogy azt a volatile, compiler barrier és hardver barrier (14.5) rész kifejtette.

/* freestanding vazlat — hosted Linuxon nem futtathato, nem tesztelt pelda */
#include <stdint.h>

#define UART_BASE 0x40000000UL
#define UART_DR   (*(volatile uint32_t *)(UART_BASE + 0x00UL))

void _start(void)   /* a belepesi pont neve es konvencioja platformfuggo */
{
    UART_DR = (uint32_t)'O';
    for (;;) { }
}
arm-none-eabi-gcc -std=c17 -ffreestanding -nostdlib -Wall -Wextra \
  -T link.ld -o app.elf start.c

A target triple, a linker script és a linkelési cím platformtény: ez nem hordozható, hanem az audit egy sorában dokumentált, konkrét profil. Freestanding módban a szabvány nem garantál malloc-ot, printf-et vagy szálkezelést; hogy a platform ad-e ilyet (kernel-API, a toolchain minimál libc-je, RTOS), az platformtény. Ha nincs, saját allokátort és saját formázót írunk; ha van, a forrását és a szerződését rögzítjük — mindkét eset bekerül az auditba.

18.4.6 Header-leltár és továbbtanulási térkép

A header-leltár az audit melléklete: headerenként, hogy hosted-only (stdio.h, stdlib.h, string.h, time.h, locale.h, signal.h, threads.h) vagy freestanding, és mi a portabilitási kockázata. A továbbtanulási térkép elsődleges dokumentumokra mutat: az ISO/IEC 9899:2018 szövege, a C23 nyilvános munkatervezete (N3096 — munkadokumentum, nem az ISO kiadott szabványa), a GCC „Standards” oldala, a Clang dokumentációja és a kiválasztott target ABI-specifikációja. Ezek konkrét verzióját a labornapló rögzíti; ebben az egységben nem állítom, hogy bármelyiküket ellenőriztem vagy lefuttattam.

18.4.7 Tipikus hibák

A leggyakoribb hiba a CHAR_BIT == 8 és a sizeof(int) == 4 hallgatólagos feltételezése. Gyakori a sizeof(void*) == sizeof(long) átvitele Linuxról Windowsra, a pointer long-ba töltése és a csendben csonkolódó cím. Sok kód %d-vel ír ki size_t-et, ami C++-ban és C-ben egyaránt nem definiált viselkedés; a helyes %zu. Gyakori a union-os endian-trükk, ami C-ben implementation-defined, C++-ban UB, tehát az auditban nem kaphat „ISO” címkét. Végül tipikus a #ifdef __linux__ egyedüli ellenőrzésként: ez nem fedi a libc- és architektúra-különbségeket, és nem ad fallbacket, ha az epoll nem elérhető.

18.4.8 Összefoglaló

A hordozhatósági audit nem stílus, hanem szerződés: feltételezés, címke, ellenőrzés, fallback. A nyelvi mag (CHAR_BIT, szélességek, pointermodell, igazítás) preprocesszorral és statikus állításokkal kényszeríthető ki; a platform és az ABI csak méréssel és dokumentációval. A feature-test policy egy helyen dönt, és a nem támogatott kombinációt fordítási hibává teszi. Freestanding módban a szabvány csak a minimumot garantálja; a startup, az MMIO és minden további könyvtár a platform vagy a saját kód felelőssége, és ezt az auditban kell rögzíteni.

18.4.9 Feladatok

1. Címkézés. Sorold be az alábbi állításokat ISO / implementation-defined / POSIX / Linux / compiler extension / ABI / mérés kategóriába, és indokold: (a) unsigned char értékkészlete 0…2CHAR_BIT−10\ldots 2^{CHAR\_BIT}-1; (b) sizeof(long) == 8; (c) errno beállítása hiba esetén a read() hívás után; (d) __builtin_expect működik; (e) a uint8_t létezik.

2. Kódolás. Egészítsd ki a portability.h-t úgy, hogy CHAR_BIT != 8 esetén a build álljon meg, kivéve ha PT_ALLOW_NON_OCTET definiált; és adj pt_rotl32 fallbacket __builtin_rotateleft32 hiányában.

3. Diagnosztika. Az alábbi részlet Linux x86-64-en működik, másutt nem. Találd meg a hibákat, javítsd, és add meg az ellenőrzési feltételt:

void *p = get_buffer();
long addr = (long)p;
printf("buffer @ %d, meret %d\n", (int)addr, (int)sizeof(p));

18.4.10 Megoldások

1. (a) ISO: az unsigned char kitöltő bit nélküli, ezért a limits.h garantálja. (b) ABI + mérés: LP64 alatt 8, ILP32 alatt 4; az állítás csak profilhoz köthető. (c) POSIX: az errno szemantikája a POSIX read() szerződése, nem az ISO C-é. (d) Compiler extension: GCC/Clang-specifikus, __has_builtin-nel őrizendő. (e) ISO, de opcionális: csak akkor igaz, ha létezik pontosan 8 bites, kitöltő bit nélküli típus; UINT8_MAX definiáltságával ellenőrizhető. Ellenőrzési feltétel: minden sor mellett szerepel a konkrét header vagy dokumentum, amelyre a címke támaszkodik.

2. A védelmet a #if CHAR_BIT != 8 ágba tett #if !defined(PT_ALLOW_NON_OCTET) / #error pár adja, a PT_ALLOW_NON_OCTET-et pedig a buildrendszer definiálja (-DPT_ALLOW_NON_OCTET), nem a forrás. A rotáció fallbackje shift-alapú: ((v << n) | (v >> (32 - n))) uint32_t-n, n % 32 normalizálással, n == 0 külön kezelve (a >> 32 ugyanis nem definiált). Ellenőrzési feltétel: a __has_builtin mindkét ága forduljon le, és a két implementáció eredménye n = 0, 1, 31 esetén egyezzen.

3. Két hiba: a pointer long-ba töltése LLP64/ILP32 alatt csonkol, és a %d int-et vár, miközben size_t-et adunk át — ez nem definiált viselkedés. Helyes változat: a pointert csak akkor alakítjuk egésszé, ha uintptr_t létezik (#if defined(UINTPTR_MAX)), különben az audit „nem támogatott” ágra fut; a formátum %zu, a címhez PRIxPTR a <inttypes.h>-ból, ha az elérhető. Ellenőrzési feltétel: -Wall -Wextra -Wpedantic -Wformat=2 mellett nincs figyelmeztetés, és a kimenet sizeof(void*) értékétől függetlenül helyes.