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

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

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

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

Читать инструкции процессора в виде чисел очень неудобно (а писать тем более):

    2c93:       48 8d 91 00 00 fe ff
    2c9a:       48 39 c2 
    2c9d:       b8 00 00 02 00
    2ca2:       48 0f 46 c1

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

    lea    -0x20000(%rcx),%rdx
    cmp    %rax,%rdx
    mov    $0x20000,%eax
    cmovbe %rcx,%rax

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

    lea rdx, [rcx - 0x20000]
    cmp rdx, rax
    mov eax, 0x20000
    cmovbe rax, rcx

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

Мы будем использовать синтаксис AT&T, потому что в среде GNU он используется по умолчанию.

Регистры

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

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

Instruction pointer (program counter): eip.

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

РегистрМладшие 16 битДва младших байта
eaxaxah, al
ebxbxbh, bl
ecxcxch, cl
edxdxdh, dl
esisi
edidi
ebpbp

(Есть ещё регистр esp, который мы пока не трогаем.)

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

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

Операнд-регистр записывается после знака процента: %eax.

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

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

movl %eax, %ebx // скопировать биты eax в ebx
                // старое значение ebx теряется
movw %ax, %bx
movb %ah, %bl

Суффиксы размера операндов:

  • b (byte) — 8 бит
  • w (word) — 16 бит
  • l (long) — 32 бита
  • q (quad) — 64 бита (не используем)

Справочник (в синтаксисе Intel)

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

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

movl $42, %ecx   // положить в %ecx битовое представление числа 42

movl $0x80, %edx // шестнадцатеричная запись операнда
movl $-1, %eax   // установить все биты eax в 1

Библиотека simpleio

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

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

    .global main
main:
    call readi32     // считали первое число
    movl %eax, %ecx  // сохранили его в ecx
    call readi32     // считали второе число в eax
    addl %ecx, %eax  // сложили первое и второе
    call writei32    // вывели результат
    call finish      // завершили программу

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

$ gcc -m32 -g sum.S simpleio_i686.S -o sum
$ ./sum

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

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

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

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

movw $0x1234, %ax
shrw $4, %ax        // ax == 0x0123
shlw $4, %ax        // ax == 0x1230
addw $7, %ax        // ax == 0x1237
rorw $4, %ax        // ax == 0x7123

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

movw 0xfff0, %ax    // ax == -16
sarw $4, %ax        // ax == 0xffff == -1
salw $5, %ax        // ax == 0xfff0 == -16

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