Az előfeldolgozó hibáit gyakran nem az eredeti forrás, hanem a fordítónak ténylegesen átadott tokensorozat teszi érthetővé. Egy makró neve helyes műveletet sugallhat, miközben a helyettesítés mást eredményez; egy konfigurációs makró pedig már szerepelhet a végső makrólistában, noha a rá támaszkodó header feldolgozásakor még nem létezett.
A Makrók, feltételes fordítás és veszélyes helyettesítés szabályaira építve most bizonyítékokat gyűjtünk: előfeldolgozott forrást, makrólistát és include-nyomvonalat. Ezekből különítjük el a helyettesítési hibát, az include-sorrend hibáját és a platformképességekre vonatkozó téves feltételezést.
A példák alapja ISO C17. A választott konkrét laborprofil Linux, x86-64 System V ABI, LP64 adatmodell, glibc 2.40; a két fordító GCC 14.2.0 és Clang 19.1.7. Más kiadás is használható, de a tényleges verziót és targetet rögzíteni kell. Az alábbi tokenrészletek levezetett várakozások, nem elvégzett fordítási mérések.
Az előfeldolgozott forrás (preprocessed source) az include-ok, feltételes fordítási ágak és makróhelyettesítések feldolgozása után kapott szöveges reprezentáció. Nem őrzi meg a forrás minden szerkezeti információját, viszont megmutatja, milyen deklarációk és kifejezések jutnak tovább.
A következő kapcsolók GCC/Clang-eszközfunkciók, nem ISO C szintaktikai elemek:
| Kapcsoló | Mit mutat? | Mire nem elegendő? |
|---|---|---|
-E |
Az előfeldolgozás eredményét | Nem végez teljes típusellenőrzést vagy linkelést |
-E -dM |
Az előfeldolgozás végén fennálló makródefiníciókat | Nem rekonstruálja a makróállapot történetét |
-H |
A ténylegesen beolvasott headerek beágyazását | Nem bizonyítja az interfészek helyességét |
A -H nyomvonala a standard hibakimenetre kerül.
Formátuma és a rendszerheaderek útvonala verzió- és környezetfüggő.
Készítsd el az alábbi három fájlt ugyanabban a könyvtárban. Ez szándékosan hibás, de a megadott kifejezésekkel nem UB-t bemutató C17 tanítási példa.
Az agent_config.h:
#ifndef AGENT_CONFIG_H
#define AGENT_CONFIG_H
#ifndef AGENT_WIDE
#define AGENT_WIDE 1
#endif
#define AGENT_SCALE(x) x * 2
#endifAz agent_api.h:
#ifndef AGENT_API_H
#define AGENT_API_H
#if defined(AGENT_WIDE) && AGENT_WIDE
enum { AGENT_CAPACITY = 32 };
#else
enum { AGENT_CAPACITY = 16 };
#endif
struct agent_record {
unsigned char bytes[AGENT_CAPACITY];
};
#endifA main.c:
#include "agent_api.h"
#include "agent_config.h"
#include <stdio.h>
#define DIAG_INT(value) \
((void)fprintf(stderr, "%s:%d:%s: %s=%d\n", \
__FILE__, __LINE__, __func__, #value, (value)))
int main(void)
{
int scaled = AGENT_SCALE(1 + 3);
DIAG_INT(scaled);
DIAG_INT(AGENT_CAPACITY);
return 0;
}A fejlesztő szándéka szerint az alapértelmezett kapacitás 32, a skálázott érték pedig . A program azonban egyik szándékot sem valósítja meg.
Az alábbi Bash-parancsok külön laborlépések. A CC
értékét a rögzített kiadást kiválasztó parancsra állítsd; a teljes
sorozatot Clanggal is ismételd meg, külön munkakönyvtárban.
CC=gcc
FLAGS=(-std=c17 -Wall -Wextra -Wpedantic)
"$CC" --version > compiler.txt
"$CC" -dumpmachine >> compiler.txt
"$CC" "${FLAGS[@]}" main.c -o agent
"$CC" "${FLAGS[@]}" -E main.c -o main.i
"$CC" "${FLAGS[@]}" -E -dM main.c > macros.txt
"$CC" "${FLAGS[@]}" -E -H main.c \
-o /dev/null 2> includes.txt
grep -n -E 'AGENT_CAPACITY|int scaled' main.iA main.i rendszerheaderekből származó részei hosszúak
lehetnek. Elsőként a saját azonosítóidra keress. Az alapértelmezett
kimenet forráshelyjelöléseit érdemes megtartani: ezek kapcsolják vissza
a tokensorozatot az eredeti fájlokhoz. A GCC/Clang -P
kapcsolója olvashatóbb kimenetet adhat, de eltávolítja ezeket a
jelöléseket.
A lényeges, kézzel levezetett tokenrészletek:
/* Előfeldolgozott részletek, nem önálló program. */
enum { AGENT_CAPACITY = 16 };
int scaled = 1 + 3 * 2;Bizonyításvázlat a makróhibához. Az
AGENT_SCALE helyettesítési listája x * 2. Az
argumentum tokenjei 1 + 3; a helyettesítés nem tesz köréjük
automatikusan zárójelet. Ezért:
A hiba már az előfeldolgozott kifejezésben jelen van, tehát nem az optimalizáló változtatta meg a helyes számítást.
Bizonyításvázlat az include-sorrend hibájához. Az
agent_api.h feldolgozásakor az AGENT_WIDE még
nincs definiálva. A feltétel hamis, ezért a 16-os ág marad meg. A
későbbi agent_config.h már nem értékeli újra a korábbi
direktívát. Emiatt egyszerre lehet igaz, hogy:
macros.txt végső állapotában AGENT_WIDE
értéke 1;main.i tényleges deklarációjában
AGENT_CAPACITY értéke 16.A végső makrólista tehát önmagában félrevezető bizonyíték.
A makró javított definíciója az agent_config.h
fájlban:
#define AGENT_SCALE(x) ((x) * 2)Ez a precedenciaproblémát javítja. Az aritmetikai művelet helyességéhez továbbra is szükséges, hogy az eredmény ábrázolható legyen a művelet típusában.
Az include-hiba tartós javítása nem pusztán a két include
felcserélése. Az agent_api.h saját include guardján belül,
a feltételes ág előtt maga olvassa be a függőségét:
#include "agent_config.h"Így a header önmagában is rendelkezik a saját deklarációihoz
szükséges konfigurációval. Az ellenőrzési feltétel: alapértelmezett
konfigurációnál az előfeldolgozott deklaráció kapacitása 32, a kifejezés
pedig ((1 + 3) * 2) legyen.
-D és -UA GCC/Clang -DNAME=value kapcsolója a forrás
feldolgozása előtt definiálja a makrót. A -DNAME rövid alak
értéke 1. A -UNAME megszünteti a definíciót; a
-D és -U kapcsolók egymáshoz viszonyított
sorrendje számít.
A javított headerrel:
"$CC" "${FLAGS[@]}" -UAGENT_WIDE -DAGENT_WIDE=0 \
-E main.c -o narrow.i
"$CC" "${FLAGS[@]}" -DAGENT_WIDE=0 -UAGENT_WIDE \
-E main.c -o default.iAz első esetben a konfigurációs header már definiált, nulla értékű makrót talál, ezért a kapacitás 16. A másodikban nincs kezdeti definíció, így a header beállítja az alapértelmezett 1-et, és a kapacitás 32.
Két gyakori tévedés következik ebből. A -U nem tiltja
meg, hogy a forrás később újradefiniálja a nevet. Továbbá a
#ifdef AGENT_WIDE nulla értéknél is igaz: a definíció
létezését, nem az értékét vizsgálja.
ISO C17: a __FILE__ előre definiált
makró az aktuális feltételezett forrásfájl nevét adja
sztringliterálként, a __LINE__ pedig a forrássor számát
egész konstansként. Ezeket a #line direktíva is
befolyásolhatja, ezért nem feltétlenül a fizikai fájl változatlan
helyazonosítói.
A __func__ ezzel szemben nem makró.
Függvényen belüli, előre definiált azonosító; szemantikailag statikus
tárolási idejű, konstans karaktertömböt jelöl a függvény nevével. Emiatt
nem várható el, hogy a -dM listájában szerepeljen.
A DIAG_INT helper szerződése szűk:
%d formátumnak megfelelő, alapértelmezett
argumentumpromóció után int típusú értéket fogad;#value csak a
tokenek sztringesítését végzi;__func__
miatt;A diagnosztikai sztringek statikus tárolási idejűek; nincs
tulajdonátadás. A stderr könyvtár által biztosított stream,
amelyet ez a példa nem zár le. Egy korábban lezárt stream használata
megsértené a helper előfeltételét.
A platformteszt környezeti kategóriát azonosít. A képességvizsgálat (feature detection) azt ellenőrzi, hogy a programhoz szükséges konkrét nyelvi vagy könyvtári művelet rendelkezésre áll-e.
| Vizsgálat | Mit jelent? | Mit nem bizonyít? |
|---|---|---|
__linux__ |
A fordító Linux targetet jelez | Egy adott libc API elérhetőségét |
__clang__ |
Clang fordítócsaládot jelez | Egy adott bővítés támogatását |
__GNUC__ |
GNU-kompatibilitást jelző makró | Önmagában nem bizonyítja, hogy GCC fordít |
__STDC_VERSION__ |
Az ISO C nyelvi változat jelzése | POSIX API vagy futásidejű szolgáltatás meglétét |
| Fordítási/linkelési próba | A kipróbált használat elfogadhatóságát | A futásidejű sikerességet |
A libc feature-test makrója más fogalom: gyakran nem képességet jelent be, hanem azt kéri, hogy a headerek mely interfészeket tegyék láthatóvá.
A következő önálló probe_clock.c C17 +
POSIX példa, nem tisztán ISO C:
#include <time.h>
int main(void)
{
struct timespec ts;
return clock_gettime(CLOCK_MONOTONIC, &ts) == 0 ? 0 : 1;
}Fordítás és linkelés a választott Linux/glibc profilhoz:
"$CC" "${FLAGS[@]}" -D_POSIX_C_SOURCE=200809L \
probe_clock.c -o probe_clockA ts a main automatikus tárolási idejű
objektuma. A hívás csak a hívás időtartamára kap kölcsönzött
hozzáférést; nincs tulajdonátadás. Élettartama a blokk végén megszűnik.
Hibánál a program nem olvassa a tartalmát, hanem 1-et ad vissza.
A _POSIX_C_SOURCE definíciójának minden
rendszerheader előtt kell érvényesülnie. Nem elég közvetlenül a
<time.h> elé tenni, ha például a
<stdio.h> már korábban beolvasta a libc közös
konfigurációs headereit. A parancssori definíció ezt az időzítési hibát
elkerüli.
A sikeres fordítás és linkelés azt igazolja, hogy ez a használat az
adott header-, kapcsoló- és könyvtárkészlettel elfogadható. Nem
igazolja, hogy a függvény futás közben sikeres lesz. Régebbi vagy más
libc-profilnál a szükséges linkkönyvtárak is eltérhetnek. A csak
-fsyntax-only kapcsolóval végzett próba pedig még a
linkelhetőséget sem ellenőrzi.
A konfiguráció nem mindig csupán algoritmust választ: nyilvános típusokat is módosíthat.
Az alábbi táblázat a javított laborheaderre vonatkozik, a megadott x86-64 System V/LP64 profilban. A méret a tömbtagé, nem mért teljes struktúraméret.
AGENT_WIDE |
A bytes tag típusa |
A tag mérete |
|---|---|---|
| 0 | unsigned char[16] |
16 C bájt |
| 1 | unsigned char[32] |
32 C bájt |
Ha egy könyvtár és kliense eltérő értékkel dolgozza fel ezt az interfészt, eltérően értelmezheti ugyanazt a határon átadott objektumot. A linker rendszerint nem végez teljes C-típusellenőrzést; a sikeres linkelés nem zárja ki az inkompatibilis deklarációkat vagy a későbbi hibás memória-hozzáférést.
Hasonló, implementációspecifikus példa a glibc
_FILE_OFFSET_BITS beállítása: egyes 32 bites profilokon
típusokat és interfészleképezéseket változtat, míg a szokásos 64 bites
LP64 környezetben nem ugyanilyen változást okoz.
A reprodukcióhoz ezért a forrásverzió mellett őrizd meg a teljes fordítóparancsot, a kapcsolók sorrendjét, a konfigurációs headereket, az include-útvonalakat, a sysrootot, a fordító- és libc-verziót. A makrólista hasznos pillanatfelvétel, de nem helyettesíti ezeket.
Miért nem cáfolja az include-sorrend hibáját, ha a
macros.txt szerint AGENT_WIDE értéke 1? Nevezz
meg két további bizonyítékot.
Javítsd mindkét hibát. Ezután készíts külön előfeldolgozott fájlt
AGENT_WIDE=0 és AGENT_WIDE=1 beállítással. A
header működése ne függjön a kliens include-sorrendjétől.
Egy projekt Linuxon fordul, ezért automatikusan engedélyezi a
clock_gettime használatát. A _POSIX_C_SOURCE
definícióját a <stdio.h> után helyezi el. Magyarázd
meg a két külön hibát, és tervezz ellenőrzést futtatás nélkül.
A makrólista az előfeldolgozás végi állapotot mutatja. A korábbi feltételes ág kiválasztását nem módosítja egy későbbi definíció.
Az include-nyomvonal bizonyítja a fájlok feldolgozási sorrendjét, a
main.i pedig megmutatja a ténylegesen megmaradt 16-os
deklarációt. Az ellenőrzés akkor teljes, ha mindhárom adat ugyanahhoz a
fordítóparancshoz tartozik.
Az agent_api.h maga include-olja az
agent_config.h fájlt; a skálázó makró
((x) * 2) alakú.
A két előfeldolgozásban a kapacitás rendre 16 és 32, a skálázott
kifejezés mindkettőben ((1 + 3) * 2). További
ellenőrzésként egy kizárólag az agent_api.h fájlt
include-oló C17 fordítási egységnek is elfogadhatónak kell lennie.
A Linux-target azonosítása nem bizonyítja a szükséges libc-interfész láthatóságát vagy linkelhetőségét. Ettől függetlenül a késői feature-test definíció hatástalan lehet.
A javítás: _POSIX_C_SOURCE=200809L beállítása minden
rendszerheader előtt, majd a bemutatott próba fordítása és linkelése a
valódi target környezetében. Ellenőrzési feltétel a sikeres build;
futásidejű működést ebből nem szabad állítani.
A -E a tényleges tokensorozatot, a -dM a
végső makróállapotot, a -H az include-feldolgozás
nyomvonalát mutatja. Együtt alkalmasak az előfeldolgozási hibák okának
bizonyítására.
Az OS azonosítása, a fordító felismerése, az interfészek láthatóságának kérése és a képesség tényleges vizsgálata külön feladat. A konfigurációs makrókat pedig buildbemenetként kell kezelni: némelyikük az ABI-szerződés része.