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.
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 |
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.
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
az igazítás,
;
a legkisebb
-val
osztható
és ha
,
akkor
.
Az előfeltétel, hogy
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.
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.oA 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.
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.cA 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.
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.
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ő.
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.
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
;
(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));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.