Assembler x86-64 a inline assembler v GCC

Michal Sojka
sojkam1@fel.cvut.cz
ČVUT, FEL

Licence: CC-BY-SA 4.0

Registry x86-64

  • 16 obecných 64bitových registrů: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8–r15
  • K jejich částem se dá přistupovat pod jinými jmény, např.:
64 bitů 32 bitů 16 bitů 8 bitů
rax eax ax al
rbx ebx bx bl
rdi edi di dil
r8 r8d r8w r8b
  • Zápis do 32bitové části (např. eax) vynuluje horních 32 bitů (rax)
  • rip – ukazatel instrukcí, rsp – ukazatel zásobníku, rflags – příznaky

Užitečné instrukce

  • mov – přesouvá data mezi registry a pamětí
       mov $1,%rax        # ulož 1 do registru rax
    
    n: .quad 123          # návěští n ukazuje na 64bitovou proměnnou
    
       mov n(%rip),%rax   # ulož hodnotu proměnné do rax (adresa relativně k rip)
    
       mov %rax,%rbx      # zkopíruj hodnotu z rax do rbx
    
  • push/pop – práce se zásobníkem
    • Hodí se, když potřebujeme data uložit na později a nemůžeme k tomu použít registry
    push %rax         # ulož obsah rax na zásobník (vždy 8 bajtů)
    pop %rbx          # vyjmi hodnotu ze zásobníku do rbx
    

Užitečné instrukce 2

  • add – sečte dva operandy
    add $2,%rax      # rax = rax + 2
    add %rax,%rbx    # rbx = rbx + rax
    
  • sub – odečte dva operandy
    sub $2,%rax      # rax = rax – 2
    

Užitečné instrukce 3

  • call – zavolá podprogram
  • ret – vrátí se z podprogramu k volajícímu
plusone:
    add $1,%rax
    ret
main:
    mov $12,%rax
    call plusone
    ...

Užitečné instrukce 4

  • div <op> – celočíselné dělení (není to jednoduchá instrukce)
    https://www.felixcloutier.com/x86/div
    • 64bitový operand: rdx:rax se dělí operandem instrukce <op>
      výsledek: rax = rdx:rax / <op>, rdx = rdx:rax % <op>
      mov $0,%rdx      # horní polovina dělence je 0
      mov $12345,%rax
      mov $10,%rbx
      div %rbx         # rax = 12345/10 = 1234, rdx = 5
      
    • 16/32bitový operand: obdobně s dx:ax, edx:eax
    • 8bitový operand: ax se dělí <op>
      výsledek: al = ax / <op>, ah = ax % <op>
      mov $42,%ax
      mov $12,%bl
      div %bl          # al = 42/12 = 3
      

Užitečné instrukce 5

  • cmp – porovná dvě hodnoty
    cmp $2,%rax      # porovnej rax s 2 a nastav registr rflags
    je label         # skoč na návěští, pokud se rax rovnalo 2
    jl label         # skoč, pokud bylo rax menší
    jg label         # skoč, pokud bylo rax větší
    jle label        # skoč, pokud menší nebo rovno
    jge label        # skoč, pokud větší nebo rovno
    
  • Příklad:
        cmp $0x30,%al
        jl nodigit
        cmp $0x39,%al
        jg nodigit
    digit:
        ... něco udělej ...
    nodigit:
        ... ošetři chybu
    

Extended assembler

  • Dovoluje použití výrazů jazyka C v instrukcích assembleru
  • Programátor specifikuje „šablonu assemblerovských instrukcí“ (viz "mov %%rsp,%0;" níže)
  • Překladač v šabloně nahradí parametry (např. %0 níže) reálnými operandy (registry, adresami v paměti, ...) na základě specifikovaných omezení
  • Kompilátor se nesnaží porozumět tomu, co programátor do šablony napsal
  • Proto musí programátor říct kompilátoru, jak použité instrukce mění stav programu
// Přeložte pomocí gcc -O2 -Wall ...
#include <stdio.h>
int main()
{
    void *stack_ptr;
    asm volatile ("mov %%rsp,%0;" : "=g" (stack_ptr));
    printf("Hodnota registru RSP je %p\n", stack_ptr);
    return 0;
}

Syntaxe extended assembleru

asm ( šablona instrukcí
      : výstupní operandy           /* nepovinné */
      : vstupní operandy            /* nepovinné */
      : co je instrukcemi změněno   /* nepovinné */
    );

Syntaxe operandů za „:“ je:

<omezení> (<výraz v C>), ...

Každý operand se vztahuje k odpovídajícímu parametru (%<číslo>) v šabloně.

https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html

Syntaxe extended assembleru – příklad

#include <stdio.h>

int main(int argc, char *argv[])
{
  int result, op1 = 4, op2 = 2;
  asm volatile (
      "mov %1,%0;"
      "add %2,%0;"
      : "=r" (result)
      : "r" (op1), "r" (op2)
      : "cc");
  printf("result = %d\n", result);
  return 0;
}

%0 odpovídá result, %1 odpovídá op1 a %2 odpovídá op2.

Přeloží se na (viz objdump -d ...):

...
1051: ba 04 00 00 00     mov $0x4,%edx
1056: b8 02 00 00 00     mov $0x2,%eax
...
105e: 89 d2              mov %edx,%edx
1060: 01 c2              add %eax,%edx
...

Omezení v extended assembleru 1

  • V seznamu vstupních nebo výstupních operandů říkají překladači, jaké registry nebo jiné operandy může použít s danou instrukcí v šabloně
  • Obecná omezení
    • "r" – libovolný registr – překladač zajistí, že v registru (např. ebx) bude hodnota proměnné var
    • "m" – místo v paměti – v instrukci bude adresa proměnné var
    • "i" – přímý operand (konstanta)
asm ("mov %0,%%eax" : : "r" (var) : "rax");  // → např. mov %ebx,%eax
asm ("mov %0,%%eax" : : "m" (var) : "rax");  // → např. mov var(%rip),%eax
asm ("mov %0,%%eax" : : "i" (123) : "rax");  // → mov $123,%eax

Omezení v extended assembleru 2

  • Architekturně (HW) závislá omezení
    • "a" – rax (resp. eax, ax, al podle velikosti operandu)
    • "b", "c", "d" – rbx, rcx, rdx
    • "S", "D" – rsi, rdi
  • Modifikátory:
    • "=" před omezením říká, že se jedná o výstupní operand.

Omezení v extended assembleru 3

  • Za 3. dvojtečkou (co je změněno) říkají překladači, co instrukce mění.
  • Může to být např.
    • konkrétní registry, např. "rcx", "r11" (mění je instrukce syscall) – píší se jména registrů, ne omezení jako "a"
      • To je nutné jen pokud nejsou uvedeny jako vstup nebo výstup (za 1. nebo 2. dvojtečkou)
    • "cc" – říká, že instrukce modifikují registr příznaků (condition
      codes, někdy také označovaný jako flags, u x86-64 rflags).
      • Toto je potřeba uvést, například pokud šablona obsahuje instrukci add. Ta totiž nastavuje příznak „carry“ podle toho, jestli výsledek sčítání přetekl nebo ne.
    • "memory" – říká, že instrukce čtou nebo zapisují do paměti (na jiná místa než ta uvedená jako vstup nebo výstup)
      • Pokud toto neuvedete a nemáte nebo v navazujícím kódu nepoužijete výstupní operand (omezení s "="), překladač si může myslet, že daný kus assembleru nedělá nic užitečného a „vyoptimalizuje“ (smaže) ho.
      • Toto budete potřebovat minimálně pro systémová volání read a write, která pracují s pamětí předanou jádru pomocí ukazatele.