7.5 Statikus és dinamikus memória: .bss, .data és egyszerű allokáció

7.5.1 Miért számít, honnan származik a memória?

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.

7.5.2 Statikus memória: .data és .bss

A 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:

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.

7.5.3 Miért kisebb a fájl .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.

7.5.4 Verem, heap és statikus tárolá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.

7.5.5 A brk és az mmap szerepe

A 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:

  1. Nulla cím megadásával lekérdezhetjük az aktuális töréspontot.
  2. Növeléskor új végcímet adunk meg, nem bájtszámot.
  3. Siker esetén a visszatérési érték a beállított végcím.
  4. Sikertelen növeléskor Linuxon az aktuális töréspont érkezik vissza, nem a szokásos negatív -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 BB, a kért növelés pedig nn bájt, akkor siker után az újonnan használható tartomány:

[B,B+n) [B,\ B+n)

Itt BB folyamatbeli virtuális bájtcím; n>0n>0, é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.

7.5.6 Kidolgozott példa: statikus puffer és egyszerű heap-növelés

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/a026

A 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,"",@progbits

Az 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:

A szimbolikus címképzés miatt nincs szükség kézzel választott folyamatcímre.

7.5.6.1 Összeállítás, linkelés, futtatás

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.

7.5.7 Hibák és ellenpróbák

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-memory

A 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 quit

Vá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.

7.5.8 Feladatok

  1. (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?

  2. (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.

  3. (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?

7.5.9 Megoldások

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. 2⋅4096=81922 \cdot 4096 = 8192; 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ú: 2⋅4096=81922\cdot4096=8192 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.

7.5.10 Rövid összefoglaló

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.

7.5.11 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 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.