Egy protokollfeldolgozóban más művelet tartozhat az egyes üzenettípusokhoz; egy ellenőrző rutinban pedig több, egymást kizáró feltétel alapján választunk eredményt. C-ben mindkettő néhány sor. Assemblyben nekünk kell biztosítanunk, hogy pontosan a megfelelő ág fusson le, majd a végrehajtás a megfelelő helyen folytatódjon.
A vezérlési gráf (control-flow graph) a
program végrehajtási útvonalainak modellje: csomópontjai elágazás
nélküli utasítássorozatok, élei a lehetséges továbbhaladások. Az
if és a switch fordításakor ezt a gráfot
építjük fel összehasonlításokból, elágazásokból és közös folytatási
pontokból.
Az előfeltétel a Feltétel nélküli és feltételes elágazás ismerete. Az alfejezet minden assembly példája A64 ISA, AArch64, ARMv8-A, little-endian környezetre vonatkozik; a direktívák GNU assembler-direktívák. Cortex-M-kód itt nem szerepel. Az ugrótáblához szükséges egyetlen memóriaolvasást helyben értelmezzük; a címzési módok rendszeres tárgyalása a Load/store címzési módok témája.
if:
sorrend és közös folytatásTekintsük ezt a C-referenciafüggvényt:
#include <stdint.h>
uint32_t classify(int32_t x)
{
if (x < 0)
return 11;
else if (x >= 10 && x <= 19)
return 22;
else if (x == 30 || x == 50)
return 33;
else
return 44;
}Az x előjeles, 32 bites érték. A feltételek sorrendje a
program jelentésének része: az első igaz ág kiválasztása után a további
feltételeket már nem értékeljük ki.
A kézi leképezés lépései:
else ághoz
vezet.A rövidzáras kiértékelés (short-circuit evaluation) további vezérlési éleket jelent:
| C-kifejezés | Első rész eredménye | Következmény |
|---|---|---|
A && B |
hamis | Az egész hamis; B kimarad |
A && B |
igaz | B kiértékelése szükséges |
A || B |
igaz | Az egész igaz; B kimarad |
A || B |
hamis | B kiértékelése szükséges |
Ez nem egyszerűen optimalizálási lehetőség, hanem C-szemantika. Ha a második operandus mellékhatást tartalmaz, vagy csak az első feltétel teljesülésekor biztonságos, a feltétel nélküli kiértékelés megváltoztathatja a program működését. A C logikai operátorait ezért általában nem helyettesíthetjük a két operandus feltétel nélküli kiszámításával és bitenkénti összekapcsolásával.
if, && és ||Munkakönyvtár: ~/arm-labor/a64/a019/ —
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/a019
cd ~/arm-labor/a64/a019A következő teljes program a referenciafüggvény döntését valósítja
meg, de nem függvényhívással: a kiválasztott eredmény Linux-kilépési
állapot lesz. Mentsük lab-if.s néven.
// A64 ISA; GNU as; little-endian Linux.
// LAB_X: a laborban -1 vagy 0..65535.
.ifndef LAB_X
.equ LAB_X, 15
.endif
.section .text
.global _start
_start:
mov W0, #LAB_X
cmp W0, #0
b.lt .Lnegative
// x >= 10 && x <= 19
cmp W0, #10
b.lt .Lspecial
cmp W0, #19
b.le .Linterval
.Lspecial:
// x == 30 || x == 50
cmp W0, #30
b.eq .Lselected
cmp W0, #50
b.eq .Lselected
mov W0, #44
b .Lexit
.Lnegative:
mov W0, #11
b .Lexit
.Linterval:
mov W0, #22
b .Lexit
.Lselected:
mov W0, #33
.Lexit:
mov W8, #93
svc #0
.section .note.GNU-stack,"",@progbitsAz A64 CMP W0, #10 után az A64 B.LT
előjeles összehasonlítás alapján választ: ha x < 10, az
&& első operandusa hamis, ezért a
19-cel való összehasonlítás kimarad. Az ||
esetében a 30-cal való egyezés rögtön az eredményblokkra
vezet.
A közvetlen operandusok itt mind kódolhatók (lásd az A64 immediate-korlátok összefoglalóját). Egy
példára jellemző részlet: a -1 értéket az assembler
MOVN kódolással állítja elő. A Linux-rendszerhívás számát
az A64 X8 tartalmazza; a W8 írása annak felső
32 bitjét nullázza.
A konkrét összeállítás–linkelés–futtatás:
# Target: qemu-user, AArch64 Linux, little-endian.
aarch64-linux-gnu-as -EL -march=armv8-a --defsym LAB_X=15 \
-o lab-if.o lab-if.s
aarch64-linux-gnu-ld -EL -e _start -o lab-if lab-if.o
qemu-aarch64 ./lab-if
lab_status=$?
printf 'status=%d\n' "$lab_status"Levezetett, várt kimenet: status=22. A
példákat itt nem futtatjuk; a tényleges ellenőrzés a labor feladata. A
nem nulla kilépési állapot ebben a gyakorlatban szándékos eredmény, nem
automatikusan build- vagy futtatási hiba.
A --defsym LAB_X=... értékének módosítása és az
újraépítés után:
LAB_X, előjeles |
Várt állapot | Kiválasztott ág |
|---|---|---|
| −1 | 11 | Negatív |
| 9 | 44 | Végső else |
| 10 | 22 | Intervallum alsó határa |
| 19 | 22 | Intervallum felső határa |
| 20 | 44 | Végső else |
| 30 | 33 | Az || első operandusa igaz |
| 50 | 33 | Az || második operandusa igaz |
p != NULL && *p != 0A fenti feltételek tiszta összehasonlítások: ha a gép mindkét
operandust kiértékelné, csak lassabb lenne, az eredmény ugyanaz maradna.
A C && rövidzára azonban nem csak optimalizálás,
hanem szemantikai garancia: a jobb oldali operandus
csak akkor értékelődik ki, ha a bal oldali igaz. Ez akkor válik
láthatóvá, amikor a jobb oldal érvényessége a bal oldaltól függ:
/* C-referencia: igaz, ha p érvényes mutató, és nem üres karakterláncra mutat. */
int lab_nonempty(const char *p)
{
if (p != NULL && *p != 0)
return 1;
return 0;
}Itt a *p kiértékelése NULL mutatón
definiálatlan viselkedés, a gépen jellemzően szegmentálási hiba. Az
assemblyben ezért az ágak sorrendje maga a védelem: a betöltés csak a
mutatóteszt után, annak igaz ágán következhet. Mentsük
lab-guard.s néven:
// A64 ISA; GNU as; little-endian Linux.
// LAB_CASE: 0 = NULL mutató, 1 = üres karakterlánc, 2 = "A".
.ifndef LAB_CASE
.equ LAB_CASE, 2
.endif
.section .rodata
lab_empty: .byte 0
lab_text: .asciz "A"
.section .text
.global _start
_start:
.if LAB_CASE == 0
mov X0, #0 // p = NULL
.elseif LAB_CASE == 1
adrp X0, lab_empty // p -> ""
add X0, X0, :lo12:lab_empty
.else
adrp X0, lab_text // p -> "A"
add X0, X0, :lo12:lab_text
.endif
// p != NULL && *p != 0
cbz X0, .Lfalse // bal oldal hamis: *p NEM értékelődik ki
ldrb W1, [X0] // csak érvényes mutatón olvasunk
cbz W1, .Lfalse // jobb oldal hamis
mov W0, #1
b .Lexit
.Lfalse:
mov W0, #0
.Lexit:
mov W8, #93
svc #0
.section .note.GNU-stack,"",@progbitsAz && bal operandusát itt nem CMP,
hanem CBZ vizsgálja: a nullateszthez nem kell az NZCV. A
lényeg a sorrend: az LDRB csak azon az úton érhető el,
amelyen az X0 már bizonyítottan nem nulla.
# Target: qemu-user, AArch64 Linux, little-endian.
for lab_case in 0 1 2; do
aarch64-linux-gnu-as -EL -march=armv8-a --defsym LAB_CASE=$lab_case \
-o lab-guard.o lab-guard.s &&
aarch64-linux-gnu-ld -EL -e _start -o lab-guard lab-guard.o &&
{ qemu-aarch64 ./lab-guard; printf 'LAB_CASE=%d status=%d\n' "$lab_case" "$?"; }
doneLevezetett, várt kimenet:
LAB_CASE |
Mutató | Várt állapot | Indoklás |
|---|---|---|---|
| 0 | NULL |
0 | a bal oldal hamis, a betöltés kimarad |
| 1 | "" |
0 | a bal oldal igaz, a betöltött bájt 0 |
| 2 | "A" |
1 | mindkét oldal igaz |
Ellenpróba: felcserélt kiértékelési sorrend. Cseréljük fel a két első utasítást, mintha a gép „előbb betöltené az adatot, aztán döntene”:
ldrb W1, [X0] // HIBÁS: a dereferálás megelőzi a NULL-tesztet
cbz X0, .Lfalse
cbz W1, .LfalseA LAB_CASE=1 és LAB_CASE=2 eset ugyanazt az
eredményt adja, mint a helyes változat — a tiszta, érvényes bemenetekre
írt tesztek tehát nem veszik észre a hibát. A LAB_CASE=0
esetben viszont az LDRB a 0-s címet olvassa: a QEMU
szegmentálási hibával áll le, és a shell jellemzően 139
(128 + SIGSEGV) állapotot jelez. Ezért kell a C rövidzáras
sorrendjét az assemblyben is megőrizni, és ezért kell a tesztesetek közé
érvénytelen bemenetet is felvenni.
switch két
tipikus leképezéseA C switch nem ír elő konkrét gépi megvalósítást. Két
gyakori lehetőség:
Kevés vagy erősen szórt esethez az elágazáslánc egyszerű és helytakarékos lehet. Sűrű esetekhez az ugrótábla elkerüli a hosszú összehasonlítássorozatot.
Ez azonban nem merev szabály. A fordító figyelembe veheti az optimalizálási szintet, a kódméretet és az ágak gyakoriságát; használhat döntési fát vagy vegyes megoldást is. Ha az ágak csupán konstansokat adnak vissza, akár adatértékeket tartalmazó táblát is készíthet kódcímeket tartalmazó ugrótábla helyett.
A következő referenciafolyamot most szándékosan ugrótáblával valósítjuk meg:
#include <stdint.h>
uint32_t dispatch(int32_t key)
{
switch (key) {
case 10: return 60;
case 11: return 61;
case 12: return 62;
case 13: return 63;
default: return 99;
}
}Az esetértékek a [10, 13] zárt intervallumot töltik ki.
A táblához 0..3 index kell, ezért először levonjuk a
10 alsó határt.
Mentsük a teljes A64-programot lab-switch.s néven:
// A64 ISA; GNU as; little-endian Linux.
// LAB_KEY: a laborban -1 vagy 0..65535.
.ifndef LAB_KEY
.equ LAB_KEY, 12
.endif
.section .text
.global _start
_start:
mov W0, #LAB_KEY
sub W1, W0, #10
cmp W1, #3
b.hi .Ldefault
adr X2, .Ltable
ldr X3, [X2, W1, uxtw #3]
br X3
.Lr60:
mov W0, #60
b .Lexit
.Lr61:
mov W0, #61
b .Lexit
.Lr62:
mov W0, #62
b .Lexit
.Lr63:
mov W0, #63
b .Lexit
.Ldefault:
mov W0, #99
.Lexit:
mov W8, #93
svc #0
.section .rodata
.balign 8
.Ltable:
.xword .Lr60, .Lr61, .Lr62, .Lr63
.section .note.GNU-stack,"",@progbits# Target: qemu-user, AArch64 Linux, little-endian.
aarch64-linux-gnu-as -EL -march=armv8-a --defsym LAB_KEY=12 \
-o lab-switch.o lab-switch.s
aarch64-linux-gnu-ld -EL -e _start -o lab-switch lab-switch.o
qemu-aarch64 ./lab-switch
lab_status=$?
printf 'status=%d\n' "$lab_status"Levezetett, várt kimenet:
status=62.
Az A64 SUB W1, W0, #10 32 bites, modulo
eredményt képez. Az eredményt ezután előjel nélküli
indexként vizsgáljuk.
key, előjeles |
A64 W1, hexadecimális |
W1 > 3, előjel nélkül |
Útvonal |
|---|---|---|---|
| −1 | 0xfffffff5 |
Igaz | default |
| 9 | 0xffffffff |
Igaz | default |
| 10 | 0x00000000 |
Hamis | Első bejegyzés |
| 12 | 0x00000002 |
Hamis | Harmadik bejegyzés |
| 13 | 0x00000003 |
Hamis | Negyedik bejegyzés |
| 14 | 0x00000004 |
Igaz | default |
A kivonás miatt az alsó határ alatti értékek nagy előjel nélküli
indexszé válnak. Így az A64 B.HI egyetlen vizsgálattal
kizárja mindkét oldali tartományhibát.
A megfelelő C-indexképzés:
uint32_t index = (uint32_t)key - 10u;Ez nem azonos az általános, előjeles key - 10 kifejezés
kiértékelésével: utóbbi bizonyos bemeneteknél előjeles túlcsordulást
okozhatna.
Az A64-példában:
ADR X2, .Ltable kiszámítja a tábla címét.LDR X3, [X2, W1, UXTW #3] az indexet
nullakiterjeszti 64 bitre, nyolccal szorozza, hozzáadja a táblacímhez,
majd onnan 8 bájtot olvas.BR X3 a beolvasott címre ugrik, visszatérési cím
mentése nélkül.Az effektív cím:
ahol és bájtcím, dimenzió nélküli index, és az ellenőrzés után .
A GNU .xword direktíva bejegyzésenként 8 bájtot hoz
létre; a .balign 8 8 bájtos igazítást kér. A tábla tehát 32
bájtos. A linker helyettesíti a szimbolikus célokat tényleges
kódcímekkel: nincs kézzel beírt memóriacím.
Kódolási korlátok:
SUB és az ADR korlátait az A64 immediate-korlátok összefoglalója adja; az
ugrótábla az ADR ±1 MiB-os hatótávolságán belül van.LDR regisztereltolásos alaknál a
skálázás #0 vagy #3 lehet; itt az utóbbi
illeszkedik a bejegyzésmérethez.Ez kisméretű, nem PIE laborprogram, abszolút kódcímeket tartalmazó táblával. Pozíciófüggetlen fordítói kódban gyakoriak a relatív eltolásokat tároló táblák is.
Legyen a három esetérték 10, 1000 és
60000, eredményük rendre 60, 61,
62. A teljes intervallumot lefedő, 8 bájtos bejegyzésű
tábla 59 991 bejegyzést, azaz 479 928 bájtot igényelne.
Ehhez képest három összehasonlítás gazdaságos.
Másoljuk a lab-switch.s fájlt lab-sparse.s
néven, majd az A64 SUB utasítástól az A64 BR
utasításig terjedő döntési részt cseréljük erre:
// A64 ISA; a lab-sparse.s döntési részének helyettesítése.
cmp W0, #10
b.eq .Lr60
cmp W0, #1000
b.eq .Lr61
mov W1, #60000
cmp W0, W1
b.eq .Lr62
b .LdefaultA megmaradó táblát és a 63-as eredményblokkot ez a
változat nem használja; eltávolíthatók.
A 60000 nem kódolható az A64 CMP
immediate-alakjával: nem fér 12 bitbe, és nem ilyen érték 12 bites balra
tolása. Egy 16 bites A64 MOV konstansmezőbe viszont
belefér, ezért regiszteres összehasonlítást használunk.
# Target: qemu-user, AArch64 Linux, little-endian.
aarch64-linux-gnu-as -EL -march=armv8-a --defsym LAB_KEY=60000 \
-o lab-sparse.o lab-sparse.s
aarch64-linux-gnu-ld -EL -e _start -o lab-sparse lab-sparse.o
qemu-aarch64 ./lab-sparse
lab_status=$?
printf 'status=%d\n' "$lab_status"Levezetett, várt kimenet: status=62;
LAB_KEY=11 esetén viszont status=99.
default,
lefedettség és tipikus hibákAz ugrótábla tartományellenőrzése csak az indexhatárokat ellenőrzi.
Ha például a 12 eset hiányzik, a hozzá tartozó
táblabejegyzésnek a default címére kell mutatnia. A
tartományon belüli lyuk is lefedendő bemenet.
C-ben default nélkül egyetlen egyező eset hiányában a
végrehajtás a switch után folytatódik. Ilyenkor az assembly
megfelelő célja a közös folytatás, nem egy önkényesen kiválasztott eset.
Szándékos C-átfolyásnál (fall-through) pedig nem szabad
automatikusan minden eset végére kilépő elágazást tenni.
Hibakereséshez vizsgáljuk meg a tényleges gépi kódot:
# Target-objektumok: A64, qemu-user labor; hostoldali elemzés.
aarch64-linux-gnu-objdump -dr lab-if.o
aarch64-linux-gnu-objdump -dr lab-switch.o| Tünet | Hipotézis | Adatgyűjtés és egyetlen javítás |
|---|---|---|
| Mindig egy későbbi ág eredménye jelenik meg | Hiányzó ugrás miatt átfolyás történik | Disassemblyben követjük az eredményblokk végét; pótoljuk a közös kilépésre ugrást |
| Alsó határ alatti index átjut az ellenőrzésen | A64 B.GT került A64 B.HI helyére |
Statikusan ellenőrizzük a feltételkódot; előjel nélküli feltételre javítjuk |
| A második táblabejegyzéstől hibás cél adódik | Hibás indexskálázás | Ellenőrizzük a 8 bájtos bejegyzést és az A64 #3
skálázást; csak a skálát javítjuk |
| A hiányzó eset mégis eredményt kap | Rossz bejegyzés maradt a táblában | A megfelelő bejegyzést a default címére cseréljük |
Ismerten hibás tartományellenőrzéssel ne futtassuk végig a programot: először statikus elemzéssel javítsuk, és csak utána végezzünk határértékteszteket.
if programot x=9, illetve
x=30 esetén! Mely összehasonlítások maradnak ki a rövidzár
miatt?switch változatból töröld szemantikailag a
12 esetet, de tartsd meg az ugrótáblát! Ellenőrizd a
11, 12, 13 és 14
bemenetet.B.GT utasítást használ. Mi történne
key=9 esetén? A hibás változat futtatása nélkül indokold
meg a hibát, majd javítás után teszteld a 9,
10, 13, 14 határértékeket!1. megoldás — Segítség. Rajzold fel az
if-láncot blokkonként, és minden x-re csak
addig haladj, amíg az adott feltétel igaz vagy hamis volta ténylegesen
eldönti a következő lépést.
1. megoldás — Teljes megoldás. x=9 nem
negatív, de kisebb 10-nél. Ezért a 19-cel való
összehasonlítás kimarad. A 30 és 50 vizsgálata
hamis, az eredmény 44. x=30 esetén az
intervallum mindkét összehasonlítása megtörténik, de a második hamis.
Ezután a 30-cal való egyezés miatt az 50
vizsgálata marad ki; az eredmény 33.
2. megoldás — Segítség. Az ugrótábla mérete és indexelése nem változik, csak az egyik bejegyzés célcíméje — gondold át, hova kell mutatnia egy “törölt” esetnek.
2. megoldás — Teljes megoldás. A GNU
.xword sor harmadik bejegyzése legyen
.Ldefault. A tartományellenőrzés továbbra is
0..3 indexet enged. A várt állapotok sorrendben
61, 99, 63, 99: a
12 tartományon belüli lyuk, a 14 tartományon
kívüli érték.
3. megoldás — Segítség. Számítsd ki, milyen
bitmintát hagy maga után az indexszámítás key=9-re, és nézd
meg, mit jelent ugyanaz a bitminta előjelesen, illetve előjel nélkül
értelmezve.
3. megoldás — Teljes megoldás. key=9
után az A64 W1 értéke 0xffffffff. Ez előjel
nélkül 4 294 967 295, előjelesen −1. Az A64 B.GT ezért nem
ugrana a default ágra, és érvénytelen táblán kívüli olvasás
következhetne. A megfelelő utasítás az A64 B.HI. Javítás
után a várt állapotok: 99, 60,
63, 99.
A C többágú döntéseit összehasonlításokkal, feltételes elágazásokkal
és közös folytatási pontokkal képezhetjük le A64-re. Az ágak sorrendje
és a rövidzár megőrzése a helyesség része: ha a jobb oldali operandus a
bal oldal igaz voltától függ (például
p != NULL && *p != 0), a sorrend felcserélése
érvénytelen bemeneten hibás memóriaelérést okoz, amelyet a csak érvényes
bemenetű tesztek nem mutatnak ki. A sűrű switch esetekhez
ugrótábla, a szórtakhoz összehasonlításlánc lehet célszerű, de a fordító
más szerkezetet is választhat. A normalizált index előjel nélküli
ellenőrzése egyszerre kezelheti az alsó és felső határt. A táblán belüli
lyukakat külön le kell fedni. Határértékekkel, kilépési állapottal és
disassemblyvel ellenőrizzük a megvalósítást; utasításszámból önmagában
nem állapítunk meg gyorsulást.
Az alábbiak dokumentációs kiindulópontok, nem ebben a munkafolyamatban megnyitott, olvasott források:
ADR, LDR és BR.Ellenőrzési hiányok: a példák összeállítása és
QEMU-futtatása itt nem történt meg. A várt állapotok levezetettek. Ez a
lab-guard.s példára is vonatkozik, a felcserélt sorrendű
ellenpróba 139-es állapotát is beleértve. Az Ubuntu
24.04-ben telepített eszközverziókat és a tényleges disassemblyt a
laborban kell rögzíteni. Teljesítménymérést nem közöltünk; a QEMU
user-mode futás nem szolgáltat garantáltan hardverhű ciklusszámot.