Egy C-ből hívott assembly függvény tömbelemet olvas. Az első elemmel működik, a másodikkal szegmentálási hibát kapunk. A hibakereső egy betöltőutasításnál áll meg, de ebből még nem következik, hogy maga a betöltés kódolása rossz: hibás lehet a cím előállítása, egy korábban felülírt regiszter vagy a hívási szerződés megsértése is.
A szegmentálási hiba (segmentation fault,
Linux alatt jellemzően SIGSEGV) azt jelzi, hogy a folyamat
olyan memória-hozzáférést kísérelt meg, amelyet a
virtuálismemória-leképezés vagy annak jogosultságai nem engednek meg. Ez
operációsrendszer-szintű esemény, nem egyetlen assembly programozási
hibafajta neve.
Két címet különböztessünk meg:
A labor a Disassembly objdump/readelf-fel
munkafolyamatára épít. Minden assembly utasítás és regiszternév
A64 ISA, AArch64, ARMv8-A környezetre vonatkozik. A
direktívák GNU as direktívák ehhez a célhoz. A célprofil
little-endian Linux, az elsődleges futtató
qemu-aarch64.
Az alábbi eredmények levezetett, várt eredmények; a kódot itt nem futtattuk.
Egy hibás indexelés nem szükségképpen okoz azonnali összeomlást. Ha a rossz cím továbbra is olvasható lapon van, a program egyszerűen más adatot olvas. Ezért a laborban egy védőlapot (guard page) hozunk létre: egy leképezett, de a program számára nem olvasható és nem írható memórialapot.
A címeket az mmap szolgáltatja. Nem használunk rögzített
vagy találomra választott címet. A lapméretet futás közben kérdezzük
le.
Legyen:
Az első lap olvasható és írható; a következő lap hozzáférhetetlen. A
lap végére helyezett uint32_t tömb kezdete:
A helyes elemcím . A hibás függvény ehelyett címet képez: összekeveri a 64 bites mutató méretét a tömbelem 4 bájtos méretével.
Levezetés az alapteszthez: , esetén:
| Mennyiség | Levezetett érték |
|---|---|
| Tömb kezdete | |
| Helyes második elem címe | |
| Hibásan képzett cím | |
| Hibás olvasás célterülete | A védőlap első 4 bájtja |
Így a hiba nem a környező memória véletlen tartalmától függ.
Munkakönyvtár: ~/arm-labor/a64/a042/ —
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/a042
cd ~/arm-labor/a64/a042A függvény C-interfésze:
uint32_t lab_get(const uint32_t *p, size_t i);
Előfeltétele, hogy p megfelelően igazított, legalább
i+1 olvasható elemet tartalmazó tömbre mutasson. A függvény
nem végez határellenőrzést.
Az AAPCS64 szerint a mutató az A64 X0, az index az A64
X1 regiszterben érkezik; a 32 bites eredmény az A64
W0 regiszterben tér vissza.
Mentsük a hibás függvényt lab-offset.s néven:
// A64 ISA, ARMv8-A; GNU as; little-endian laborprofil.
.arch armv8-a
.text
.p2align 2
.global lab_get
.type lab_get, %function
lab_get:
.cfi_startproc
add x2, x0, x1, lsl #3 // HIBA: az indexet 8-cal szorozza.
ldr w0, [x2] // 4 bájtos elem betöltése.
ret
.cfi_endproc
.size lab_get, .-lab_get
.section .note.GNU-stack,"",%progbitsAz A64 ADD itt eltolt regiszteres operandust használ. A
64 bites változat LSL eltolási értéke 0…63
lehet; a #3 tehát szabályosan kódolható. Ez
szemantikai hiba, nem assemblerhiba. A helyes eltolás
#2, mert
.
Az A64 X2 hívó által mentendő ideiglenes regiszter,
ezért használata nem sérti a hívási szerződést. A függvény nem hív másik
függvényt, és nem módosítja a vermet. A .cfi_startproc és
.cfi_endproc a veremvisszafejtéshez használható metaadatot
jelöli; nem hajt végre regisztermentést.
A teljes tesztprogram legyen lab-offset.c:
#define _GNU_SOURCE
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
extern uint32_t lab_get(const uint32_t *p, size_t i);
static void fail(const char *what)
{
perror(what);
exit(2);
}
__attribute__((noinline))
static void run_case(size_t n, size_t i, int tail)
{
long raw_page = sysconf(_SC_PAGESIZE);
if (raw_page <= 0) {
fputs("Nem kerdezheto le a lapmeret.\n", stderr);
exit(2);
}
size_t page = (size_t)raw_page;
if (page > SIZE_MAX / 2 || n == 0 ||
n > page / sizeof(uint32_t) || i >= n) {
fputs("Ervenytelen teszteset.\n", stderr);
exit(2);
}
unsigned char *map = mmap(NULL, 2 * page, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (map == MAP_FAILED)
fail("mmap");
if (mprotect(map, page, PROT_READ | PROT_WRITE) != 0)
fail("mprotect");
size_t offset = tail ? page - n * sizeof(uint32_t) : 0;
uint32_t *p = (uint32_t *)(map + offset);
for (size_t j = 0; j < n; ++j)
p[j] = 1000u + 17u * (uint32_t)j;
uint32_t expected = p[i];
uint32_t got = lab_get(p, i);
if (got != expected) {
fprintf(stderr,
"FAIL: n=%zu i=%zu tail=%d got=%u expected=%u\n",
n, i, tail, (unsigned)got, (unsigned)expected);
exit(1);
}
if (munmap(map, 2 * page) != 0)
fail("munmap");
}
int main(int argc, char **argv)
{
unsigned cases = 0;
if (argc == 1) {
run_case(2, 1, 1);
cases = 1;
} else if (argc == 2 && strcmp(argv[1], "--all") == 0) {
const size_t lengths[] = {1, 2, 3, 16};
for (size_t k = 0; k < sizeof lengths / sizeof lengths[0]; ++k) {
for (int tail = 0; tail <= 1; ++tail) {
for (size_t i = 0; i < lengths[k]; ++i) {
run_case(lengths[k], i, tail);
++cases;
}
}
}
} else {
fputs("Hasznalat: lab-offset [--all]\n", stderr);
return 2;
}
printf("OK: %u eset\n", cases);
return 0;
}A tail=0 esetek a lap elejére, a tail=1
esetek közvetlenül a védőlap elé helyezik a tömböt. Így egyszerre
teszteljük a helyes értéket és a határközeli címzést.
Ubuntu 24.04 cross-toolchainnel, qemu-user célra:
# Target: qemu-user / AArch64 Linux, ARMv8-A, little-endian.
aarch64-linux-gnu-as -g -EL -march=armv8-a \
-o lab-offset.o lab-offset.s
aarch64-linux-gnu-gcc -std=c11 -g3 -O0 -Wall -Wextra \
-march=armv8-a -mlittle-endian \
-fno-omit-frame-pointer -fno-optimize-sibling-calls \
-c lab-offset.c -o lab-driver.o
aarch64-linux-gnu-gcc -static -no-pie \
-o lab-offset lab-driver.o lab-offset.o
aarch64-linux-gnu-objdump -d --disassemble=lab_get lab-offset
# Szándékos összeomlás: ne keletkezzen core fájl.
ulimit -c 0
lab_status=0
qemu-aarch64 ./lab-offset || lab_status=$?
printf 'status=%s\n' "$lab_status"A statikus linkeléshez a célarchitektúra statikus C-könyvtára is
szükséges. Ha a linker például a céloldali libc.a hiányát
jelzi, az környezeti probléma, nem a vizsgált címzési hiba.
Várt eredmény: SIGSEGV; Bash alatt
jellemzően status=139, azaz
.
A QEMU és a shell diagnosztikai szövege verziófüggő. Az OK
sor nem jelenhet meg.
Első terminálban indítsuk el a célt a QEMU távoli hibakereső-felületével:
# Target: qemu-user / AArch64; várakozás a GDB kapcsolatára.
ulimit -c 0
qemu-aarch64 -g 12345 ./lab-offsetMásodik terminálban:
# Debugger: x86_64 host; target: qemu-user / AArch64.
gdb-multiarch -q ./lab-offset \
-ex 'set pagination off' \
-ex 'target remote 127.0.0.1:12345' \
-ex 'break lab_get' \
-ex 'continue' \
-ex 'info registers x0 x1 x2 sp x29 x30 pc' \
-ex 'set $base = $x0' \
-ex 'x/2uw $x0' \
-ex 'si' \
-ex 'p/d $x2 - $base' \
-ex 'continue' \
-ex 'x/i $pc' \
-ex 'info registers x0 x1 x2 sp x29 x30 pc' \
-ex 'bt'A lépések jelentése:
x/2uw $x0 két 32 bites, előjel nélküli decimális
értéket olvas: várhatóan 1000 és 1017.si egyetlen gépi utasítást léptet: végrehajtja a
hibás címképzést.8, holott az index
1, az elemméret pedig 4 bájt.SIGSEGV miatt megáll.A hiba pillanatában:
| A64 állapot | Várt értelmezés |
|---|---|
PC |
Az A64 LDR W0, [X2] utasítás címe |
X0 |
A tömb kezdőcíme; a sikertelen betöltés még nem adott eredményt |
X1 |
1, előjel nélküli index |
X2 |
A védőlap kezdőcíme |
SP |
A függvénybelépéshez képest változatlan |
A GDB-ben a p $_siginfo paranccsal megpróbálhatjuk
lekérni a jel részletes adatait. Ha a QEMU–GDB kapcsolat támogatja ezt,
a si_addr mező a hozzáférési hibacímet adja. A
távoli cél nem minden változata szolgáltat ilyen adatot. Hiánya
esetén az utasításból és regiszterekből vezetjük le a címet: ennél az
A64 betöltésnél az effektív cím pontosan az X2
tartalma.
A bt által megjelenített hívási lánc
(backtrace) várt szerkezete:
| Keret | Függvény |
|---|---|
#0 |
lab_get |
#1 |
run_case, n=2, i=1,
tail=1 |
#2 |
main |
A frame 1, majd az info args és
info locals parancsokkal ellenőrizhetjük a C-oldali
előfeltételeket és a lapméretet. Ezután a frame 0
visszavisz a hibázó függvényhez.
A hívási lánc nem önmagában bizonyíték: sérült verem vagy hibás visszafejtési metaadat esetén téves vagy hiányos lehet. Ugyanígy a debugger esetleges sikeres memóriaolvasása sem bizonyítja, hogy a program ugyanazt a címet jogosultan olvashatja: a hibakereső memóriaelérése eltérhet a célszálétól.
A hipotézisünk konkrét:
A tömbelem címe azért kerül a védőlapra, mert a függvény az indexet 4 helyett 8 bájttal skálázza.
Ez ellenőrizhető előrejelzést ad: kizárólag az eltolás javítása után
a címkülönbség 4, a visszatérési érték 1017, a
kilépési állapot 0 lesz.
Ne módosítsuk egyszerre a teszttömböt, a lapvédelmet és az assembly kódot. Hozzunk létre külön javított változatot:
# Target: qemu-user / AArch64; egyetlen forrásváltoztatás.
sed 's/lsl #3/lsl #2/' lab-offset.s > lab-offset-fixed.s
aarch64-linux-gnu-as -g -EL -march=armv8-a \
-o lab-offset-fixed.o lab-offset-fixed.s
aarch64-linux-gnu-gcc -static -no-pie \
-o lab-offset-fixed lab-driver.o lab-offset-fixed.o
aarch64-linux-gnu-objdump -d --disassemble=lab_get lab-offset-fixed
qemu-aarch64 ./lab-offset-fixed
printf 'status=%s\n' "$?"
qemu-aarch64 ./lab-offset-fixed --all
printf 'status=%s\n' "$?"Várt kimenet: először OK: 1 eset, majd
OK: 44 eset; mindkét futás kilépési állapota
0. A teljes tesztszám:
ahol a kettes szorzó a két elhelyezést jelenti.
A javított binárist a korábbi GDB-lépéssorral is vizsgáljuk meg: a
címképzés után 4 legyen az eltérés, a betöltés után az A64
W0 értéke 1017. Ez összekapcsolja a
forrásváltozást, a tényleges gépi kódot és a megfigyelt eredményt.
| Tünet | Hipotézis | Célzott adatgyűjtés és ellenpróba |
|---|---|---|
| Már az első betöltés címe rossz | Hibás offset vagy elemskálázás | Bemeneti mutató, index, elemméret és effektív cím összevetése |
| Egy belső hívás után romlik el a mutató | Élő érték maradt egy felülírható regiszterben | Regiszterérték rögzítése a hívás előtt és után; kizárólag a megőrzés javítása |
| A hívó állapota változik meg váratlanul | A függvény nem állít helyre megőrzendő regisztert | Az A64 X19–X29 érintett regisztereinek összevetése
belépéskor és kilépéskor |
| Visszatéréskor vagy később történik a hiba | Aszimmetrikus prológus/epilógus | Veremmutató és mentési helyek követése; a foglalás és felszabadítás párosítása |
Az A64 X30 visszatérési címét egy újabb A64
BL felülírja. Egy további függvényt hívó rutin ezért nem
támaszkodhat feltétel nélkül az eredeti regisztertartalomra. A jelenlegi
levélfüggvény viszont nem végez ilyen hívást.
A veremhiba sem mindig egyszerű igazítási hiba. Ha a prológus 16
bájtot foglal, az epilógus pedig 32 bájtot szabadít fel, az A64
SP továbbra is 16 bájtra igazított lehet, mégsem áll
helyre. Az igazítás ellenőrzése szükséges, de nem helyettesíti a
belépési és kilépési érték összevetését.
A „most lefutott” gyenge bizonyíték. Ebben a laborban négy külön ellenőrzés kell:
Az érvénytelen, például i == n index nem része a
függvény szerződésének. Annak tesztelése nem helyettesíti az érvényes
bemenetek ellenőrzését.
A hibajegyzőkönyvet például lab-report.md néven
rögzíthetjük:
| Mező | Tartalom |
|---|---|
| Tünet | SIGSEGV a második elem olvasásakor |
| Gyökérok (root cause) | Hibás, nyolcbájtos elemskálázás |
| Közreható tényező (contributing factor) | A mutatóméret és az elemméret összekeverése |
| Felismerést késleltető tényező | A nulladik index tesztje mindkét skálázással helyes címet ad |
| Bizonyíték | X1=1, címkülönbség 8, betöltési cím a
védőlapon |
| Javítás | Kizárólag az eltolás módosítása |
| Elfogadási feltétel | Helyes címkülönbség, helyes eredmény, 44 sikeres eset |
A védőlap nem a programhiba gyökéroka: a meglévő hibát tette determinisztikusan láthatóvá.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért nem különbözteti meg a helyes és
hibás függvényt az i=0 teszt? Lapvégi, 16 elemű tömbnél mi
a helyes utolsó elem címe a védőlap kezdetéhez viszonyítva?
(implementációs, alapszint — cél: rutinépítés)
Egészítsd ki a hosszlistát a 8 értékkel. Számítsd ki az új
esetszámot, majd építsd újra a javított programot azonos fordítási
kapcsolókkal.
(hibakeresési, középszint — cél: rutinépítés)
Futtasd az eredeti hibás binárist --all kapcsolóval. Miért
lehetséges, hogy nem SIGSEGV, hanem FAIL az
első eredmény? GDB-ben ellenőrizd az első értékeltérésnél a képzett
címet és az olvasott adatot.
1. megoldás — Segítség. Helyettesítsd be -t mindkét képletbe ( és ) — különböznek-e az eredmények? A lapvégi elhelyezésnél induljon ki abból, hogy a tömb kezdete .
1. megoldás — Teljes megoldás. Nulla index esetén , ezért mindkét függvény a tömb elejét olvassa. A 16 elemű lapvégi tömb kezdete ; az utolsó elem helyes címe . A teljes négysávos bájttartomány még az olvasható lapon van.
2. megoldás — Segítség. Az esetszám-képlet minden hosszra; csak egy taggal kell bővíteni az összeget.
2. megoldás — Teljes megoldás. A hosszlista
{1, 2, 3, 8, 16} lesz. Az esetszám
.
A javított változat várt kimenete OK: 60 eset, kilépési
állapota 0. Az új esetek az indexelési tartományt bővítik,
nem módosítják a függvény szerződését.
3. megoldás — Segítség. Melyik tail
elhelyezésnél (0 vagy 1) esik a hibásan képzett cím MÉG a leképezett,
csak épp rossz tartalmú területre, nem a védőlapra?
3. megoldás — Teljes megoldás. A ciklussorrendben az
első eltérés várhatóan n=2, i=1,
tail=0 esetén jelentkezik. A hibás cím a tömb kezdete után
8 bájttal van: a tömbön kívül, de még az olvasható lapon. Az anonim
leképezés érintetlen része kezdetben nulla, ezért a várt diagnózis
got=0 expected=1017, a kilépési állapot 1. Ez
bizonyítja, hogy a szegmentálási hiba hiánya nem jelent helyes
címzést.
A szegmentálási hiba vizsgálatakor külön kezeljük a hibázó utasítás címét és az elérni próbált memóriacímet. A regiszterekből rekonstruált címképzést a függvény szerződésével és a C-oldali adatelrendezéssel vetjük össze. A védőlap reprodukálhatóvá teszi a határátlépést, az értékellenőrzés pedig az olvasható memórián belüli téves hozzáférést is felismeri. Egyetlen célzott változtatást tesztelünk, majd regressziós és negatív ellenpróbával ellenőrizzük a javítást. A hívási láncot mindig a verem és a visszafejtési információ megbízhatóságával együtt értelmezzük.
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.
master ág dokumentációja (közös verziókorlát).Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
ténylegesen fennmaradnak: a buildet, a futtatást és a konkrét
GDB-kimenetet itt nem validáltuk. A laborban rögzíteni kell a telepített
eszközverziókat, ellenőrizni kell a statikus célkönyvtár elérhetőségét,
a $_siginfo támogatását és a hívási lánc tényleges
megjelenítését. A dokumentációs linkek nem rögzítik automatikusan az
Ubuntu 24.04 csomagverzióit.