A memória olvashatósága nem pusztán cím- és lapvédelmi kérdés. Egy tartomány lehet a folyamat számára írható, megfelelő méretű és megfelelően igazított, miközben egy adott C típuson keresztüli hozzáférés mégis nem definiált viselkedést okoz. A fordító ugyanis nem csupán bájtokat lát: az objektumok típusára és a hozzáférések kapcsolatára is támaszkodik.
Az Inicializálás, indeterminate érték és reprezentáció rész alapján már különbséget teszünk érték és objektumreprezentáció között. A Deklarációk, typedef és típusminősítők pedig megadta a típusdeklarációk alapját. Most azt vizsgáljuk, milyen típusú kifejezéssel szabad egy objektum tárolt értékéhez hozzáférni.
A nyelvi szabályok alapja ebben a részben ISO C17. A konkrét gépi reprezentáció, illetve a fordítói optimalizálás ettől külön kezelendő.
Az effective type, azaz a hozzáférés megítéléséhez használt effektív típus nem azonos a pointer változó típusával. Egy cast megváltoztathatja egy pointerkifejezés típusát, de ettől a célobjektum típusa nem változik meg.
Ha az objektumnak van deklarált típusa, az effektív típusa a deklarált típusa.
/* ISO C17, szemléltető részlet. */
unsigned int counter = 12u;
unsigned char buffer[64];A counter típusa unsigned int; a
buffer karakterelemekből álló tömb. A tömb nem „típus
nélküli nyers memória”, még akkor sem, ha kizárólag átmeneti tárolóként
használjuk.
Ezért egy unsigned int reprezentációjának bemásolása a
buffer elejére nem hoz létre ott deklarált
unsigned int objektumot. A visszaolvasás helyes
módszere egy megfelelő típusú célobjektumba végzett másolás, nem pedig a
karaktertömb címének átalakítása és dereferálása. A megfelelő igazítás
önmagában ezt nem javítaná ki.
A malloc által biztosított objektumnak nincs deklarált
típusa. A C17 ezért a hozzáférés módjából határozza meg az effektív
típust:
memcpy vagy memmove másol bele értéket,
az effektív típus a forrásobjektum effektív típusa lesz — ha a forrásnak
van ilyen típusa — a másoláshoz és az azt követő, értéket nem módosító
hozzáférésekhez.Az effektív típus tehát nem feltétlenül az allokáció teljes élettartamára szóló, megváltoztathatatlan címke. Másfelől a pointer átírása vagy castolása nem típusváltó művelet.
| Tároló | Van deklarált típusa? | Mi határozza meg az effektív típust? |
|---|---|---|
unsigned int x; |
Igen | A deklaráció |
unsigned char bytes[32]; |
Igen | A deklaráció; másolástól sem lesz egész objektum |
malloc eredménye |
Nincs | A C17 hozzáférési és másolási szabályai |
Deklarált objektumba végzett memcpy |
Igen | A cél deklarált típusa változatlan |
Az lvalue itt objektumot kijelölő kifejezés: például
az x vagy a *p. Nem csak íráskor használunk
lvalue-ot; a result = *p; kifejezésben a *p
olvasás tárgya.
Aliasingról akkor beszélünk, amikor különböző
hozzáférési utak ugyanazt az objektumot vagy egymást átfedő tárolót érik
el. Ez önmagában nem hiba. Két int * teljesen szabályosan
mutathat ugyanarra az int objektumra.
Az ISO C17 típuson alapuló hozzáférési szabálya, amelyet gyakran strict aliasing szabálynak neveznek, megengedi a tárolt érték elérését olyan lvalue-val, amelynek típusa:
const
változat;Az összetett típusokra vonatkozó kivétel például lehetővé teszi, hogy a fordító számoljon egy struktúrára és annak egész típusú tagjára mutató pointer átfedésével. Nem jogosít fel arra, hogy egy önálló egész objektum köré képzeletbeli struktúrát olvassunk: a méret- és objektumhatárok továbbra is érvényesek.
Egy int objektum olvasása const int
lvalue-val rendben van. A megfelelő unsigned int típus
szintén külön megengedett eset. A float viszont nem válik
kompatibilissé az int típussal attól, hogy egy platformon
azonos méretűek.
A hozzáférés helyessége több feltétel együttes teljesülését követeli meg. Ha egy hozzáférés helyes, akkor mind az öt teljesül:
Ez szükséges feltételek összefoglalása, nem teljes helyességi bizonyítás: például az adatverseny tilalmát külön is teljesíteni kell. Maga a pointerkonverzió is problémás lehet, ha az eredmény nem megfelelően igazított.
ISO C17: egy objektum reprezentációja karaktertípusú
lvalue-on keresztül hozzáférhető. C-ben ide tartozik a
char, a signed char és az
unsigned char; reprezentáció vizsgálatához általában az
utóbbi a legegyértelműbb.
Az objektumreprezentáció mérete sizeof object C
bájt. Egy C bájt CHAR_BIT bitből áll; a nyelvi
szabály nem feltételez nyolcbites oktetteket.
A kivétel iránya lényeges:
Egy egész objektum reprezentációját szabad karakterként vizsgálni; egy karaktertömböt ettől még nem szabad tetszőleges egész objektumként olvasni.
A karakteres írás sem veszélytelen. Egy reprezentáció bájtjainak önkényes módosítása létrehozhat olyan bitmintát, amely a későbbi típusos olvasáskor nem érvényes.
A következő program két eltérő esetet mutat:
A programban szereplő add_u függvény
restrict szerződését a következő rész bontja ki.
Fordítási profil: ISO C17, GNU bővítések nélkül. A helyi laborhoz választott profil Linux/x86-64 System V ABI, GCC 14.2.0 vagy Clang 19.1.0, glibc 2.40. A program nem használ konkrét típusméretet vagy bájtsorrendet. Az alábbi parancsok helyi ellenőrzésre szolgálnak; nem dokumentálnak itt elvégzett fordítást vagy futtatást.
/* representation.c */
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/*
* Szerzodes:
* - n > 0 eseten dst es src legalabb n elo elemre mutat;
* - dst elemei irhatok, src elemei olvashatok;
* - a ket feldolgozott tartomany nem fed at;
* - a fuggveny nem tartja meg a kolcsonzott pointereket.
*/
static void add_u(size_t n,
unsigned int *restrict dst,
const unsigned int *restrict src)
{
for (size_t i = 0; i < n; ++i) {
dst[i] += src[i];
}
}
int main(void)
{
unsigned int original = 0x1234u;
unsigned char image[sizeof original];
unsigned int restored;
memcpy(image, &original, sizeof original);
memcpy(&restored, image, sizeof restored);
assert(restored == original);
unsigned int *copy = malloc(sizeof *copy);
if (copy == NULL) {
return EXIT_FAILURE;
}
memcpy(copy, &original, sizeof *copy);
assert(*copy == original);
unsigned int destination[3] = {1u, 2u, 3u};
unsigned int source[3] = {10u, 20u, 30u};
add_u(3, destination, source);
assert(destination[0] == 11u);
assert(destination[1] == 22u);
assert(destination[2] == 33u);
free(copy);
copy = NULL;
return EXIT_SUCCESS;
}A parancsokban a fordítónevek a fenti verziókat jelöljék:
gcc -std=c17 -O2 -Wall -Wextra -Wpedantic representation.c -o representation-gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic representation.c -o representation-clang| Objektum vagy hozzáférés | Tulajdonos és élettartam | Invalidálás, hibakimenet |
|---|---|---|
original, image,
restored |
A main automatikus objektumai |
A blokk elhagyásakor megszűnnek |
copy által elért tároló |
A felszabadításért a main felel; sikeres
malloc-tól free-ig él |
free érvényteleníti; allokációs hiba esetén
EXIT_FAILURE |
source, destination |
A main automatikus tömbjei |
A blokk elhagyásakor megszűnnek |
src, dst |
Az add_u kölcsönzött hozzáférései |
Csak a hívás alatt használatosak; szerződéssértésre nincs hibakód, UB következhet |
Helyességi bizonyításvázlat. Az első másolás
pontosan sizeof original C bájtot ment el. Az
image közben nem módosul. A második másolás ugyanannyi
bájtot helyez egy azonos típusú, élő célobjektumba, ezért az eredeti,
érvényes érték reprezentációját állítja vissza. A padding esetleges
jelenléte ezt nem akadályozza.
A sikeres malloc elegendő méretű és az
unsigned int számára megfelelően igazított tárolót ad. A
memcpy típusos forrása miatt az allokált cél effektív
típusa a következő olvasásra unsigned int. A
*copy még a felszabadítás előtt történik.
memcpy
nem garantálA memcpy reprezentációt másol, nem numerikus értéket
konvertál. Eltérő típusok közötti másolásnál külön kell igazolni,
hogy:
Átfedéshez memmove szükséges, de az sem old meg típus-
vagy reprezentációs hibát.
Az egyenlő méret önmagában kevés. Egy egész bitmintája nem feltétlenül érvényes lebegőpontos vagy pointerreprezentáció. Külső adatból származó bájtok közvetlen bemásolása ezért nem helyettesíti a protokoll szerinti dekódolást.
Azonos értékű objektumok paddingje eltérhet, ezért a
memcmp általában nem értékegyenlőség-vizsgálat. A
memóriabeli pillanatkép pedig nem automatikusan hordozható
fájlformátum.
A type punning ugyanazon reprezentáció más típus szerinti értelmezése. C-ben a union erre külön nyelvi lehetőséget ad.
/* ISO C17, feltetelesen hasznalhato szemlelteto reszlet. */
union numeric_view {
float f;
unsigned int u;
};
union numeric_view value = {.f = 1.0f};
unsigned int bits = value.u;ISO C17: ha nem azt a uniontagot olvassuk, amelyen keresztül utoljára tároltunk, az érték reprezentációjának megfelelő részét az olvasott tag típusának reprezentációjaként értelmezzük újra. Ez csapdareprezentációhoz is vezethet. Ha az olvasott tag nagyobb, a korábban írt tagon túli bájtok értéke nem specifikált.
A részlet használatához ezért dokumentált reprezentációs feltételek
szükségesek. Nem állítjuk, hogy az 1.0f valamely
univerzális egész bitmintát eredményez. Az unsigned int
szélessége és a float formátuma sincs itt rögzítve.
A unionos forma nem azonos egy önálló float objektum
unsigned int *-en keresztüli olvasásával. A union puszta
deklarálása máshol sem tesz szabályossá tetszőleges pointeres type
punningot.
C/C++ határ: ezt a C-s lehetőséget nem szabad automatikusan C++-ra átvinni; ott az aktív uniontag és az objektumélettartam szabályait külön kell alkalmazni.
| Recept | C17 szerinti minősítés |
|---|---|
Objektum olvasása unsigned char-ként |
Megengedett reprezentációs hozzáférés |
| Egész objektum olvasása nem kompatibilis lebegőpontos lvalue-val | Nem megengedett |
| Ismert érvényes reprezentáció visszamásolása azonos típusba | Megfelelő méret és élettartam mellett helyes |
| Karaktertömb castolása típusos pointerré, majd olvasás | A cast nem létesít megfelelő típusú objektumot |
| Másik uniontag olvasása | C-s reprezentáció-újraértelmezés, reprezentációs korlátokkal |
restrict: külön
szerződés az aliasingrólA restrict nem azt jelenti, hogy két pointer címe soha
nem lehet azonos. Hozzáférési szerződés, amelynek jelentése a blokk
végrehajtásához és a ténylegesen elért objektumokhoz kötődik.
Az itt vizsgált egyszerű esetben: ha egy objektumot egy
restrict pointerből származó hozzáféréssel elérünk, és azt
a blokk végrehajtása során módosítjuk is, akkor az objektum összes
érintett hozzáférésének ugyanahhoz a korlátozott hozzáférési útvonalhoz
kell tartoznia. A pointer másolatai és belőle képzett elemcímek lehetnek
ilyen származtatott utak. A részletes szabály a mutatott típus
const minősítését is korlátozza, ha a hozzáfért objektum
módosul.
Az add_u függvényben a dst[i] olvasódik és
módosul. Ha ugyanaz az elem a src útvonalon is olvasódik,
sérül a szerződés. Az API ezért egymást nem átfedő feldolgozott
tartományokat követel.
add_u(n, a, a) hívás pozitív n esetén
sérti a szerződést.A csak olvasott, sehol nem módosított objektumok aliasolását a
restrict önmagában nem tiltja.
Optimalizálási következmény: a fordító a típus- és
restrict szerződésekre támaszkodva tarthat értékeket
regiszterben, átrendezhet műveleteket vagy vektorizálhat ciklusokat. A
megsértett szerződés nem „lassabb kódot”, hanem UB-t jelent.
A GCC/Clang -fno-strict-aliasing kapcsolója
implementációspecifikus optimalizálási beállítás, nem C17-es helyességi
bizonyítás. Nem javítja ki a dangling pointert, a hibás igazítást vagy a
restrict megsértését. A warningok és sanitizerek hiánya sem
bizonyítja e szerződések teljesülését.
Egy élő, inicializált int x objektumot
const int, unsigned int, float,
illetve unsigned char lvalue-on keresztül szeretnénk
olvasni. Melyik hozzáférést engedi a típusszabály? A pointerképzés egyéb
feltételeit tekintsd teljesítettnek.
Írj C17-részletet, amely egy double objektum érvényes
értékét karaktertömbbe menti, majd egy másik double
objektumba visszaállítja. Ne feltételezz konkrét méretet vagy
reprezentációt.
Egy fejlesztő az add_u(3, values, values) hívást
használja egy legalább háromelemű, inicializált
unsigned int tömb elemeinek megduplázására. Optimalizálás
nélkül a várt értékeket látja. Miért hibás a szerződés? Adj olyan
javítást, amely megőrzi az eredeti add_u nem átfedő
tartományokra szóló interfészét.
A const int kompatibilis típus minősített változata,
ezért megengedett. Az unsigned int a megfelelő előjel
nélküli típus, ezért szintén megengedett. A float nem
tartozik a kivételekhez. Az unsigned char reprezentációs
hozzáférésként megengedett, de egyetlen ilyen olvasás nem az egész
int numerikus értékét adja.
Ellenőrzési feltétel: a típusszabály teljesülése mellett az objektum él, inicializált, és a hozzáférés a határain belül marad.
/* ISO C17-reszlet; a forditasi egyseghez: #include <string.h> */
double source = 1.25;
unsigned char snapshot[sizeof source];
double destination;
memcpy(snapshot, &source, sizeof source);
memcpy(&destination, snapshot, sizeof destination);A három objektum ugyanazon blokk automatikus objektuma; a
memcpy csak a hívások idejére kap kölcsönzött hozzáférést.
Nincs dinamikus allokáció vagy hibakód. A blokk elhagyása lezárja az
élettartamukat.
Ellenőrzési feltétel: a pillanatkép nem módosul a
két másolás között, minden objektum él, és a másolások tartományai nem
fednek át. Ebben a példában destination == source teljesül.
Ez nem általános lebegőpontos egyenlőségteszt minden lehetséges értékre,
például NaN-ra.
restrict-hiba javításaA hívás ugyanazokat az objektumokat módosítja a dst, és
olvassa a független src útvonalon. Ez pozitív elemszámnál
szerződéssértés. Az optimalizálás nélküli megfigyelés nem teszi
helyessé.
A megduplázás külön, egyetlen hozzáférési utat használó művelet lehet:
/* ISO C17-reszlet: values elo, irhato, legalabb haromelemu tomb. */
for (size_t i = 0; i < 3; ++i) {
values[i] += values[i];
}A tömb tulajdonosa a hívó; a ciklus nem hosszabbítja meg az
élettartamát és nem tart meg pointert. Az add_u
változatlanul megmarad nem átfedő tartományokhoz.
Ellenőrzési feltétel: legalább három inicializált elem legyen írható. Az előjel nélküli összeadás túlcsordulása definiált moduloaritmetika; ha ez alkalmazási szinten nem elfogadható, külön tartományellenőrzés szükséges.
A tároló címe és mérete nem elegendő a típusos hozzáférés igazolásához. A deklarált típus, az allokált objektum effektív típusa és az lvalue típusa együtt határozza meg, mely hozzáférések megengedettek.
A karakteres hozzáférés reprezentációt vizsgál; a memcpy
reprezentációt másol; egyik sem általános értékkonverzió. A unionos type
punning C-specifikus feltételekhez kötött. A restrict pedig
külön hozzáférési ígéret: megsértése ugyanolyan helyességi hiba, mint
egy tiltott típuson keresztüli olvasás.