Язык ассемблера
Наш подопытный кролик — 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 бит | Два младших байта |
---|---|---|
eax | ax | ah , al |
ebx | bx | bh , bl |
ecx | cx | ch , cl |
edx | dx | dh , dl |
esi | si | — |
edi | di | — |
ebp | bp | — |
(Есть ещё регистр 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
См. также справочник.