10.1 Inline assembly GCC-ben

10.1.1 Motiváció: az utasítás mellett a szerződést is megírjuk

Tegyük fel, hogy egy C-ben írt többpontosságú aritmetikai modulhoz az összeg mellett az előjel nélküli átvitelre is szükségünk van. Az A64 ADDS kiszámítja az összeget és beállítja az állapotjelzőket; az A64 CSET ezekből közönséges egész értéket készíthet. Hogyan illesszük ezt a két utasítást C-kódba úgy, hogy optimalizálás mellett is helyesen működjön?

A GCC bővített beágyazott assemblyje (extended inline assembly) utasítássablont és fordítói adatfüggőségi szerződést kapcsol össze. Megadjuk, mit olvas a blokk, mit állít elő, és milyen további állapotot módosít.

A helyes utasítássor önmagában nem elegendő. A fordító a deklarált operandusokból és mellékhatásokból dolgozik; az assemblyszövegből nem vezeti le automatikusan a teljes viselkedést.

Az alfejezet minden utasítássablonja A64 ISA, AArch64, ARMv8-A alapprofil, little-endian környezetben. Thumb-2 kód itt nem szerepel. Feltételezzük az egész aritmetika, az állapotjelzők és az AAPCS64 alapjainak ismeretét; a hívási szerződés előzménye Az AAPCS64 regiszterszerződés.

10.1.2 A bővített szintaxis és az operandusok

A GCC C-kiterjesztéseként használt forma:

/* Szintaxisvázlat, nem önállóan fordítható program.
   A sablon ebben az alfejezetben A64 utasításokat tartalmaz. */
__asm__ volatile (
    "utasítássablon"
    : kimeneti_operandusok
    : bemeneti_operandusok
    : clobber_lista
);

A __asm__ írásmód szigorúbb C-nyelvi móddal, például -std=c11 mellett is használható GCC-kiterjesztésként. Nem A64 utasítás és nem assemblerdirektíva.

A négy rész szerepe:

  1. Utasítássablon: C-karakterlánc, amelybe a GCC behelyettesíti az operandusokat.
  2. Kimenetek: a blokk által előállított vagy módosított C-értékek.
  3. Bemenetek: a blokk által felhasznált C-értékek.
  4. Clobber-lista: a kimenetként nem modellezett állapotmódosítások felsorolása.

Az operandus alakja például [sum] "=r" (sum). A szögletes zárójelben szereplő név a sablonbeli hivatkozás neve, a karakterlánc az operandus-megkötés (constraint), a zárójelben pedig C-kifejezés áll. A kimenetnek írható C-objektumot kell jelölnie.

GCC-megkötés Jelentés az itteni AArch64-példákban
"r" A bemeneti értéket általános célú regiszterben kérjük.
"=r" Csak írt regiszterkimenet; a korábbi érték nem bemenet.
"+r" Ugyanaz az operandus bemenet és kimenet is.
"=&r" Korán felülírt, csak írt regiszterkimenet.

Az "r" nem konkrét regisztert választ. A GCC dönt az elhelyezésről, ezért a kód különböző optimalizálásokkal más regisztereket használhat.

A sablonban %x[name] az operandus A64 Xn, 64 bites nézetét, %w[name] az A64 Wn, 32 bites nézetét írja ki. Ezek GCC-sablonmódosítók, nem utasítások. Nem végeznek C-típuskonverziót: a C-típus és a kiválasztott utasításszélesség összhangjáért mi felelünk.

10.1.2.1 Mikor osztozhat bemenet és kimenet egy regiszteren?

Az "=r" önmagában megengedi, hogy a kimenet valamelyik bemenet regiszterét kapja. Egyetlen A64 ADD esetén ez helyes: az utasítás a forrásokból számol, majd elhelyezi az eredményt.

Többutasításos blokknál viszont előfordulhat, hogy az első utasítás már felülírja a kimenetet, egy későbbi pedig még olvasna egy bemenetet. Ilyenkor az adott kimeneten &, azaz korai felülírás (early clobber) szükséges.

Ez nem stíluskérdés: nélküle a GCC olyan regiszterátfedést választhat, amely tönkreteszi a még szükséges bemenetet. Olvasott és írt operandusnál ugyanez a jelölés "+&r" lehet.

Teljes ellenpélda: két utasítás, egy elveszett bemenet. Számítsuk ki a 2*b + a értéket két A64 utasítással. Az első utasítás már a kimenetet írja, a második még olvassa az a bemenetet. Mentsük lab-early.c néven a ~/arm-labor/a64/a036/ munkakönyvtárba (lásd a kidolgozott példa elején):

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

/* HIBÁS: az első utasítás már írja a kimenetet, a második még olvassa a-t. */
__attribute__((noinline))
static uint64_t lab_2b_plus_a_bad(uint64_t a, uint64_t b)
{
    uint64_t out;

    __asm__(
        "lsl %x[out], %x[b], #1\n\t"     /* out = 2*b   (kimenet írása)  */
        "add %x[out], %x[out], %x[a]"    /* out += a    (a még kell!)    */
        : [out] "=r" (out)
        : [a] "r" (a), [b] "r" (b)
    );
    return out;
}

/* JAVÍTOTT: "=&r" — a kimenet nem kaphatja egyik bemenet regiszterét sem. */
__attribute__((noinline))
static uint64_t lab_2b_plus_a_good(uint64_t a, uint64_t b)
{
    uint64_t out;

    __asm__(
        "lsl %x[out], %x[b], #1\n\t"
        "add %x[out], %x[out], %x[a]"
        : [out] "=&r" (out)
        : [a] "r" (a), [b] "r" (b)
    );
    return out;
}

int main(void)
{
    uint64_t bad = lab_2b_plus_a_bad(1, 10);
    uint64_t good = lab_2b_plus_a_good(1, 10);

    printf("bad=%" PRIu64 " good=%" PRIu64 "\n", bad, good);
    return good == 21 ? 0 : 1;
}
# Target: AArch64 Linux, qemu-user; a hibás változat szándékos.
for opt in 0 2; do
    aarch64-linux-gnu-gcc -std=c11 -Wall -Wextra -march=armv8-a \
        -O"$opt" -static lab-early.c -o "lab-early-O${opt}"
    qemu-aarch64 "./lab-early-O${opt}"
done
aarch64-linux-gnu-objdump -d --disassemble=lab_2b_plus_a_bad lab-early-O2

A helyes eredmény a = 1, b = 10 mellett 21. A hibás változatban a fordító "=r" alapján jogosan adhatja a kimenetnek az a regiszterét — noinline függvényben ez gyakran az X0, amely egyben a visszatérési regiszter is. Ekkor a lsl x0, x1, #1 felülírja az a-t, és az add x0, x0, x0 a 4*b = 40 értéket adja. Hogy ez bekövetkezik-e, a regiszterkiosztáson múlik: a mi mérésünkben (lásd a Források szakaszt) ugyanaz a forrás -O0 mellett bad=40, -O2 mellett bad=21 eredményt adott. A javított változat "=&r" megkötése minden szinten kizárja az átfedést, ezért good=21. A disassemblyben a hibás függvény kimenetét és bemeneteit kell összevetni; ha a kimenet regisztere azonos egy olyan bemenetével, amelyet egy későbbi utasítás még olvas, az a hiba.

Mikor nem kell &? Ha minden bemenet elfogy, mielőtt a blokk bármelyik kimenetet írná. A kidolgozott példa add64_carry blokkjában az ADDS egyszerre olvassa a két bemenetet és írja a sum-ot, a CSET pedig már csak az NZCV-t olvassa. Ott a korai felülírás jelölése felesleges lenne, és csak a regiszterkiosztást szűkítené.

10.1.3 Mit jelent, és mit nem jelent a volatile?

Ha egy nem volatile blokk eredményét senki nem használja, a GCC eltávolíthatja. Azonos bemenetű számítást megfelelő feltételek mellett összevonhat vagy kiemelhet egy ciklusból.

Tisztán számoló blokk esetén ez kívánatos. Ha az eredmény szükséges, az adatfüggőségek biztosítják az előállítását; nem kell automatikusan volatile.

A volatile többek között azt jelzi, hogy a blokk nem dobható el pusztán azért, mert a kimeneteire nincs szükség. Kimenet nélküli bővített asm blokkot a GCC implicit módon is volatile-ként kezel.

A volatile azonban:

A volatile nem helyettesíti a pontos operandus- és clobber-listát.

10.1.4 A clobber-lista három fontos esete

Állapotjelzők. Az A64 ADDS módosítja az A64 NZCV állapotjelzőket, ezért az ilyen blokk clobber-listájában "cc" szerepeljen. Az A64 ADD nem módosítja ezeket. A "cc" nem eredményoperandus: csak közli, hogy a korábbi feltételkód-állapot nem tekinthető megőrzöttnek.

Rögzített munkaregiszter. Ha a sablon közvetlenül felülírja az A64 X9 regisztert, ezt "x9" clobberrel kell jelezni, amennyiben nem operandusként modellezzük. A GCC ezután nem helyezi a blokk operandusait ebbe a regiszterbe, és nem feltételezi korábbi tartalmának megőrzését.

Az A64 X9 hívó által mentendő volta nem mentség: az inline blokk nem függvényhívás. A fordító nem alkalmaz rá automatikusan hívási regiszterrombolási szabályokat. Általában jobb a munkaregisztert operandusként kérni, mint rögzíteni.

Memória. A "memory" azt jelzi, hogy a blokk az operandusokkal pontosan le nem írt memóriát is olvashatja vagy módosíthatja. Emiatt a fordító nem tarthat fenn vele összeegyeztethetetlen memóriaérték-feltételezéseket.

Egy pointer "r" bemenetként átadása csak a cím értékének használatát jelenti. Nem mondja meg, hogy a címzett objektumot olvassuk vagy írjuk. Ezt pontos memóriaoperandussal vagy konzervatívabban "memory" clobberrel kell leírni. A "memory" fordítói korlát, nem A64 hardveres barrier, és önmagában nem oldja meg a szálak közötti adatversenyeket.

10.1.5 Kidolgozott példa: összeg és előjel nélküli átvitel

Munkakönyvtár: ~/arm-labor/a64/a036/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a036
cd ~/arm-labor/a64/a036

A következő teljes programban az első függvény egyszerű "r" bemeneteket és "=r" kimenetet használ. A második két eredményt állít elő, és deklarálja az állapotjelzők módosítását.

Mentsük lab-inline.c néven:

#include <stdint.h>
#include <stdio.h>

static inline uint64_t add64(uint64_t a, uint64_t b)
{
    uint64_t sum;

    /* A64 ISA: 64 bites ADD; nem módosítja az NZCV-t. */
    __asm__(
        "add %x[sum], %x[a], %x[b]"
        : [sum] "=r" (sum)
        : [a] "r" (a), [b] "r" (b)
    );

    return sum;
}

static inline uint64_t add64_carry(uint64_t a, uint64_t b,
                                   unsigned *carry_out)
{
    uint64_t sum;
    unsigned carry;

    /* A64 ISA: ADDS + CSET; a CS feltétel jelentése C == 1. */
    __asm__(
        "adds %x[sum], %x[a], %x[b]\n\t"
        "cset %w[carry], cs"
        : [sum] "=r" (sum), [carry] "=r" (carry)
        : [a] "r" (a), [b] "r" (b)
        : "cc"
    );

    *carry_out = carry;
    return sum;
}

int main(void)
{
    static const uint64_t cases[][2] = {
        { UINT64_C(0), UINT64_C(0) },
        { UINT64_C(7), UINT64_C(5) },
        { UINT64_MAX,  UINT64_C(1) },
        { UINT64_MAX,  UINT64_MAX }
    };

    for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
        uint64_t a = cases[i][0];
        uint64_t b = cases[i][1];
        uint64_t expected = a + b;
        unsigned expected_carry = expected < a;
        unsigned carry;
        uint64_t sum = add64_carry(a, b, &carry);

        if (add64(a, b) != expected ||
            sum != expected || carry != expected_carry) {
            fprintf(stderr, "Hiba a(z) %u. tesztnel\n", i);
            return 1;
        }
    }

    puts("lab-inline: 4 teszt OK");
    return 0;
}

A C-referencia előjel nélküli aritmetikát használ, ezért a körbefordulás definiált. Ha a matematikai összeg eléri a 2642^{64} értéket, az eltárolt összeg 2642^{64}-gyel kisebb nála, és kisebb lesz az első összeadandónál. Ez indokolja az expected < a átvitelvizsgálatot.

A várható, levezetett eredmények:

Első bemenet, előjel nélküli Második bemenet, előjel nélküli 64 bites összeg Átvitel
0 0 0x0000000000000000 0
7 5 0x000000000000000c 0
0xffffffffffffffff 1 0x0000000000000000 1
0xffffffffffffffff 0xffffffffffffffff 0xfffffffffffffffe 1

A második blokkban nincs szükség korai felülírásra: az A64 ADDS az összes bemeneti regisztert felhasználja, mielőtt a későbbi A64 CSET lefutna. Utóbbi már csak az állapotjelzőktől függ. A két utasítás egy blokkban marad, így a fordító nem illeszt közéjük más, állapotjelzőket módosító utasítást.

A carry_out címére írást a C-kód végzi, a fordító számára láthatóan. Emiatt ehhez a blokkhoz nem szükséges "memory".

10.1.5.1 Fordítás, linkelés, futtatás és vizsgálat

Az Ubuntu 24.04 LTS cross-környezetében:

# Target: AArch64 Linux, qemu-user; ARMv8-A, little-endian.
# A GCC lépése C-fordítást, assemblálást és statikus linkelést végez.
for opt in 0 2 3; do
    aarch64-linux-gnu-gcc \
        -std=c11 -Wall -Wextra -Werror \
        -march=armv8-a -mlittle-endian -O"$opt" -static \
        lab-inline.c -o "lab-inline-O${opt}" || exit 1

    qemu-aarch64 "./lab-inline-O${opt}"
    status=$?
    printf 'O%s: kilepesi allapot=%s\n' "$opt" "$status"
    test "$status" -eq 0 || exit 1
done

# Target: ugyanazon qemu-user program A64 kódjának statikus vizsgálata.
aarch64-linux-gnu-gcc \
    -std=c11 -march=armv8-a -mlittle-endian -O2 \
    -S -fverbose-asm lab-inline.c -o lab-inline-O2.s

aarch64-linux-gnu-objdump -d lab-inline-O2 > lab-inline-O2.dump

A statikus linkeléshez a célarchitektúra libc-fejlesztői állományai is szükségesek. Hiányuk környezeti hiba, nem inline-asm probléma; az alapbeállítást a Labor-alapkörnyezet: eszközlánc telepítése és verifikáció tárgyalja.

Várt, itt nem futtatott eredmény: mindhárom változat kiírja a lab-inline: 4 teszt OK sort, és 0 állapottal lép ki.

A generált assemblyben keressük az A64 ADDS–CSET párt. A konkrét regiszterszám nem ellenőrzési feltétel: azt a GCC választja. A disassembler feltételkód-szinonimát is megjeleníthet.

10.1.6 Hibakeresési eset: a memória megváltozott, a C-érték mégsem

Az alábbi szándékosan hibás program kizárólag saját, érvényes C-objektumot módosít. Mentsük lab-clobber.c néven:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

static inline void store7(uint64_t *p)
{
    /* A64 ISA: STR. HIBA: a memóriaírás nincs deklarálva. */
    __asm__ volatile(
        "str %x[value], [%x[p]]"
        :
        : [value] "r" (UINT64_C(7)), [p] "r" (p)
        : /* Hiányzik a "memory". */
    );
}

int main(void)
{
    uint64_t value = 1;
    store7(&value);
    printf("value=%" PRIu64 "\n", value);
    return value == 7 ? 0 : 1;
}
# Target: AArch64 Linux, qemu-user; szándékosan hibás szerződés.
for opt in 0 2; do
    aarch64-linux-gnu-gcc \
        -std=c11 -Wall -Wextra -march=armv8-a -mlittle-endian \
        -O"$opt" -static lab-clobber.c -o "lab-clobber-O${opt}"

    qemu-aarch64 "./lab-clobber-O${opt}"
    printf 'O%s: kilepesi allapot=%s\n' "$opt" "$?"
done

# Target: a qemu-userhez készült hibás A64 bináris vizsgálata.
aarch64-linux-gnu-objdump -d lab-clobber-O2 > lab-clobber-O2.dump

Lehetséges, nem garantált hibajelenség: -O0 mellett value=7, 0 kilépési állapot; -O2 mellett például value=1. A hibás változat kilépési állapota akár 0 is lehet: a kiírás és a visszatérési érték eltérően optimalizálódhat, ezért mindkettőt ellenőrizni kell.

A fordító a hibás szerződés alapján nem látja a value módosítását. Az A64 STR megmaradhat, miközben a későbbi C-kifejezésekben a GCC továbbra is az 1 értékkel számol. A pontos tünet a fordítóverziótól és a környező kódtól függ.

A hibakeresés menete:

  1. Tünet: optimalizálási szinttől függő eredmény.
  2. Hipotézis: az asm valós mellékhatása hiányzik a szerződésből.
  3. Adatgyűjtés: a disassemblyben ellenőrizzük a tárolást, majd a kiíráshoz és visszatéréshez előállított értéket.
  4. Egy változtatás: az üres clobber-listát cseréljük "memory"-ra.
  5. Ellenpróba: ugyanazzal az optimalizálással újrafordítjuk és futtatjuk.

A volatile már a hibás változatban is jelen van. Újbóli hozzáadása tehát nem megoldás.

10.1.6.1 Pontosabb javítás: célzott memóriaoperandus

A "memory" clobber helyes, de durva. Fordítói memóriahatás-deklaráció: azt közli, hogy a blokk az operandusokkal le nem írt memóriát is olvashat és írhat. Ezért a fordító érvényteleníti azokat a feltételezéseit, amelyek a blokk által potenciálisan elérhető memóriára vonatkoznak: a blokk előtt kiírja az ilyen objektumok függő értékét, utána pedig újraolvassa őket. Nem jelenti viszont azt, hogy minden memóriabeli értéket újraolvas. Az alias- és escape-elemzésből származó tudása megmarad: egy olyan helyi változót, amelynek a címe soha nem szökik ki, a blokk nem érhet el, így az továbbra is regiszterben maradhat. A helyettesítő eszközlánccal (Clang, AArch64, -O2) egy int local = x * 3; int a = g; __asm__ volatile("" ::: "memory"); return local + a + g; függvényben a local végig a w9 regiszterben maradt, a g globálist viszont a blokk előtt és után külön ldr olvasta. Az előbbi tehát nem sérti a clobbert, az utóbbi éppen a clobber hatása. A "memory" ettől sem lesz hardveres memória-barrier: a processzor memóriaelérési sorrendjére nem hat. Ha a blokk csak egy ismert objektumot érint, pontosabb, ha ezt az objektumot memóriaoperandusként adjuk meg:

Megkötés Jelentés
"=m" (*p) a blokk a *p objektumot teljesen felülírja, korábbi tartalmát nem olvassa
"+m" (*p) a blokk a *p objektumot olvassa és írja
"m" (*p) a blokk a *p objektumot csak olvassa

A sablonban %[obj] a GCC által választott címzési alakot írja ki (például [x0]), ezért közvetlenül LDR/STR operandusa lehet. Mentsük lab-memop.c néven:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

/* Csak a *p objektumot írja: "=m" a teljes, felülírt objektumot írja le. */
static inline void store7(uint64_t *p)
{
    __asm__(
        "str %x[value], %[obj]"
        : [obj] "=m" (*p)
        : [value] "r" (UINT64_C(7))
    );
}

/* Olvassa és írja *p-t: "+m"; a munkaregiszter "=&r", mert az első
   utasítás már írja, a harmadik pedig még a cím regiszterét használja. */
static inline void inc_in_place(uint64_t *p)
{
    uint64_t tmp;

    __asm__(
        "ldr %x[tmp], %[obj]\n\t"
        "add %x[tmp], %x[tmp], #1\n\t"
        "str %x[tmp], %[obj]"
        : [obj] "+m" (*p), [tmp] "=&r" (tmp)
    );
}

int main(void)
{
    uint64_t value = 1;
    uint64_t other = 100;

    store7(&value);
    inc_in_place(&value);
    printf("value=%" PRIu64 " other=%" PRIu64 "\n", value, other);
    return (value == 8 && other == 100) ? 0 : 1;
}
# Target: AArch64 Linux, qemu-user.
for opt in 0 2; do
    aarch64-linux-gnu-gcc -std=c11 -Wall -Wextra -march=armv8-a \
        -O"$opt" -static lab-memop.c -o "lab-memop-O${opt}"
    qemu-aarch64 "./lab-memop-O${opt}"
    printf 'O%s: kilepesi allapot=%s\n' "$opt" "$?"
done

Várt kimenet mindkét szinten: value=8 other=100, kilépési állapot 0. A fordító tudja, hogy a blokkok csak a value-t érintik, az other-re vonatkozó feltételezéseit megtarthatja. Az inc_in_place két tanulságot köt össze: a "+m" az olvasott és írt objektumot, a tmp "=&r" megkötése pedig azt írja le, hogy a munkaregiszter már az első utasításban íródik, miközben a harmadik utasítás még a %[obj] címregiszterét használja. Ha a tmp megkapná ezt a regisztert, a STR rossz címre írna.

A memóriaoperandus csak a ténylegesen megnevezett objektumot fedi le. Ha a blokk a mutatón túli memóriát is eléri (például egy egész tömböt), vagy a hozzáférés mérete eltér a C-objektum típusától, akkor vagy az egész érintett tartományt kell operandusként leírni, vagy marad a "memory" clobber. Hasonlóan alattomos lehet egy hiányzó munkaregiszter-clobber: a hiba csak akkor látszik, amikor a regiszterkiosztás éppen ütközést okoz.

10.1.7 Feladatok

  1. Magyarázó (alapszint — cél: megértés-ellenőrzés): Miért nem volatile az add64_carry blokk? Miért kell mégis "cc"? Miért nincs szüksége "memory"-ra?

  2. Implementációs (alapszint — cél: rutinépítés): Készíts uint32_t add32(uint32_t a, uint32_t b) függvényt A64 ADD utasítással, "+r" kimenettel és explicit 32 bites operandusnézettel. Egészítsd ki a lab-inline.c tesztjeit a UINT32_MAX + 1 esettel.

  3. Hibakeresési (középszint — cél: rutinépítés): Vizsgáld meg a lab-clobber.c két optimalizálási változatát. Javíts kizárólag a clobber-listán, majd ismételd meg a tesztet. Ha a hibás program sem mutat eltérést, magyarázd meg, miért nem igazolja ez a szerződés helyességét.

10.1.8 Megoldások

1. megoldás — Segítség. Vizsgáld meg külön-külön, mi számít „látható mellékhatásnak” a volatile szempontjából, és melyik A64 utasítás módosítja az NZCV jelzőket a blokkban.

1. megoldás — Teljes megoldás. Az add64_carry a bemenetekből eredményeket számol, amelyeket a C-kód felhasznál. Nem igényel külön, az eredményeitől független végrehajtási mellékhatást, ezért nem kell volatile. Az A64 ADDS módosítja az NZCV-t, ezért kell "cc". Az asm nem ér el memóriát; a pointeren keresztüli írás látható C-művelet.

2. megoldás — Segítség. A %w-módosító a Wn nézetet választja; a "+r" pedig jelzi, hogy a bemenet eredeti értékére is szükség van a kimenet felülírásakor.

2. megoldás — Teljes megoldás. A lab-inline.c állományba illeszthető függvény:

static inline uint32_t add32(uint32_t a, uint32_t b)
{
    /* A64 ISA: 32 bites ADD, állapotjelző-módosítás nélkül. */
    __asm__(
        "add %w[a], %w[a], %w[b]"
        : [a] "+r" (a)
        : [b] "r" (b)
    );
    return a;
}

A "+r" közli, hogy a eredeti értéke is szükséges. A %w az A64 Wn nézetet választja. A várt eredmény 0: az előjel nélküli 32 bites összeg modulo 2322^{32} értendő. A korábbi build- és futtatási ciklussal ellenőrizhető.

3. megoldás — Segítség. A clobber-lista hiányzó eleme pontosan azt a fajta állapotváltozást takarja el a fordító elől, amit az asm blokk a pointeren keresztül, nem regiszterben végez.

3. megoldás — Teljes megoldás. A javított utolsó mező : "memory". Ezután:

# Target: AArch64 Linux, qemu-user; javított lab-clobber.c.
aarch64-linux-gnu-gcc \
    -std=c11 -Wall -Wextra -march=armv8-a -mlittle-endian \
    -O2 -static lab-clobber.c -o lab-clobber-fixed

qemu-aarch64 ./lab-clobber-fixed
printf 'kilepesi allapot=%s\n' "$?"

Várt eredmény: value=7, majd 0 kilépési állapot. A hibás változat sikeres futása csak azt mutatná, hogy az adott fordítás nem használta ki a tévesen engedélyezett optimalizálást. Tesztelés mellett ezért a szerződés kézi ellenőrzése is szükséges.

10.1.9 Rövid összefoglaló

A GCC inline assembly két egymást kiegészítő leírásból áll: az A64 utasításokból és a fordítónak megadott szerződésből. Az operandusok írják le az értékek áramlását, a clobber-lista pedig a további állapotmódosításokat. A fordító maga osztja ki a regisztereket, ezért a sablonban következetesen kell használni a 32 vagy 64 bites nézetet. A volatile nem javítja ki a hiányzó memória- vagy regiszterdeklarációt. Optimalizálási szintek összehasonlítása és disassembly segíthet a hibakeresésben, de a szerződés helyességét nem helyettesíti.

10.1.10 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak: a programokat a könyv GNU GCC/qemu-aarch64 referenciakörnyezetében itt nem fordítottuk és nem futtattuk. A lab-early.c és a lab-memop.c eredményeit helyettesítő eszközlánccal mértük — zig cc (Clang, musl, aarch64-linux-musl), Unicorn-alapú AArch64 user-mode emulátorban: lab-early -O0 mellett bad=40 good=21, -O2 mellett bad=21 good=21; lab-memop mindkét szinten value=8 other=100. A GCC regiszterkiosztása eltérhet, ezért a hibás változat tünete a laborban külön ellenőrizendő; a javított változatok eredménye fordítófüggetlen. Az Ubuntu 24.04 telepített GCC/binutils/QEMU csomagverzióját a laborban kell rögzíteni. A hibás memóriaszerződés konkrét tünete fordítófüggő; a megadott kimenetek vártak vagy kifejezetten lehetségesként jelöltek, nem tényleges mérési eredmények.