Язык ассемблера

Наш подопытный кролик — x86-64

Компьютер IBM PC, выпущенный в 1981 году, оснащался процессором Intel 8088, а более поздние модели — процессорами 80286, 80386 и 80486, поэтому ISA стала известна как x86.

В этом тысячелетии компания AMD расширила x86 для поддержки 64-битных чисел, и результат сначала назывался x86-64, а потом разными другими словами.

Машинный код и язык ассемблера

Читать инструкции процессора в виде чисел очень неудобно (а писать тем более). Рассмотрим для примера реализацию алгоритма Евклида для 32-битных чисел на x86:

39 c8 74 07 77 01 91 29 c8 eb f5 

Допустим, она расположена в памяти начиная с адреса 0x43210, и IP (instruction pointer) процессора равен 0x43210. Посмотрим, что будут значить для процессора эти байты:

43210:  ┌─►  39 c8         // сравнить регистры 0 и 1
43212:  │    74 07 ─┐      // если числа равны, прибавить к IP число 0x07 = 7
43214:  │┌─  77 01  │      // если первое число больше, прибавить к IP число 0x01 = 1
43216:  ││   91     │      // поменять местами регистры 0 и 1
43217:  │└►  29 c8  │      // вычесть регистр 1 из регистра 0
43219:  └──  eb f5  │      // прибавить к IP число 0xf5 = −11
4321b:       ...   ◄┘      // предположительно дальше там вывод результата

Для инструкций придумывают названия (мнемоники) и правила записи их операндов, а потом делают конвертор из такого текстового представления в двоичное (машинный код). Такой конвертор называется ассемблером, а текстовое представление инструкций — языком ассемблера.

43210:       39 c8     cmp eax, ecx     // сравнить регистры 0 и 1
43212:       74 07     je 0x4321b       // если числа равны, прибавить к IP число 0x07 = 7
43214:       77 01     ja 0x43217       // если первое число больше, прибавить к IP число 0x01 = 1
43216:       91        xchg eax, ecx    // поменять местами регистры 0 и 1
43217:       29 c8     sub eax, ecx     // вычесть регистр 1 из регистра 0
43219:       eb f5     jmp 0x43210      // прибавить к IP число 0xf5 = −9
4321b:       ...

В мире x86 исторически больше всего используются два синтаксиса языка ассемблера: AT&T vs Intel. Эти же инструкции в синтаксисе AT&T выглядят так:

    cmp    %ecx,%eax
    je     ...
    ja     ...
    xchg   %eax,%ecx
    sub    %ecx,%eax
    jmp    ...

Можно заметить, что мнемоники инструкций в основном те же, но операнды записываются иначе и идут в другом порядке.

Мы будем использовать синтаксис Intel.

Регистры

«Переменные» внутри процессора.

  von Neumann             closer to reality
┌────────────────┐       ┌────────────────┐
│  CPU           │       │ CPU            │
│                │       │                │
│ ┌────────────┐ │       │                │
│ │Control unit│ │       │ Registers      │
│ │            │ │       │                │
│ │IP          │ │       │ (including IP) │
│ └────────────┘ │       │                │
│                │       │                │
└────────┬───────┘       └────────┬───────┘
         │                        │
         │                        │
┌────────┴───────┐       ┌────────┴───────┐
│  Memory        │       │ Cache(s)       │
│                │       │                │
│                │       │                │
│                │       └────────┬───────┘
│                │                │
│                │       ┌────────┴───────┐
│                │       │ RAM            │
│                │       │                │
│                │       │                │
└────────────────┘       └────────────────┘

Instruction pointer (program counter): rip.

Регистры общего назначения (general purpose registers):

РегистрМладшие 32 битаМладшие 16 битДва младших байта по отдельности
raxeaxaxah, al
rbxebxbxbh, bl
rcxecxcxch, cl
rdxedxdxdh, dl
rsiesisi—, sil
rdiedidi—, dil
rspespsp—, spl
rbpebpbp—, bpl
r8r8dr8w—, r8b
...
r15r15dr15w—, r15b

(Регистры *sp имеют специальное значение, мы их пока не трогаем.)

Первые инструкции

Инструкция выглядит примерно так: мнемоника операнд, операнд.

Наша первая мнемоника: mov.

mov DST, SRC    // копировать SRC в DST

mov ebx, eax   // скопировать биты eax в ebx
               // и выставить старшие 32 бита rbx в 0;
               // старое значение rbx теряется
mov ax, bx
mov ah, bl

Справочник по вариантам инструкции MOV

Список инструкций

Непосредственно заданный операнд:

mov rcx, 42   // положить в rcx битовое представление числа 42

mov rdx, 0x80 // шестнадцатеричная запись операнда
mov eax, -1   // установить все биты eax в 1 (а старшие биты rax в 0)

Библиотека simpleio

call writei32   // напечатать на экране значение edi
                // как знаковое десятичное число
call writei64   // вывести rdi
call readi32    // ввести с клавиатуры число и сохранить в eax
call readi64    // ввести с клавиатуры число и сохранить в rax
call finish     // завершить исполнение программы

Любой вызов подпрограммы сохраняет значения регистров rbx, rsp, rbp, r12–r15. Остальные регистры могут измениться произвольным образом.

Наша первая программа на языке ассемблера x86, вычисляющая сумму двух чисел:

    .intel_syntax noprefix
    .global main
main:
    call readi64     // считали первое число
    mov r12, rax     // сохранили его в r12
    call readi64     // считали второе число
    add r12, rax     // сложили первое и второе
    mov rdi, r12
    call writei64    // вывели результат
    call finish      // завершили программу

Сохраним её в файл sum.S (да, заглавная S), оттранслируем и запустим:

$ gcc -g sum.S simpleio_x86_64.S -o sum
$ ./sum

Некоторые арифметические инструкции

add DST, SRC  // DST += SRC
sub DST, SRC  // DST -= SRC
inc DST       // DST++
dec DST       // DST--
neg DST       // DST = -DST
not DST       // DST = ~DST
and DST, SRC  // DST &= SRC
or  DST, SRC  // DST |= SRC
xor DST, SRC  // DST ^= SRC

Инструкции сдвига

Логический сдвиг: двигаем биты внутри регистра, дополняя его нулями и теряя то, что «выпало».

mov ax, 0x1234
shr ax, 4        // ax == 0x0123
shl ax, 4        // ax == 0x1230
add ax, 7        // ax == 0x1237
ror ax, 4        // ax == 0x7123

Арифметический сдвиг вправо: двигаем биты, дополняя слева знаковым битом

mov ax, 0xfff0   // ax == -16
sar ax, 4        // ax == 0xffff == -1
sal ax, 5        // ax == 0xfff0 == -16

См. также справочник.