Bakhönnun gagnaskipana
Þegar verkefni verða stærri en einföld lykilorðsathugun fara forrit að geyma gögnin sín í gagnaskipanum (e. data structures): fylkjum, listum, trjám og töflum. Til að skilja hvað forritið gerir þurfum við að bera kennsl á þessar gagnaskipanir þegar þær birtast í bakþýðanda eða í minni.
Hver gagnaskipan skilur eftir sig einkennandi mynstur í kóðanum. Þegar við lærum að þekkja mynstrin getum við oft áttað okkur fljótt á því hvaða gagnaskipan er notuð.
Forsendur
Þessi kafli byggir á C-kaflanum, sérstaklega bendlum, fylkjum og því hvernig á að lesa úttak bakþýðanda.
Af hverju skipta gagnaskipanir máli?
Tökum dæmi: forrit sem geymir orðalista. Það gæti notað:
- fylki: einfalt en leit getur verið hæg
- tætitöflu: mjög hröð uppfletting
- tré: raðað og hægt að leita hratt
Sama verkefnið má útfæra á þrjá ólíka vegu og hver útfærsla lítur öðruvísi út í bakþýðandanum. Ef við þekkjum gagnaskipanina sjáum við hvernig gögnin tengjast og eigum auðveldara með að rekja okkur að fánanum.
Grunneiningin: grunngerð
Næstum allar flóknari gagnaskipanir eru byggðar úr grunngerðum (e. struct). Grunngerð hópar saman nokkrum sviðum (e. fields) í eina heild. Í minni er grunngerð einfaldlega nokkur gildi sem liggja hlið við hlið:
struct User {
int id; // hliðrun 0 (4 bæti)
int age; // hliðrun 4 (4 bæti)
char name[16]; // hliðrun 8 (16 bæti)
}; // alls 24 bæti
Í minni:
block-beta
columns 6
BytesId["bæti 0–3"] BytesAge["bæti 4–7"] BytesName["bæti 8–23"]:4
Id["id"] Age["age"] Name["name[16]"]:4
style BytesId fill:transparent,stroke:transparent,color:#94a3b8
style BytesAge fill:transparent,stroke:transparent,color:#94a3b8
style BytesName fill:transparent,stroke:transparent,color:#94a3b8
style Id fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Age fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Name fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
Reitirnir eru í réttum hlutföllum: id og age taka hvort um sig 4 bæti en name tekur 16. Hliðrunin segir hversu mörg bæti eru frá upphafi grunngerðarinnar að byrjun sviðsins. Sviðið name hefst því 8 bætum frá byrjun User.
Hvernig grunngerð birtist í bakþýðanda
Bakþýðendur bera ekki alltaf kennsl á grunngerðir sjálfkrafa. Í staðinn sjáum við oft fastar hliðranir (e. offsets) út frá einum bendli:
// Það sem bakþýðandinn sýnir í fyrstu:
iVar1 = *(int *)(param_1 + 4); // param_1->age
puts((char *)(param_1 + 8)); // param_1->name
Þegar sami bendillinn birtist með nokkrum föstum hliðrunum, til dæmis +0, +4 og +8, er það vísbending um grunngerð.
Skilgreinum grunngerð í bakþýðandanum
Tólin geta sýnt þetta mun skýrar ef við skilgreinum grunngerð og beitum henni á breytuna. Þá breytist param_1 + 8 í læsilegt param_1->name.
- Í Ghidra búum við til grunngerð í Data Type Manager og hægrismellum síðan á breytuna → Retype variable (
Ctrl+L). - Í IDA búum við til grunngerð í Structures-glugganum og ýtum síðan á
Ytil að gefa breytunni gagnatagið. - Í Binary Ninja skilgreinum við gagnatagið í Types-glugganum og ýtum síðan á
Yá breytunni.
Fylki
Einfaldasta gagnaskipanin er röð gilda af sömu gerð sem liggja samfellt í minni. Við flettum gildunum upp með vísitölu.
int scores[5];
scores[2] = 99;
Hvernig á að þekkja fylki
Í smalamáli og bakþýðanda birtist aðgangur að fylki oft sem grunnvistfang að viðbættri vísitölu sem er margfölduð með stærð hvers staks:
// arr[i] þar sem arr er int (4 bæti):
iVar1 = *(int *)(arr + i * 4);
mov eax, dword ptr [rbx + rcx*4] ; eax = arr[rcx]
Mynstrið grunnvistfang + vísitala * stærð er skýrasta merkið um fylki.
Keðjulisti
Keðjulisti er röð hnúta þar sem hver hnútur geymir gögn og bendil á næsta hnút.
struct Node {
int data;
struct Node *next; // bendir á sömu gerð!
};
%%{init: {"themeVariables": {"fontSize": "18px"}}}%%
flowchart LR
Head["head"] --> Node1
subgraph Node1["Hnútur 1"]
direction LR
Data1["data: 12"] ~~~ Next1["next"]
end
subgraph Node2["Hnútur 2"]
direction LR
Data2["data: 7"] ~~~ Next2["next"]
end
Next1 --> Node2
Next2 --> Null["NULL"]
style Head fill:transparent,stroke:transparent,color:#94a3b8
style Null fill:transparent,stroke:transparent,color:#94a3b8
style Data1 fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Data2 fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Next1 fill:#4c1d95,stroke:#a78bfa,color:#ffffff
style Next2 fill:#4c1d95,stroke:#a78bfa,color:#ffffff
Lykilmerkið
Grunngerð sem inniheldur bendil á aðra grunngerð sömu tegundar. Þegar við sjáum slíka sjálftilvísun er það vísbending um tengda gagnaskipan, til dæmis keðjulista eða tré.
Yfirferð keðjulista getur litið svona út í bakþýðanda:
// Að fara í gegnum allan listann:
for (node = head; node != NULL; node = *(void **)(node + 8)) {
// node + 8 er 'next' bendillinn
process(*(int *)(node + 0)); // node->data
}
Lykkja sem endar á node = node->next, það er les bendil úr hnútnum og notar hann sem nýja hnútinn, er einkenni keðjulista.
Tré og tvíundatré
Tré svipar til keðjulista en hver hnútur getur haft fleiri en einn bendil á börn. Algengt dæmi er tvíundatré þar sem hver hnútur hefur í mesta lagi tvö börn:
struct TreeNode {
int value;
struct TreeNode *left; // vinstra barn
struct TreeNode *right; // hægra barn
};
flowchart TB
Root["8"] --> Left["3"]
Root --> Right["10"]
Endurkvæmni gefur tré upp
Endurkvæmni birtist oft þegar unnið er með tré, það er í föllum sem kalla á sjálf sig fyrir vinstra og hægra barn:
void walk(struct TreeNode *node) {
if (node == NULL) return;
walk(node->left); // kallar á sjálft sig
process(node->value);
walk(node->right); // kallar á sjálft sig aftur
}
Þegar fall kallar á sjálft sig tvisvar með tveimur ólíkum bendlum úr sömu grunngerð er það sterk vísbending um yfirferð tvíundatrés.
Vissir þú?
Röðin sem við heimsækjum hnútana í skiptir máli. Við getum til dæmis farið fyrst um vinstra undirtréð, síðan rótina og loks hægra undirtréð (e. inorder traversal). Aðrar algengar raðir eru preorder og postorder. Í tvíleitartré skilar inorder-yfirferð gildunum í stærðarröð, sem getur hjálpað okkur að raða saman fána.
Tætitafla
Tætitafla geymir pör lykla og gilda og notar tætifall til að reikna í hvaða sæti hvert gildi fer. Þetta gefur mjög hraða uppflettingu.
Lykilmerkið
Fylki ásamt reikningi sem breytir lykli í vísitölu, oft með margföldun, XOR og afgangsdeilingu (%):
unsigned hash = 0;
for (int i = 0; key[i]; i++)
hash = hash * 31 + key[i]; // dæmigert tætifall
index = hash % TABLE_SIZE; // afgangsdeiling → sæti í fylkinu
Þegar við sjáum streng felldan saman í eina tölu með margföldun og % og töluna síðan notaða sem vísitölu í fylki er það vísbending um tætitöflu.
Net
Net (e. graph) er almenn tengd gagnaskipan: hnútar tengjast með brúnum án fastrar stigskiptingar eins og í tré. Net er oft geymt sem grannfylki, sem er tvívítt fylki, eða grannlistar, sem eru fylki af listum.
Net birtast í þrautum þar sem við þurfum að finna leið milli hnúta. Slík verkefni leysum við oft með leitarreikniritum eins og þverleit (e. breadth-first search, BFS) og lóðleit (e. depth-first search, DFS). Dýpri umfjöllun er í æfingaverkefninu Metro Map.
Yfirlit: hvernig á að þekkja skipanina
| Mynstur sem birtist í bakþýddum kóða | Líkleg gagnaskipan |
|---|---|
grunnvistfang + vísitala * stærð |
fylki |
Sami bendill með föstum hliðrunum (+0, +4, +8) |
grunngerð |
| Grunngerð með einum bendli á sömu tegund | oft keðjulisti |
| Grunngerð með tveimur bendlum á sömu tegund | oft tvíundatré eða tvítengdur listi |
| Grunngerð með fylki af bendlum á sömu tegund | tré með mörgum börnum |
| Fall sem kallar á sjálft sig tvisvar | yfirferð um tré |
Margföldun + % til að fá vísitölu |
tætitafla |
| Tvívítt fylki af tengingum eða listi af nágrönnum | net / graf |
Að skoða gagnaskipanir á keyrslutíma í GDB
Stundum er auðveldast að rekja gagnaskipanina á meðan forritið er í keyrslu. Í GDB getum við elt bendla á milli hnúta:
# Skoða hnút (grunngerð með data við hliðrun 0 og next við hliðrun 8)
x/2gx 0x5555555592a0 # fyrsta gildið inniheldur data og uppfyllingu; annað er next
# Elta 'next' bendilinn í næsta hnút
x/2gx <gildið sem next benti á>
Ef við höfum skilgreint grunngerð í bakþýðandanum eða notum pwndbg getum við einnig prentað heilan hnút í einu. Þetta er sérstaklega gagnlegt þegar tréð eða listinn er byggður upp á keyrslutíma og er ekki til staðar í kyrrstæðu gögnunum.
Teiknum upp gagnaskipanina
Þegar við eltum bendla er gagnlegt að teikna kassana og örvarnar á blað. Tré og listar verða skýrari þegar við sjáum þau fyrir okkur og við áttum okkur fyrr á því í hvaða röð við eigum að lesa gögnin.
Æfingar
Þessi verkefni æfa okkur í að bera kennsl á og rekja gagnaskipanir. Þau eru ívið erfiðari en fyrri verkefni og reyna á allt sem við höfum lært:
- spell-checker: Við skoðum hvernig forritið skipuleggur gögn á keyrslutíma og sækir upplýsingar úr þeim.
- Family Tree: Við rekjum tengsl milli hnúta og ákveðum í hvaða röð eigi að lesa gögn þeirra.
- Metro Map: Við skoðum tengingar milli staða á korti og finnum leið sem uppfyllir skilyrði verkefnisins.
- The Long Way Round: Við skoðum reiknirit sem tekur óraunhæfan tíma og leitum að skilvirkari nálgun.