Két gépen azonos sizeof értéket mérni még nem
bizonyítja, hogy egy struktúra bájtjai hordozható hálózati üzenetet
alkotnak. A natív memóriaelrendezést a nyelvi szabályok, a target ABI-ja
és a fordítási beállítások együtt határozzák meg. A hálózati
reprezentációt ezzel szemben a protokoll szerződésének kell
rögzítenie.
Ebben a laborban ugyanazt a C17 programot vizsgáljuk x86-64 és AArch64 környezetben. Külön dokumentáljuk a layoutot, a generált memória-hozzáféréseket és a protokolltesztek eredményét. A kiindulópont a Padding, alignment és offsetof, a Byte order és biztonságos szerializálás, valamint a Reprodukálható fordítási és diagnosztikai labor.
A példák minimumkövetelménye ISO C17. A két összehasonlítandó referencia-profil:
| Tulajdonság | x86-64 profil | AArch64 profil |
|---|---|---|
| Operációs környezet | Linux, hosted | Linux, hosted |
| ABI | x86-64 System V, LP64 | AAPCS64, LP64 |
| Bájt | CHAR_BIT == 8 |
CHAR_BIT == 8 |
| Byte order | little-endian | little-endian |
| Fordító | GCC 14.2.0 | GCC 14.2.0 |
| C könyvtár | glibc 2.40 | glibc 2.40 |
| Nyelvi mód | -std=c17 |
-std=c17 |
Ezek választott laborfeltételek, nem egy már elvégzett mérés környezeti adatai. Más kiadás használható, de külön profilként dokumentálandó. Rögzítsd a tényleges fordítóverziót, target triple-t, libc-verziót, fordítási kapcsolókat, kernelverziót, továbbá azt, hogy valódi hardveren vagy emulátorban futott-e a program.
Háromféle bizonyítékot különíts el:
sizeof C
bájtban adja meg az objektum méretét.Az alábbi konkrét layoutértékek előrejelzések; a kódhoz nem tartozik itt közölt futtatási eredmény.
Az üzenet három mezője: egy műveletazonosító, egy sorszám és egy
számláló. Az érvényes azonosítók 1 és 2;
mindkettőhöz a mezőtípusok teljes értéktartománya megengedett.
A wire format, vagyis a továbbított bájtsorozat formátuma pontosan hét oktett:
| Mező | Natív x86-64: várható offset/méret | Natív AArch64: várható offset/méret | Wire: offset/méret |
|---|---|---|---|
tag |
0 / 1 | 0 / 1 | 0 / 1 |
| belső padding | 1 / 3 | 1 / 3 | nincs |
sequence |
4 / 4 | 4 / 4 | 1 / 4 |
count |
8 / 2 | 8 / 2 | 5 / 2 |
| záró padding | 10 / 2 | 10 / 2 | nincs |
| teljes méret | 12 | 12 | 7 |
| igazítás | 4 | 4 | oktettenként feldolgozható |
A natív oszlopok az előbbi profilokra, szokásos ABI-layouttal, packingot módosító kapcsolók nélkül értendők. A wire formátumban a többoktettes mezők big-endian sorrendűek.
A dekóder pontosan egy teljes rekordot fogad:
P_SIZE;P_SIZE;tag: P_TAG;P_ARGUMENT;P_OK.Hiba esetén a kimeneti objektum változatlan marad. A függvény nem stream-framingot végez: több összefűzött rekordból a hívó választja ki a feldolgozandó hétoktettes tartományt.
A következő lab.c önálló C17 program. Az exact-width
típusokat és a nyolcbites C bájtot fordításkor ellenőrzi. Ezek a labor
követelményei, nem minden C implementációra igaz állítások.
#include <assert.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#if !defined(UINT16_MAX) || !defined(UINT32_MAX)
#error "This lab requires uint16_t and uint32_t"
#endif
#ifdef NDEBUG
#error "This test program requires enabled assertions"
#endif
_Static_assert(CHAR_BIT == 8, "This protocol lab requires octets");
enum { WIRE_SIZE = 7 };
enum status {
P_OK,
P_ARGUMENT,
P_SIZE,
P_TAG
};
struct message {
unsigned char tag;
uint32_t sequence;
uint16_t count;
};
static int valid_tag(unsigned char tag)
{
return tag == 1 || tag == 2;
}
static int same(struct message a, struct message b)
{
return a.tag == b.tag &&
a.sequence == b.sequence &&
a.count == b.count;
}
static enum status encode(unsigned char *dst, size_t n,
const struct message *src)
{
if (dst == NULL || src == NULL)
return P_ARGUMENT;
if (n != WIRE_SIZE)
return P_SIZE;
if (!valid_tag(src->tag))
return P_TAG;
dst[0] = src->tag;
dst[1] = (unsigned char)(src->sequence >> 24);
dst[2] = (unsigned char)(src->sequence >> 16);
dst[3] = (unsigned char)(src->sequence >> 8);
dst[4] = (unsigned char)src->sequence;
dst[5] = (unsigned char)(src->count >> 8);
dst[6] = (unsigned char)src->count;
return P_OK;
}
static enum status decode(const unsigned char *src, size_t n,
struct message *dst)
{
if (src == NULL || dst == NULL)
return P_ARGUMENT;
if (n != WIRE_SIZE)
return P_SIZE;
if (!valid_tag(src[0]))
return P_TAG;
struct message tmp = {
.tag = src[0],
.sequence = ((uint32_t)src[1] << 24) |
((uint32_t)src[2] << 16) |
((uint32_t)src[3] << 8) |
(uint32_t)src[4],
.count = (uint16_t)(((uint32_t)src[5] << 8) |
(uint32_t)src[6])
};
*dst = tmp;
return P_OK;
}
/* Külön vizsgálható, natív reprezentációt kezelő függvények. */
uint32_t load_aligned(const uint32_t *p)
{
return *p;
}
void store_aligned(uint32_t *p, uint32_t value)
{
*p = value;
}
uint32_t load_native_bytes(const unsigned char *p)
{
uint32_t value;
memcpy(&value, p, sizeof value);
return value;
}
void store_native_bytes(unsigned char *p, uint32_t value)
{
memcpy(p, &value, sizeof value);
}
static void expect_decode_error(const unsigned char *src, size_t n,
enum status expected)
{
const struct message before = {2, UINT32_MAX, UINT16_MAX};
struct message after = before;
assert(decode(src, n, &after) == expected);
assert(same(after, before));
}
int main(void)
{
printf("message: size=%zu align=%zu\n",
sizeof(struct message), _Alignof(struct message));
printf("offsets: tag=%zu sequence=%zu count=%zu\n",
offsetof(struct message, tag),
offsetof(struct message, sequence),
offsetof(struct message, count));
printf("uint32_t: size=%zu align=%zu\n",
sizeof(uint32_t), _Alignof(uint32_t));
const unsigned char golden[WIRE_SIZE] = {
0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab
};
const struct message expected = {
1, UINT32_C(0x01234567), UINT16_C(0x89ab)
};
unsigned char bytes[WIRE_SIZE];
struct message decoded = {0};
assert(encode(bytes, sizeof bytes, &expected) == P_OK);
assert(memcmp(bytes, golden, sizeof golden) == 0);
assert(decode(golden, sizeof golden, &decoded) == P_OK);
assert(same(decoded, expected));
const uint32_t sequences[] = {
0, 1, UINT32_C(0x80000000), UINT32_MAX
};
const uint16_t counts[] = {0, 1, UINT16_MAX};
for (unsigned char tag = 1; tag <= 2; ++tag) {
for (size_t i = 0;
i < sizeof sequences / sizeof sequences[0]; ++i) {
for (size_t j = 0;
j < sizeof counts / sizeof counts[0]; ++j) {
struct message input = {tag, sequences[i], counts[j]};
assert(encode(bytes, sizeof bytes, &input) == P_OK);
assert(decode(bytes, sizeof bytes, &decoded) == P_OK);
assert(same(input, decoded));
}
}
}
for (size_t n = 0; n < WIRE_SIZE; ++n)
expect_decode_error(golden, n, P_SIZE);
const unsigned char too_long[WIRE_SIZE + 1] = {1};
expect_decode_error(too_long, sizeof too_long, P_SIZE);
unsigned char bad_tag[WIRE_SIZE];
memcpy(bad_tag, golden, sizeof bad_tag);
bad_tag[0] = 3;
expect_decode_error(bad_tag, sizeof bad_tag, P_TAG);
expect_decode_error(NULL, WIRE_SIZE, P_ARGUMENT);
assert(decode(golden, sizeof golden, NULL) == P_ARGUMENT);
struct message invalid = expected;
invalid.tag = 0;
memset(bytes, 0xa5, sizeof bytes);
assert(encode(bytes, sizeof bytes, &invalid) == P_TAG);
for (size_t i = 0; i < sizeof bytes; ++i)
assert(bytes[i] == 0xa5);
puts("All protocol assertions passed.");
return 0;
}Fordítás mindkét target saját környezetében:
gcc -std=c17 -O2 -g -Wall -Wextra -Wpedantic -Wconversion \
lab.c -o labA tesztet külön laborlépésként indítsd el; a jegyzőkönyvbe a tényleges kimenetet és kilépési státuszt másold. Az utolsó üzenet a program várt sikerjelzése, nem itt igazolt teszteredmény.
| Hozzáférés | Tulajdonos és élettartam | Előfeltétel, invalidálás és hibakimenet |
|---|---|---|
encode, decode pointerei |
a hívó objektumai; kölcsönzött hozzáférés a hívás végéig | a nem null pointer megfelelő élő objektumra/tartományra mutat; a forrás és cél nem fed át |
load_aligned, store_aligned |
a hívó élő uint32_t objektuma |
megfelelő igazítás és olvashatóság/írhatóság; megsértésük UB |
load_native_bytes, store_native_bytes |
a hívó bájttartománya | legalább sizeof(uint32_t) elérhető C bájt; külön egész
típusú igazítás nem kell |
| tesztadatok | main automatikus objektumai |
élettartamuk a blokk végén ér véget; a függvények nem tárolnak el pointert |
Az élettartam vége vagy a tároló felszabadítása érvénytelenítené a kölcsönzött hozzáférést. A hosszparaméter nem bizonyítja a tároló tényleges méretét: a hazudott hossz bounds-hibához vezethet. Az API hibakódjai nem helyettesítik ezeket az előfeltételeket.
A golden byte vector a specifikációból, az encoder működésétől függetlenül előállított referencia-bájtsorozat. A példában:
Ezért a négy továbbított oktett 01 23 45 67. A
count hasonlóan 89 ab.
A round-trip tulajdonság:
ahol érvényes üzenet, az encoder, a decoder. Az egyenlőség itt mezőértékek egyenlőségét jelenti, nem a struktúra teljes objektumreprezentációjának azonosságát.
Bizonyításvázlat. Az encoder a sorszámot négy,
egymást nem átfedő nyolcbites részre bontja. A decoder ezeket eredeti
helyükre tolja, majd bitenkénti VAGY művelettel egyesíti. A számlálónál
ugyanez két résszel történik. A tag változatlanul
másolódik, ezért mindhárom mező helyreáll.
A teszt 24 határérték-kombinációt jár be, nem a teljes értéktartományt. A golden teszt azért is szükséges, mert két azonosan hibás, például little-endian encoder és decoder sikeres round-tripet adhatna.
A struktúrák összehasonlítására használt memcmp tipikus
hiba lenne: a padding nem logikai mező. A wire bájttömbök
összehasonlítására viszont itt megfelelő.
A már definiált négy hozzáférési függvény assemblyjét mindkét gépen állítsd elő:
gcc -std=c17 -O2 -S lab.c -o lab.sAArch64-en készíts külön változatot:
gcc -std=c17 -O2 -mstrict-align -S lab.c -o lab-strict.sGCC-specifikus kapcsoló: a
-mstrict-align AArch64 targeten használható; nem ISO C
tulajdonság.
A következőket keresd, ne egy előre feltételezett teljes utasítássorozatot:
| Függvény | x86-64-en lehetséges alak | AArch64-en lehetséges alak |
|---|---|---|
| igazított betöltés/tárolás | movl memóriaoperandussal |
ldr / str W regiszterrel |
bájtforrásból memcpy |
szintén egyetlen gépi betöltés | konfigurációtól függően ldr vagy kisebb betöltések |
bájtcélba memcpy |
szintén egyetlen gépi tárolás | konfigurációtól függően str vagy kisebb tárolások |
Ezek keresési támpontok, nem mért kimenetek. A memcpy
nem kötelezően függvényhívás: a fordító a kisméretű másolást
beépítheti.
A lényegi különbség a forrásprogram szerződése. A
uint32_t * dereferálásához megfelelően igazított
uint32_t objektum szükséges. A bájtforrásos
memcpy nem követel ilyen igazítást a forrástól. A fordító
ettől még használhat az adott környezetben megengedett unaligned gépi
betöltést.
Az AArch64 sem jelent általános „unaligned hozzáféréskor mindig összeomlik” szabályt. Az utasítás, memóriatípus és rendszerbeállítás is számít; a hardver toleranciája pedig nem teszi definiálttá a hibás C kódot.
Az alábbi bad.c szándékosan hibás, UB-t
tartalmazó diagnosztikai példa. Nincs garantált normál
kimenete.
#include <limits.h>
#include <stdint.h>
#ifndef UINT32_MAX
#error "This example requires uint32_t"
#endif
_Static_assert(CHAR_BIT == 8, "Octet-based lab");
_Static_assert(_Alignof(uint32_t) > 1,
"This experiment requires nontrivial alignment");
int main(void)
{
_Alignas(uint32_t)
unsigned char storage[sizeof(uint32_t) + 1] = {0};
const unsigned char *borrowed = storage + 1;
return (int)*(const uint32_t *)(const void *)borrowed;
}A storage a tároló tulajdonosa; a borrowed
a blokk végéig élő, kölcsönzött bájthozzáférés. A tartomány elég hosszú,
de az egybájtos eltolás miatt a cím nem megfelelően igazított. Ráadásul
a deklarált bájttömbön keresztül így nem kapunk szabályos
uint32_t objektumhozzáférést sem.
gcc -std=c17 -O1 -g -Wall -Wextra \
-fsanitize=alignment -fno-sanitize-recover=alignment \
bad.c -o badA sanitizer várhatóan hibás igazítású betöltést jelez; a pontos szöveg és forráshely a fordítókiadástól függ. A jegyzőkönyvben az elvárt igazítást és az érintett műveletet azonosítsd. Egyetlen lelet nem a program összes hibájának felsorolása.
A helyes natív javítás a korábbi load_native_bytes:
megfelelően igazított helyi uint32_t objektumba másolunk. A
labor exact-width unsigned típusánál minden bitminta érvényes értéket
képvisel.
Ez azonban nem byte-order konverzió. Wire adatokhoz
a decode explicit mezőösszeállítása kell. A cast
eltüntetése javíthatja az objektumhozzáférést, de önmagában nem
valósítja meg a protokollt.
Futtasd a mérőprogramot mindkét profilon. Készíts táblázatot a tényleges méretről, igazításról és offsetekről. Indokold, miért nem bizonyít hordozható wire formátumot az eredmények egyezése.
Egészítsd ki a tesztet úgy, hogy az összes lehetséges oktettértéket
ellenőrizze tag mezőként. Érvénytelen tag esetén ellenőrizd
a kimenet változatlanságát is.
Dokumentáld a hibás program sanitizer-leletét. Készíts javított
változatot memcpy-vel, majd hasonlítsd össze a négy
hozzáférési függvény assemblyjét mindkét targeten. AArch64-en a
-mstrict-align változatot is vizsgáld.
A referencia-profilokon a várakozás: sizeof == 12,
_Alignof == 4, az offsetek pedig 0,
4, 8. Eltéréskor először a tényleges ABI-t,
kapcsolókat és típusokat ellenőrizd.
Az egyezés csak két konkrét profilra vonatkozó megfigyelés. A natív reprezentáció paddingot tartalmaz, mezősorrendjének változtatása módosíthatja a layoutot, és a little-endian mezők nem a protokoll big-endian reprezentációját adják. Ellenőrzési feltétel: a mérési táblázat mellé teljes környezeti profil tartozik.
A következő C17 részlet a main függvénybe illeszthető, a
golden deklarációja után:
for (unsigned int t = 0; t < 256; ++t) {
unsigned char candidate[WIRE_SIZE];
memcpy(candidate, golden, sizeof candidate);
candidate[0] = (unsigned char)t;
if (t == 1 || t == 2) {
struct message result = {0};
assert(decode(candidate, sizeof candidate, &result) == P_OK);
assert(result.tag == t);
} else {
expect_decode_error(candidate, sizeof candidate, P_TAG);
}
}A ciklusváltozó szándékosan nem unsigned char: annak
körbefordulása könnyen végtelen ciklust okozna. Ellenőrzési feltétel:
két elfogadott és 254 elutasított tag, hibánál változatlan kimenet.
A javított bad.c tartalmazza a
<string.h> headert, és a hibás castos olvasás helyett
ezt használja:
uint32_t value;
memcpy(&value, borrowed, sizeof value);
return (int)value;Ez a megadott, nullákkal feltöltött bemenetre vonatkozó részlet. Ellenőrzési feltétel: ezen a hozzáférésen nincs alignment-lelet, és a bounds továbbra is helyes.
Az assemblyben a memcpy akár ugyanarra az utasításra
egyszerűsödhet, mint a hibás castos olvasás. Ettől a két C program
helyessége nem lesz azonos: az utasításazonosság nem bizonyítja a nyelvi
előfeltételek teljesülését.
A layoutmérés egy konkrét build tulajdonságait mutatja meg; a wire formátumot külön szerződés rögzíti. A golden teszt a reprezentációt, a round-trip az oda-vissza értékmegőrzést, a negatív tesztek a hibaszerződést ellenőrzik. Az assembly megmagyarázza a végrehajtást, a sanitizer pedig hibákat tárhat fel, de egyik sem helyettesíti az igazítási, objektum-, bounds- és élettartam-előfeltételek indoklását.