Hoppa yfir í efnið

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

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.