Základy x86_64

V této kapitole si projdeme základní stavební kameny programování v x86_64 assembly: registry, základní instrukce a syscally (systémová volání).

Registry

Registr je malá paměť přímo v procesoru, která drží data, se kterými právě pracujete. Práce s registry je extrémně rychlá — mnohem rychlejší než práce s hlavní pamětí (RAM).

V x86_64 máme 16 základních obecných 64bitových registrů:

64-bit32-bit16-bit8-bitTypické použití
raxeaxaxalAkumulátor, návratová hodnota funkcí
rbxebxbxblBase register (volně použitelný)
rcxecxcxclCounter, 4. argument syscallu
rdxedxdxdlData, 3. argument syscallu
rsiesisisilSource index, 2. argument syscallu
rdiedididilDestination index, 1. argument syscallu
rbpebpbpbplBase pointer (rámec zásobníku)
rspespspsplStack pointer (vrchol zásobníku)
r8r15r8dr15dr8wr15wr8br15bObecné registry (r10 = 4. arg syscallu)

Jak to s těmi velikostmi funguje?

Každý registr může být přistupován v různých velikostech. Tohle je historická záležitost z doby, kdy procesory byly 16bitové a pak 32bitové — novější, širší registry pohltily ty starší, úzké. Dnes se tedy nejedná o samostatné registry, ale o různé pohledy na jeden a ten samý fyzický registr.

Vezměme si třeba registr rax. Je 64bitový, tedy obsahuje 64 bitů (8 bajtů) dat. Když přistupujete přes jméno eax, pracujete jen se spodními 32 bity toho samého registru. Přes ax se dostanete ke spodním 16 bitům. A ještě níž — přes al pracujete s úplně nejspodnějším bajtem (bity 0 až 7), zatímco přes ah s bajtem nad ním (bity 8 až 15).

Představte si to jako matrjošku: rax obsahuje eax, eax obsahuje ax, a ax se skládá z ah (vyšší bajt) a al (nižší bajt).

Důsledek je, že když zapíšete mov al, 5, změní se jenom nejspodnější bajt — zbytek registru zůstane nedotčený. Jedna drobná výjimka: pokud zapisujete do 32bitové části (eax), procesor automaticky vynuluje horních 32 bitů rax. U 8bitových a 16bitových zápisů se to neděje. Tohle je typický zdroj bugů, na který si dejte pozor.

Základní instrukce

mov — přesun dat

Nejzákladnější instrukce. Zkopíruje hodnotu ze zdroje do cíle. V Intel syntaxi je to mov cíl, zdroj:

mov rax, 60          ; rax = 60 (okamžitá hodnota)
mov rbx, rax         ; rbx = rax (z registru do registru)
mov rcx, [rsi]       ; rcx = hodnota na adrese v rsi (čtení z paměti)
mov [rdi], rdx       ; do paměti na adrese rdi zapíše rdx (zápis)

Aritmetické instrukce

add rax, 5           ; rax = rax + 5
sub rbx, rcx         ; rbx = rbx - rcx
inc rax              ; rax = rax + 1 (rychlejší než add rax, 1)
dec rbx              ; rbx = rbx - 1
mul rcx              ; rax = rax * rcx (beznaménkové)
neg rax              ; rax = -rax

Logické a bitové operace

and rax, rbx         ; bitové AND
or  rax, rbx         ; bitové OR
xor rax, rax         ; rychlý způsob jak vynulovat rax!
shl rax, 3           ; posun doleva o 3 bity (= násobení 8)
shr rax, 1           ; posun doprava o 1 bit (= dělení 2)

Tip: xor rax, rax se používá místo mov rax, 0, protože je kratší ve strojovém kódu a procesory ji umí zpracovat ještě rychleji.

Porovnání a skoky

cmp rax, 10          ; porovná rax s 10 (nastaví flagy)
je  label            ; skoč na label, pokud rovno (jump if equal)
jne label            ; skoč, pokud nerovno
jl  label            ; skoč, pokud menší (jump if less)
jg  label            ; skoč, pokud větší
jmp label            ; bezpodmínečný skok

Zásobník (stack)

push rax             ; uloží rax na zásobník, rsp se zmenší o 8
pop  rbx             ; vyzvedne hodnotu do rbx, rsp se zvětší o 8
call funkce          ; zavolá funkci (uloží návratovou adresu)
ret                  ; návrat z funkce

Syscally (systémová volání)

Když chce program udělat něco "venku" — vypsat text, otevřít soubor, přečíst ze sítě — musí požádat jádro (kernel) operačního systému. To se dělá pomocí syscallu.

Na Linuxu x86_64 to funguje takto:

  1. Do rax dáte číslo syscallu
  2. Do rdi, rsi, rdx, r10, r8, r9 dáte argumenty (v tomto pořadí)
  3. Zavoláte instrukci syscall
  4. Výsledek najdete v rax

Nejdůležitější syscally

ČísloNázevPopisArgumenty
0readČtení z file descriptorufd, buffer, počet bajtů
1writeZápis do file descriptorufd, buffer, počet bajtů
2openOtevření souborucesta, flags, mode
3closeZavření file descriptorufd
60exitUkončení programunávratový kód

File descriptory

Standardní file descriptory, které máte vždy k dispozici:

  • 0stdin (standardní vstup, typicky klávesnice)
  • 1stdout (standardní výstup, typicky terminál)
  • 2stderr (chybový výstup)

Příklad: ukončení programu s kódem 0

mov rax, 60          ; číslo syscallu "exit"
mov rdi, 0           ; návratový kód 0
syscall              ; provedení syscallu

Příklad: zápis na obrazovku

mov rax, 1           ; syscall "write"
mov rdi, 1           ; fd = 1 (stdout)
mov rsi, zprava      ; adresa textu
mov rdx, 13          ; délka textu
syscall

Úplný seznam syscallů najdete například na filippo.io/linux-syscall-table nebo v man 2 syscalls.


V další kapitole si tyto znalosti spojíme a napíšeme první reálný program!