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.
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:
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.
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-O2A 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é.
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.
Á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.
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/a036A 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
értéket, az eltárolt összeg
-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".
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.dumpA 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.
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.dumpLehetsé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:
"memory"-ra.A volatile már a hibás változatban is jelen van. Újbóli
hozzáadása tehát nem megoldás.
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" "$?"
doneVá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.
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?
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.
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.
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
é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.
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.
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.
volatile, clobberek és
AArch64 operandusmódosítók. Ellenőrizve: legújabb
listázott kiadás GCC 16.2.& korai felülírás,
valamint az m, =m, +m
memóriaoperandusok jelentése. Ebben az átdolgozásban nem nyitottuk meg
újra.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.