Egy adatfeldolgozó programnak szüksége lehet egy kezdetben ismert számlálóra, egy nullaértékekkel induló munkapufferre és egy csak futás közben meghatározható méretű tárolóra. C-ben ezeket néhány deklaráció és egy allokátorhívás elfedi; assemblyben nekünk kell megkülönböztetnünk az objektum helyét, kezdeti tartalmát és élettartamát.
Ebben az alfejezetben Linux alatt futó, little-endian AArch64/A64 programot készítünk GNU assemblerrel. Minden alábbi assemblyutasítás és regiszter ebben a profilban értendő; a direktívák GNU assembler-direktívák, nem processzorutasítások. Cortex-M-en nincs Linux-betöltő vagy Linux-rendszerhívás: az ottani inicializálást a Startup kód: .data másolása, .bss törlése tárgyalja.
A cél nem egy teljes malloc megírása, hanem a statikus
tárolás és a futásidejű memóriaterület-növelés pontos
megkülönböztetése.
.data és .bssA statikus tárolási időtartamú objektum (static
storage duration object) a program teljes futása alatt létezik. Ez
nem azonos a C static kulcsszó valamennyi szerepével:
például a fájlszinten definiált, külső kapcsolású változónak is statikus
a tárolási időtartama.
A GNU assembler két fontos, írható adatszekciója:
.data: rendszerint a nem nulla
kezdőértékkel rendelkező statikus adatok helye. A kezdeti bájtok az
ELF-fájlban is szerepelnek..bss: rendszerint a nulla-inicializált
statikus adatok helye. A fájl leírja a szükséges méretet és igazítást,
de a nullabájtokat nem tárolja adatként.C-kapcsolatként az alábbi objektumok szokásos ELF-elhelyezése rendre
.data, illetve .bss:
/* C-példa; célprofil: AArch64 Linux, little-endian. */
#include <stdint.h>
uint32_t counter = 7;
unsigned char scratch[4096];Ez fordítói és objektumformátum-konvenció, nem a C szabvány
szekciónév-előírása. Az explicit nulla inicializáló, például
uint32_t counter = 0;, szintén lehet
.bss-beli.
A GNU assembler .bss direktívája nem futás
közben törli a memóriát. A Linux ELF-betöltése biztosítja a
megfelelő tartomány kezdeti nullaértékét. A betöltő elsősorban a
programszegmenseket használja, nem a .bss szekció nevét
keresi; a részletes hátteret Az ELF formátum alapjai
és a program életciklusa adja.
Az igazítás itt is szerződés. A későbbi példában a GNU assembler
.balign 4 direktívája négybájtos, .balign 16
direktívája tizenhat bájtos határra igazítja a következő objektumot. Ez
a Struktúrák, igazítás és mezőelérés
szabályainak közvetlen alkalmazása.
.bss használatával?ELF-ben a .data jellemzően PROGBITS, a
.bss pedig NOBITS típusú szekció. Utóbbinak
van memóriabeli mérete, de nincs ugyanekkora fájlbeli adattartalma.
Levezetett példa: egy 4096 bájtos, nullával induló pufferhez:
| Elhelyezés | Kezdeti adatbájtok a fájlban | Objektumméret a memóriában |
|---|---|---|
.data, 4096 tárolt nullabájttal |
4096 bájt | 4096 bájt |
.bss |
0 bájt | 4096 bájt |
A teljes ELF-fájl méretkülönbsége nem szükségképpen pontosan 4096 bájt: szekciófejlécek, igazítási kitöltések és a linker elrendezése is számítanak.
A .bss tehát fájlméretet takarít meg, nem az
objektum virtuális címterületét tünteti el. A fizikai
memórialapok tényleges hozzárendelése már operációsrendszer-kérdés. A
lusta lapkiosztás (demand paging) miatt a lefoglalt virtuális
méretből önmagában nem következik azonnali, ugyanekkora
fizikaimemória-fogyasztás.
| Tulajdonság | Statikus memória | Verem (stack) | Heap |
|---|---|---|---|
| Tipikus feladat | Globális állapot, állandó méretű munkaterület | Hívásokhoz kötött ideiglenes állapot | Futás közben változó méretű objektumok |
| Élettartam | A program teljes futása | Többnyire a hívási keret élettartama | Az allokációs szerződés szerint |
| Elhelyezés kezelője | Fordító, assembler, linker, betöltő | Hívó és hívott kód | Allokátor, alatta rendszerhívások |
| Kezdeti tartalom | .data: előírt bájtok; .bss: nulla |
Nem általánosan nulla | Az API-tól függ; malloc nem nulláz |
| Felszabadítás | Nem objektumonként történik | Keret lebontásával | Allokátorhívással vagy terület-visszaadással |
A verem részletes kezelése A stack frame felépítése és a verem iránya témája. Most elég látni: egy lokális tömb nem azért „verem”, mert tömb, és egy nagy tömb nem automatikusan „heap”. A tárolási módot a létrehozás és az élettartam szerződése határozza meg.
brk és az
mmap szerepeA programtöréspont (program break) a
hagyományos, brk-alapú adattartomány végcíme. A Linux
brk rendszerhívás ezt a végcímet próbálja módosítani.
A Linux AArch64 rendszerhívási ABI szerint a brk száma
214, amelyet az A64 X8 regiszter kap; a kért végcím az
A64 X0 regiszterbe kerül. Ezek Linux ABI-értékek, nem
az A64 ISA részei.
Fontos a nyers rendszerhívás szerződése:
-errno.Ez különbözik a C-könyvtári brk() burkolófüggvény
visszatérési szerződésétől.
Ha a régi végcím , a kért növelés pedig bájt, akkor siker után az újonnan használható tartomány:
Itt folyamatbeli virtuális bájtcím; , és a címösszegnek ábrázolhatónak kell lennie. A lefoglalt rész kezdete a régi töréspont, nem a visszakapott új végcím.
A brk önmagában nem tart nyilván objektumokat, nem kezel
szabadlistát és nem oldja meg az általános igazítást. A tartomány
közepéből nem lehet vele tetszőleges blokkot visszaadni. C-könyvtári
malloc mellett ne módosítsuk önállóan a töréspontot:
megsérthetjük az allokátor belső állapotát.
A Linux AArch64 mmap rendszerhívás száma 222. Ezzel
különálló virtuális leképezés kérhető; anonim, privát, írható leképezést
gyakran nagyobb memóriaterületekhez használnak. Nulla címjavaslat esetén
a rendszer választ kezdőcímet, a leképezés lapigazított, kezdeti
tartalma anonim leképezésnél nulla. A nyers visszatérési értéket itt
negatív hibakódra kell ellenőrizni. A visszaadást a 215 számú
munmap szolgálja.
A lapigazítás nem jelent tetszőleges, lapméretnél nagyobb igazítást; a lapméretet sem szabad általánosan 4096 bájtnak feltételezni.
Munkakönyvtár: ~/arm-labor/a64/a026/ —
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/a026
cd ~/arm-labor/a64/a026A lab-memory.s fájl önálló, C-könyvtár nélküli program.
Ellenőrzi a statikus kezdőértékeket, 4096 bájttal növeli a töréspontot,
használja a kapott területet, majd visszaállítja az eredeti töréspontot.
A 4096 itt kért bájtszám, nem feltételezett
rendszerszintű lapméret.
// AArch64 Linux, A64 ISA, ARMv8-A, little-endian.
// GNU assembler-direktívák; nincs C runtime vagy malloc.
.arch armv8-a
.data
.balign 4
counter:
.word 7
.bss
.balign 16
scratch:
.skip 4096
.text
.global _start
.type _start, %function
_start:
// Inicializált statikus adat: 7 -> 8.
adrp X9, counter
add X9, X9, :lo12:counter
ldr W2, [X9]
cmp W2, #7
b.ne static_error
add W2, W2, #1
str W2, [X9]
// Két mintavételi pont a nulla-inicializált pufferben.
adrp X10, scratch
add X10, X10, :lo12:scratch
ldr W3, [X10]
cbnz W3, static_error
ldr W3, [X10, #4092]
cbnz W3, static_error
str W2, [X10]
// Linux AArch64: brk(0), aktuális töréspont lekérdezése.
mov X0, #0
mov X8, #214
svc #0
mov X19, X0
// Új végcím = régi végcím + 4096 bájt.
adds X20, X19, #1, lsl #12
b.cs heap_error
mov X0, X20
mov X8, #214
svc #0
cmp X0, X20
b.ne heap_error
// Egyetlen bájt használata: nincs külön igazítási igény.
mov W2, #42
strb W2, [X19]
ldrb W3, [X19]
cmp W3, #42
b.ne heap_error
lab_inspect:
// A saját növelés visszavonása.
mov X0, X19
mov X8, #214
svc #0
cmp X0, X19
b.ne heap_error
mov X0, #0
b finish
static_error:
mov X0, #1
b finish
heap_error:
mov X0, #2
finish:
mov X8, #93
svc #0
.size _start, .-_start
.section .note.GNU-stack,"",@progbitsAz A64 X19 a régi, az X20 a kért új végcímet őrzi. Nincs függvényhívás: a program közvetlenül a belépési ponttól a kilépési rendszerhívásig fut.
A korlátokat az A64 immediate-korlátok összefoglalója adja; ebből a példában két dolog számít:
ADD 12 bites mezőjébe csak
eltolva fér bele, ezért #1, lsl #12.LDR skálázott eltolásaként a
#4092 még kódolható (4 × 1023). Az ADRP utáni
:lo12: relokáció a szimbólumcím alsó 12 bitjét adja.A szimbolikus címképzés miatt nincs szükség kézzel választott folyamatcímre.
Az alábbi parancsok célja qemu-user / AArch64, Ubuntu 24.04 hoston:
# Target: qemu-user, AArch64 Linux, little-endian.
aarch64-linux-gnu-as -EL -g -o lab-memory.o lab-memory.s
aarch64-linux-gnu-ld -EL -e _start -o lab-memory lab-memory.o
aarch64-linux-gnu-readelf -SW lab-memory
aarch64-linux-gnu-readelf -lW lab-memory
aarch64-linux-gnu-objdump -d lab-memory
qemu-aarch64 ./lab-memory
printf 'kilepesi_allapot=%d\n' "$?"Várt, nem itt futtatással igazolt eredmény:
kilepesi_allapot=0. A program saját szöveges kimenetet nem
ad. Az 1 statikusadat-ellenőrzési, a 2 heapkezelési vagy
heapadat-ellenőrzési hibát jelez.
A readelf kimenetében a .bss várt típusa
NOBITS, mérete 0x1000; a .data
típusa PROGBITS, mérete 4 bájt. A betölthető szegmenseknél
a MemSiz és FileSiz különbsége mutatja a
fájladattal nem teljesen lefedett memóriarészt.
A lab_inspect címkénél elvárt állapot:
| Objektum vagy A64 regiszter | Várt tartalom |
|---|---|
counter |
Előjel nélküli 32 bites 8 |
scratch első négy bájtja |
Little-endian: 0x08 0x00 0x00 0x00 |
scratch fennmaradó bájtjai |
Nulla |
| X19 | Eredeti töréspont |
| X20 | X19 + 4096 |
| X3 | Előjel nélküli 64 bites 42 |
| X19 címén egy bájt | 0x2A |
A program a .bss kezdeti nullázottságát csak két ponton
mintavételezi; ez nem teljes tartományvizsgálat.
Hibás elképzelés: a .bss inicializálatlan, tehát
szemétértékű. Linux ELF-betöltés után a tartomány nullaértékű.
Ha nem ezt látjuk, először ellenőrizzük, hogy a program nem írta-e már
felül, illetve valóban a megfelelő szimbólum címét vizsgáljuk-e.
Hibás cím: az új töréspontot használjuk pufferkezdetként. Ez a foglalás utáni első cím. Az írás nem válik helyessé attól, hogy egy lapkerekítés miatt esetleg nem okoz azonnali hibát.
Hibás hibakezelés: a brk eredményében csak
negatív értéket keresünk. A nyers Linux brk
növelésénél a kért és a visszakapott végcímet kell összevetni.
Hibás élettartam: visszaállítás után tovább használjuk a
területet. A régi mutató bitmintája megmaradhat, de már nem
jogosít hozzáférésre. A frissen kért brk-tartományt se
kezeljük általános nullázó allokátorként: korábban használt, visszaadott
rész ismételt kiterjesztésekor nem alapozhatunk ilyen szerződésre.
A vizsgálathoz az első terminálban:
# Target: qemu-user; a fenti build, induláskor GDB-re vár.
qemu-aarch64 -g 1234 ./lab-memoryA második terminálban:
# Target: a qemu-user AArch64 folyamata.
gdb-multiarch -q ./lab-memory \
-ex 'target remote :1234' \
-ex 'break lab_inspect' \
-ex continue \
-ex 'info registers x19 x20 x3' \
-ex 'x/wu $x9' \
-ex 'x/wu $x10' \
-ex 'x/ub $x19' \
-ex continue \
-ex quitVárt eredmény a fenti táblázat, majd normál kilépés. Hibánál a munkamenet: tünet → hipotézis → regiszterek és disassembly ellenőrzése → egyetlen módosítás → újrafuttatás. Az emuláció itt funkcionális ellenőrzés, nem hardverhű időmérés.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért nem jelent 4096 bájttal kisebb
futásidejű objektumot, ha a puffert .data helyett
.bss tárolja? Ki biztosítja a kezdeti nullákat?
(implementációs, középszint — cél: rutinépítés) Növeld a dinamikus területet 8192 bájtra. Írd a 42 értéket az első és az utolsó bájtjába, és ellenőrizd mindkettőt. A visszaállítás maradjon meg.
(hibakeresési/mérési, középszint — cél:
rutinépítés) Készíts lab-memory-filezeros.s
változatot, amelyben kizárólag a .bss direktívát cseréled
.data direktívára. Hasonlítsd össze a szekciókat és a
fájlméreteket. Miért nem feltétlenül pontosan 4096 bájt a
különbség?
1. megoldás — Segítség. A .bss és a
.data közül melyik befolyásolja a futásidejű VIRTUÁLIS
CÍMTARTOMÁNY méretét, és melyik csak a FÁJLBAN tárolt bájtokat?
1. megoldás — Teljes megoldás. A puffer mindkét
esetben 4096 bájt címtartományt igényel. A .bss csak a
kezdő nullabájtok fájlbeli tárolását hagyja el; a Linux ELF-betöltése
gondoskodik a nulla kezdeti tartalomról.
2. megoldás — Segítség.
;
az ADD/ADDS immediate 12 bites, opcionális 12
bites balra tolással kódolható — hogyan írható fel ezzel a 8192?
2. megoldás — Teljes megoldás. Az A64 növelés
adds X20, X19, #2, lsl #12 alakú:
bájt. Az utolsó bájt címe X20 − 1; ezt például az A64
sub X4, X20, #1 képezheti. Ezután
strb W2, [X4], majd ldrb W3, [X4] használható.
Mindkét visszaolvasásnál 42 az elvárt előjel nélküli érték. A
X20 címére már tilos írni.
3. megoldás — Segítség. A .data
ugyanazt a méretet jelenti a memóriában, mint a .bss, de a
FÁJLBAN is tárolnia kell a nullabájtokat — mi minden más (fejlécek,
igazítás) változhat még emellett a szekcióelrendezésben?
3. megoldás — Teljes megoldás. A változat létrehozása és vizsgálata:
# Target: qemu-user, AArch64 Linux, little-endian.
sed 's/^[[:space:]]*\.bss[[:space:]]*$/ .data/' \
lab-memory.s > lab-memory-filezeros.s
aarch64-linux-gnu-as -EL -g \
-o lab-memory-filezeros.o lab-memory-filezeros.s
aarch64-linux-gnu-ld -EL -e _start \
-o lab-memory-filezeros lab-memory-filezeros.o
aarch64-linux-gnu-readelf -SW lab-memory-filezeros
wc -c lab-memory lab-memory-filezeros
qemu-aarch64 ./lab-memory-filezeros
printf 'kilepesi_allapot=%d\n' "$?"A GNU assembler .skip 4096 itt a .data
részeként tárolt nullákat hoz létre. Várt eredmény:
továbbra is 0 kilépési állapot, de nagyobb fájl. A .data a
számlálót, az igazítási kitöltést és a puffert is tartalmazza; a
szekcióelrendezés és a fejlécek változása miatt a teljes
méretkülönbséget mérni kell, nem pontosan 4096-nak kijelenteni.
A memória eredete meghatározza a kezdeti tartalom és az élettartam
szerződését. Linuxon a .data kezdőértékei fájlból
származnak, a .bss nulláit a betöltés biztosítja. Ettől a
.bss kisebb fájlt eredményezhet, de a futó programban
továbbra is helyet igényel. A nyers brk végcímet módosít:
eredményét a kért címmel kell összevetni, a használható új rész pedig a
régi végcímen kezdődik. Az mmap külön leképezést adhat.
Egyik rendszerhívás sem helyettesíti önmagában az objektumokat és
igazítást kezelő allokátort.
brk-kel kezelt
tartomány végcíme.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.
brk(2) — a nyers rendszerhívás és a C-könyvtári burkoló
eltérése. Tartalmilag ellenőrizve: a Linux man-pages
projekt 6.19-es kiadása.mmap(2) — anonim leképezés, igazítás és hibakezelés.
Ugyanaz a man-pages 6.19-es kiadás.elf(5) — SHT_NOBITS, szekciók és
betölthető szegmensek. Ugyanaz a man-pages 6.19-es kiadás.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak: a kód itt nem futott; a pontos Ubuntu-csomagverziókat, a QEMU adott kiadásának viselkedését, az ELF-elrendezést és a fájlméretkülönbséget a laborban kell rögzíteni.