Smalamál
Þrep í þýðingarferlinu
Þegar við þýðum C-forrit gerist eftirfarandi:
flowchart LR
Source["C-kóði"] -->|Þýðandi| Assembly["Smalamál"]
Assembly -->|Smali| Machine["Vélarkóði"]
Við vinnum á smalamálsstiginu, mun nær vélbúnaðinum en þegar við lesum C-kóða.
Smalamálið (e. assembly language) sem við skoðum hér er x86-64, einnig nefnt AMD64. Það er algengt á 64-bita Linux- og Windows-kerfum. x86-64 notar flókið skipanasett (e. Complex Instruction Set Computer, CISC), þar sem skipanir eru breytilegar að lengd, frá 1 upp í 15 bæti. Í sumum öðrum skipanasettum eru skipanir allar jafn langar.
Sjá smalakóða í rauntíma
Á godbolt.org getum við skrifað C-kóða í vafranum og séð smalakóðann samstundis. Það hjálpar okkur að tengja saman C og smalamál.
Gistar
Gistar eru mjög hraðvirkir geymslustaðir beint inni í örgjörvanum. Helstu gistar í x86-64:
| Gisti | Notkun |
|---|---|
rax |
Gisti fyrir skilagildi, niðurstaða falls |
rbx |
Almennur gisti |
rcx |
4. frumbreyta í föllum samkvæmt System V-kallvenjunni |
rdx |
3. frumbreyta í föllum |
rsi |
2. frumbreyta í föllum |
rdi |
1. frumbreyta í föllum |
r8–r15 |
Almennir gistar |
rsp |
Staflabendir (e. stack pointer): bendir efst á staflann |
rbp |
Rammabendir (e. frame pointer): vísar í grunn núverandi vakningarfærslu þegar rammabendir er notaður |
rip |
Vistfangsgisti skipunar (e. instruction pointer): geymir vistfang næstu skipunar |
Mismunandi stærðir
Sami gistinn hefur mismunandi nöfn eftir stærð:
64 bitar: rax (8 bæti)
32 bitar: eax (4 bæti) ← neðri helmingur rax
16 bitar: ax (2 bæti)
8 bitar: al (1 bæti)
Gott að muna
Í 64-bita Linux-föllum samkvæmt System V-kallvenjunni berast fyrstu sex frumbreyturnar í rdi, rsi, rdx, rcx, r8 og r9. Skilagildið kemur til baka í rax.
Kallvenjur
Kallvenja skilgreinir hvernig frumbreytur eru sendar í föll og hvernig skilagildi koma til baka.
64-bita Linux: System V AMD64 ABI
Þetta er staðlaða kallvenjan á 64-bita Linux-kerfum:
| Röð frumbreyta | Gisti |
|---|---|
| 1. | rdi |
| 2. | rsi |
| 3. | rdx |
| 4. | rcx |
| 5. | r8 |
| 6. | r9 |
| 7+. | Á stafla |
Skilagildi: rax (og rdx fyrir 128-bita skilagildi)
; C: int result = add(1, 2, 3);
mov rdi, 1 ; 1. frumbreyta
mov rsi, 2 ; 2. frumbreyta
mov rdx, 3 ; 3. frumbreyta
call add
; Niðurstaðan er nú í rax
32-bita Linux: cdecl
Í 32-bita Linux með cdecl eru allar frumbreytur settar á staflann í öfugri röð:
; C: int result = add(1, 2, 3);
push 3 ; Síðasta frumbreytan fyrst
push 2
push 1 ; Fyrsta frumbreytan efst á staflanum
call add
add esp, 12 ; Hreinsa staflann eftir kall (3 × 4 bæti)
; Niðurstaðan er í eax
32-bita og 64-bita skrár í CTF
Skipunin file sýnir okkur hvort tvíundaskrá er 32-bita eða 64-bita. Það skiptir meðal annars máli við nýtingu tvíundaskráa, þar sem lausnin þarf að fylgja réttri kallvenju.
Stafli
Staflinn er hluti af minni forritsins sem notaður er til að geyma:
- Staðværar breytur
- Skilavistfang þegar fall er kallað
- Vistuð gistigildi
Staflinn vex niður á við; þegar gögnum er bætt við minnkar rsp. Myndin sýnir eina vakningarfærslu með 8-bæta sætum, dæmigerðum vistföngum og hliðrunum frá rbp:
block-beta
columns 3
High["↑ Hærra vistfang"] space:2
AddrReturn["0x1028"] Return["Skilavistfang"] ReturnOffset["rbp + 8"]
AddrSaved["0x1020"] Saved["Vistað rbp"] Rbp["rbp + 0 · ← rbp"]
AddrLocal1["0x1018"] Local1["Staðvær breyta 1"] Local1Offset["rbp − 8"]
AddrLocal2["0x1010"] Local2["Staðvær breyta 2"] Rsp["rbp − 16 · ← rsp"]
Low["↓ Lægra vistfang"] space:2
style Return fill:#4c1d95,stroke:#a78bfa,color:#ffffff
style Saved fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Local1 fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style Local2 fill:#1e3a8a,stroke:#60a5fa,color:#ffffff
style High fill:transparent,stroke:transparent,color:#94a3b8
style Low fill:transparent,stroke:transparent,color:#94a3b8
style AddrReturn fill:transparent,stroke:transparent,color:#94a3b8
style AddrSaved fill:transparent,stroke:transparent,color:#94a3b8
style AddrLocal1 fill:transparent,stroke:transparent,color:#94a3b8
style AddrLocal2 fill:transparent,stroke:transparent,color:#94a3b8
style ReturnOffset fill:transparent,stroke:transparent,color:#94a3b8
style Rbp fill:transparent,stroke:transparent,color:#94a3b8
style Local1Offset fill:transparent,stroke:transparent,color:#94a3b8
style Rsp fill:transparent,stroke:transparent,color:#94a3b8
Þegar fallið hefur vistað eldra gildi rbp og tekið frá pláss fyrir staðværar breytur vísar rbp á fastan grunn vakningarfærslunnar en rsp á lægsta notaða vistfang hennar, 0x1010 í dæminu. Skilavistfangið er sérstaklega mikilvægt við nýtingu tvíundaskráa. Vistföng lækka eftir því sem staflinn vex og rsp færist því niður myndina. Tölurnar eru aðeins dæmi; raunveruleg vistföng ráðast meðal annars af keyrsluumhverfinu og slembistaðsetningu minnis.
Skipanirnar push og pop
push rax ; rsp -= 8, [rsp] = rax
pop rbx ; rbx = [rsp], rsp += 8
Algengar skipanir
Gagnaflutningsskipanir
mov rax, 5 ; rax = 5
mov rax, rbx ; rax = rbx
mov rax, [rbx] ; rax = gildið á vistfanginu sem rbx bendir á
mov [rax], rbx ; Skrifar rbx á vistfangið sem rax bendir á
lea rax, [rbx+8] ; rax = rbx + 8 (reiknar vistfang, les ekki minni)
Reikningsskipanir
add rax, rbx ; rax = rax + rbx
sub rax, 10 ; rax = rax - 10
imul rax, rbx ; rax = rax * rbx
xor rax, rax ; rax = 0 (XOR við sjálft sig = 0, algeng leið til að núllstilla)
inc rax ; rax = rax + 1
dec rax ; rax = rax - 1
Samanburður og stökk
Skipanirnar cmp og test geyma ekki niðurstöðuna heldur uppfæra stöðufána (e. status flags) í örgjörvanum. Skilyrtar stökkskipanir lesa þessa fána:
ZF(e. zero flag) segir hvort niðurstaðan hafi verið núll.SF(e. sign flag) segir hvort formerki niðurstöðunnar sé neikvætt.CF(e. carry flag) er meðal annars notaður við samanburð óformerktra talna.OF(e. overflow flag) er meðal annars notaður við samanburð formerktra talna.
cmp rax, rbx ; Ber saman rax og rbx (reiknar rax - rbx en geymir ekki niðurstöðu)
test rax, rax ; Athugar hvort rax sé 0 (AND-aðgerð við sjálft sig)
jmp label ; Hoppar alltaf
je label ; Hoppar ef jafnt
jne label ; Hoppar ef ójafnt
jl label ; Hoppar ef minna, formerktur samanburður
jg label ; Hoppar ef meira, formerktur samanburður
jle label ; Hoppar ef minna eða jafnt, formerktur samanburður
jge label ; Hoppar ef meira eða jafnt, formerktur samanburður
jb label ; Hoppar ef minna, óformerktur samanburður
ja label ; Hoppar ef meira, óformerktur samanburður
jbe label ; Hoppar ef minna eða jafnt, óformerktur samanburður
jae label ; Hoppar ef meira eða jafnt, óformerktur samanburður
je og jne virka eins fyrir formerktar og óformerktar tölur. Þegar stærð talna er borin saman verðum við hins vegar að vita hvort kóðinn túlkar þær sem formerktar eða óformerktar og velja samsvarandi stökkskipanir.
Fallakall
call 0x401234 ; Vistar skilavistfang á stafla og hoppar í fallið
ret ; Sækir skilavistfang af stafla og hoppar þangað
Dæmi: if/else í smalakóða
C-kóði:
if (x == 42) {
puts("Rétt!");
} else {
puts("Rangt.");
}
Smalakóði:
cmp dword ptr [rbp-0x4], 0x2a ; ber saman x við 42 (0x2a í sextándakerfi = 42 í tugakerfi)
jne else_branch ; ef ójafnt, hoppa í else_branch
lea rdi, [rip + str_right] ; 1. frumbreyta: "Rétt!"
call puts
jmp end
else_branch:
lea rdi, [rip + str_wrong] ; 1. frumbreyta: "Rangt."
call puts
end:
Dæmi: for-lykkja í smalakóða
C-kóði:
for (int i = 0; i < 10; i++) {
// ...
}
Smalakóði:
mov dword ptr [rbp-0x4], 0 ; i = 0
loop_start:
cmp dword ptr [rbp-0x4], 9 ; i < 10?
jg loop_end ; ef i > 9, hætta
; ... líkami lykkjunnar ...
add dword ptr [rbp-0x4], 1 ; i++
jmp loop_start
loop_end:
Æfingar
Verkefni til að æfa okkur í að lesa smalamál:
- Runestone: Við æfum okkur í að lesa handskrifað x86-64-smalamál.
- Rotary Dial: Við skoðum hvernig smalamálskóðinn breytir bitum og snúum aðgerðunum við.
- Rube Goldberg: Við greinum flókið x86-64-forrit skref fyrir skref.