Основы ассемблера x86-64¶
Ассемблер (assembly language) — это низкоуровневый язык программирования, в котором каждая инструкция почти однозначно
соответствует одной машинной инструкции процессора. В отличие от машинного кода, представляющего собой
последовательность двоичных байт, ассемблер использует мнемоники — читаемые обозначения инструкций (mov, add, jmp)
вместо их числовых кодов.
Разновидности ассемблера¶
Ассемблер зависит от архитектуры процессора: для каждой ISA (Instruction Set Architecture) существует свой набор инструкций и мнемоник. Наиболее распространённые архитектуры:
- x86-64 — 64-битные ПК и серверы (Linux, Windows, macOS);
- x86-32 — устаревшая 32-битная версия x86;
- ARM / AArch64 — мобильные устройства, Apple Silicon, встроенные системы;
- RISC-V — открытая архитектура, активно развивающаяся.
Для x86-64 существуют два синтаксиса, которые по-разному записывают одни и те же инструкции:
Intel-синтаксис (используется в NASM, MASM):
AT&T-синтаксис (используется в GNU as, GCC):
В системах GNU/Linux (в том числе при компиляции GCC) по умолчанию используется AT&T-синтаксис. Регистры записываются с
префиксом %, непосредственные значения — с префиксом $, а суффикс инструкции задаёт размер операнда: b (byte, 8
бит), w (word, 16 бит), l (long, 32 бита), q (quad, 64 бита).
Регистры процессора x86-64¶
Регистры — это быстрые ячейки хранения данных непосредственно внутри процессора. Доступ к ним на порядки быстрее, чем к оперативной памяти. В архитектуре x86-64 доступны 16 регистров общего назначения шириной 64 бита каждый, причём к их подрегистрам можно обращаться по отдельности:
| Регистр (64-бит) | 32-бит | 16-бит | 8-бит | Назначение |
|---|---|---|---|---|
| rax | eax | ax | al | Аккумулятор; возвращаемое значение функции; результат умножения/деления |
| rbx | ebx | bx | bl | Базовый регистр; сохраняется вызываемой функцией (callee-saved) |
| rcx | ecx | cx | cl | Счётчик циклов и сдвигов; 4-й аргумент функции |
| rdx | edx | dx | dl | Данные; часть результата умножения/деления; 3-й аргумент |
| rsi | esi | si | sil | Source index; 2-й аргумент функции |
| rdi | edi | di | dil | Destination index; 1-й аргумент функции |
| rsp | esp | sp | spl | Stack pointer — вершина стека |
| rbp | ebp | bp | bpl | Base pointer — основание стекового фрейма |
| r8–r15 | r8d–r15d | r8w–r15w | r8b–r15b | Дополнительные регистры, появившиеся в x86-64 |
rax (64 бита)
┌────────────────────────────────────────────────────────────────┐
│ rax │
├────────────────────────────────┬───────────────────────────────┤
│ (старшие 32) │ eax │
│ ├───────────────┬───────────────┤
│ │ (старш. 16) │ ax │
│ │ ├───────┬───────┤
│ │ │ ah │ al │
└────────────────────────────────┴───────────────┴───────┴───────┘
64 бита 32 бита 16 бит 8 бит 8 бит
Запись в 32-битный подрегистр (например, eax) обнуляет верхние 32 бита 64-битного регистра (rax). Запись в 8-
или 16-битный подрегистр верхние биты не затрагивает.
Соглашение о вызовах System V AMD64 ABI¶
В Linux x86-64 действует соглашение о вызовах System V AMD64 ABI, определяющее, как функции передают аргументы и возвращают результаты:
- Аргументы (целые числа и указатели) передаются в регистрах:
rdi,rsi,rdx,rcx,r8,r9. Остальные аргументы — на стеке. - Вещественные аргументы передаются в
xmm0–xmm7. - Возвращаемое значение — в
rax(иrdxдля 128-битных результатов). - Callee-saved регистры (должны быть сохранены вызываемой функцией):
rbx,rbp,r12–r15,rsp. - Caller-saved регистры (могут быть изменены функцией):
rax,rcx,rdx,rsi,rdi,r8–r11.
Основные инструкции¶
Пересылка данных¶
Инструкция mov — наиболее часто встречающаяся. Она копирует значение из источника в приёмник:
movq %rax, %rbx # rbx = rax (64-бит)
movl $42, %eax # eax = 42 (32-бит; верхние 32 бита rax обнуляются)
movb $0xFF, %al # al = 0xFF (8-бит)
movq (%rax), %rbx # rbx = *(uint64_t *)rax (загрузка из памяти)
movq %rax, (%rbx) # *(uint64_t *)rbx = rax (сохранение в память)
Квадратные скобки в Intel-синтаксисе соответствуют круглым скобкам в AT&T: (%rax) означает «значение по адресу,
хранящемуся в rax».
Адресация памяти в x86-64 поддерживает обобщённую форму disp(base, index, scale):
movl (%rbx), %eax # [rbx]
movl 8(%rbx), %eax # [rbx + 8]
movl (%rbx, %rcx), %eax # [rbx + rcx]
movl (%rbx, %rcx, 4), %eax # [rbx + rcx*4] — удобно для массивов
movl -4(%rbp), %eax # [rbp - 4] — локальная переменная
Расширяющие пересылки применяются при загрузке узкого значения в широкий регистр:
movsbq %al, %rax # знаковое расширение byte -> qword
movzbq %al, %rax # нулевое расширение byte -> qword
movswq %ax, %rax # знаковое расширение word -> qword
movslq %eax, %rax # знаковое расширение long -> qword (= movsxd)
LEA (Load Effective Address) вычисляет адрес, не обращаясь к памяти:
leaq 8(%rax), %rbx # rbx = rax + 8 (без чтения из памяти)
leaq (%rax, %rcx, 4), %rbx # rbx = rax + rcx*4
LEA часто используется компиляторами для быстрого умножения на 2, 3, 4, 5, 8, 9 за одну инструкцию:
Арифметические инструкции¶
addq %rbx, %rax # rax += rbx
subq $10, %rax # rax -= 10
imulq %rbx, %rax # rax *= rbx (знаковое умножение)
idivq %rbx # rax = rdx:rax / rbx; rdx = остаток (знаковое)
incq %rax # rax++
decq %rax # rax--
negq %rax # rax = -rax
Перед idivq необходимо расширить знак rax в пару rdx:rax с помощью инструкции cqto (convert quad to octa).
Логические и сдвиговые инструкции¶
andq %rbx, %rax # rax &= rbx (побитовое И)
orq %rbx, %rax # rax |= rbx (побитовое ИЛИ)
xorq %rbx, %rax # rax ^= rbx (побитовое XOR)
notq %rax # rax = ~rax (побитовое НЕ)
shlq $3, %rax # rax <<= 3 (логический сдвиг влево)
shrq $3, %rax # rax >>= 3 (логический сдвиг вправо)
sarq $3, %rax # rax >>= 3 (арифметический сдвиг, сохраняет знак)
Типичный идиом для обнуления регистра — xorq %rax, %rax: эта инструкция короче movq $0, %rax и на один байт
компактнее в машинном представлении.
Пример: функция на ассемблере¶
Следующий фрагмент определяет функцию add_numbers, принимающую два аргумента в rdi и rsi и возвращающую их сумму в
rax:
.globl add_numbers
.type add_numbers, @function
add_numbers:
movq %rdi, %rax # rax = первый аргумент
addq %rsi, %rax # rax += второй аргумент
ret # возврат; результат в rax
Объявление и использование из C:
long add_numbers(long a, long b);
int main(void) {
long result = add_numbers(5, 3); /* rdi=5, rsi=3 -> rax=8 */
return 0;
}
Сборка:
Связанные темы¶
- Условные переходы и флаги — управление потоком через RFLAGS и
jcc - Стековые фреймы и вызовы функций — пролог/эпилог, ABI в деталях
- Ассемблерные функции и GDB — интеграция
.s-файлов с C, отладка - Встроенный ассемблер в GCC —
asm(), constraints, rdtsc - Режимы процессора и системные вызовы — инструкция
syscall
Источники¶
man 1 as— документация GNU Assemblerman 1 gcc— компилятор GCC и флаги ассемблирования- System V Application Binary Interface AMD64 Supplement: https://gitlab.com/x86-psABIs/x86-64-ABI
- Intel 64 and IA-32 Architectures Software Developer's Manual: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- AT&T Assembly Syntax Overview (GAS manual): https://sourceware.org/binutils/docs/as/