Assembly (x86-64)
Ассемблер x86-64 (NASM, Linux) за 20 минут: регистры, секции, mov, арифметика, стек, переходы, циклы, syscall и вызов функций на примерах.
Ассемблер x86-64 (синтаксис NASM, Linux) — это язык, в котором каждая инструкция почти напрямую соответствует машинной команде процессора. Вы работаете с регистрами, памятью и системными вызовами вручную. Здесь нет циклов for, объектов и сборщика мусора — только данные, адреса и переходы. Весь язык на одной странице, всё объяснено в комментариях кода. Собрать и запустить: nasm -f elf64 prog.asm && ld prog.o -o prog && ./prog.
Что такое ассемблер и регистры
Регистры — это сверхбыстрые ячейки внутри процессора. В x86-64 они 64-битные.
; Это однострочный комментарий: всё после ';' игнорируется ассемблером.
; Ассемблер NASM нечувствителен к регистру в мнемониках, но метки чувствительны.
; --- Регистры общего назначения (64 бита каждый) ---
; rax ; аккумулятор: результаты арифметики, номер syscall, возврат функции
; rbx ; база: обычно "сохраняемый" регистр (callee-saved)
; rcx ; счётчик: используется инструкцией loop и сдвигами
; rdx ; данные: старшая часть при умножении/делении, 3-й аргумент
; rsi ; source index: источник при строковых операциях, 2-й аргумент
; rdi ; destination index: приёмник, 1-й аргумент функции
; rbp ; base pointer: указатель на базу кадра стека
; rsp ; stack pointer: указатель вершины стека (НЕ трогать вручную без нужды)
; r8..r15 ; дополнительные регистры общего назначения
; rip ; instruction pointer: адрес следующей инструкции (меняется переходами)
; --- Доступ к частям регистра rax ---
; rax (64 бита) -> eax (младшие 32) -> ax (16) -> al (младшие 8) / ah (биты 8-15)
; Запись в eax обнуляет старшие 32 бита rax. Запись в al — нет.
Секции программы
Программа делится на секции: инициализированные данные, неинициализированные данные и код.
section .data ; инициализированные данные (есть начальное значение)
message db "Привет", 0xA ; db = define byte; 0xA — символ перевода строки
msg_len equ $ - message ; equ — константа времени сборки;
; '$' — текущий адрес, '$ - message' = длина строки
number dq 42 ; dq = define quadword (8 байт), число 42
pi dd 3 ; dd = define doubleword (4 байта)
section .bss ; неинициализированные данные (только резервируем место)
buffer resb 64 ; resb = reserve bytes: 64 байта под буфер
counter resq 1 ; resq = reserve quadword: одно 8-байтовое число
section .text ; секция с исполняемым кодом
global _start ; делаем метку _start видимой для компоновщика (ld)
_start: ; точка входа в программу
; ... код будет здесь ...
nop ; nop = no operation, ничего не делает
Пересылка данных: mov и lea
mov копирует значение, lea вычисляет адрес.
section .text
global _start
_start:
mov rax, 10 ; положить число 10 в rax (немедленное значение)
mov rbx, rax ; скопировать значение из rax в rbx (теперь rbx = 10)
mov rcx, [number] ; загрузить из памяти по метке number в rcx (значение)
mov [counter], rax ; сохранить значение rax в память по адресу counter
; lea — "load effective address": кладёт АДРЕС, а не значение по адресу
lea rsi, [message] ; rsi = адрес начала строки message
lea rdx, [rax + rbx*2 + 4] ; rdx = rax + rbx*2 + 4 (быстрый расчёт адреса)
; mov НЕ умеет копировать память -> память за одну инструкцию:
; mov [counter], [number] ; ОШИБКА! Нужно через регистр-посредник:
mov rax, [number]
mov [counter], rax
Арифметика
section .text
global _start
_start:
mov rax, 7
add rax, 3 ; rax = rax + 3 -> 10
sub rax, 4 ; rax = rax - 4 -> 6
inc rax ; rax = rax + 1 -> 7 (инкремент)
dec rax ; rax = rax - 1 -> 6 (декремент)
neg rax ; rax = -rax -> -6 (смена знака)
; Умножение: imul (со знаком). Двухоперандная форма удобнее:
mov rax, 6
imul rax, 7 ; rax = rax * 7 -> 42
; Беззнаковое mul: результат в паре rdx:rax (старшие:младшие 64 бита)
mov rax, 1000000
mov rbx, 1000000
mul rbx ; rdx:rax = rax * rbx; rax — младшая часть
; Деление: делим rdx:rax на операнд. Частное -> rax, остаток -> rdx.
mov rax, 17
xor rdx, rdx ; ОБЯЗАТЕЛЬНО обнулить rdx перед делением!
mov rbx, 5
div rbx ; rax = 17/5 = 3, rdx = 17 mod 5 = 2
Стек: push и pop
Стек растёт вниз (в сторону меньших адресов). rsp всегда указывает на вершину.
section .text
global _start
_start:
mov rax, 100
mov rbx, 200
push rax ; положить rax на стек: rsp -= 8, [rsp] = rax
push rbx ; положить rbx: rsp -= 8, [rsp] = rbx
; ... здесь rax и rbx можно свободно портить ...
mov rax, 0
mov rbx, 0
pop rbx ; снять с вершины в rbx: rbx = [rsp], rsp += 8 -> 200
pop rax ; снять следующее в rax -> 100 (порядок зеркальный!)
; Важно: что положили последним — то снимаем первым (LIFO).
; Резерв места под локальные переменные:
sub rsp, 16 ; выделить 16 байт на стеке
mov qword [rsp], 5 ; записать туда число
add rsp, 16 ; освободить место обратно
Сравнения и переходы, флаги
cmp вычитает операнды и выставляет флаги, но результат не сохраняет.
section .text
global _start
_start:
mov rax, 5
mov rbx, 8
cmp rax, rbx ; сравнить rax и rbx (внутри: rax - rbx), флаги обновятся
; Условные переходы читают флаги после cmp:
je equal ; jump if equal (rax == rbx)
jne not_equal ; jump if not equal (rax != rbx)
jg greater ; jump if greater (rax > rbx, со знаком)
jl less ; jump if less (rax < rbx, со знаком)
jge ge ; jump if greater or equal (>=)
jle le ; jump if less or equal (<=)
; Для беззнаковых: ja (above), jb (below), jae, jbe
jmp done ; jmp — безусловный переход (всегда)
equal: nop
not_equal: nop
greater: nop
less: nop
ge: nop
le: nop
done:
; test — побитовое И без сохранения, удобно проверять на ноль:
test rax, rax ; выставит флаг нуля (ZF), если rax == 0
jz is_zero ; jz == je: переход, если результат был нулём
is_zero:
nop
Циклы
Цикл строится из cmp + переход назад, либо инструкцией loop через rcx.
section .text
global _start
_start:
; --- Цикл вручную: посчитать сумму 1..5 ---
mov rax, 0 ; rax — накопитель суммы
mov rcx, 1 ; rcx — счётчик i = 1
.loop_start:
cmp rcx, 5 ; сравнить i с 5
jg .loop_end ; если i > 5 — выйти
add rax, rcx ; сумма += i
inc rcx ; i++
jmp .loop_start ; вернуться к проверке
.loop_end:
; теперь rax = 1+2+3+4+5 = 15
; Метки с точкой '.loop_start' — локальные (привязаны к ближайшей обычной метке)
; --- Цикл через инструкцию loop (rcx — счётчик повторений) ---
mov rcx, 3 ; повторить 3 раза
.repeat:
; ... тело цикла ...
loop .repeat ; rcx--; если rcx != 0 — прыжок на .repeat
Системные вызовы (syscall)
Обращение к ядру Linux: номер вызова в rax, аргументы в rdi, rsi, rdx.
section .data
text db "Hello", 0xA ; строка + перевод строки
len equ $ - text ; длина строки
section .text
global _start
_start:
; write(fd=1, buf=text, count=len): номер syscall = 1
mov rax, 1 ; номер системного вызова write
mov rdi, 1 ; 1-й аргумент: файловый дескриптор 1 = stdout
mov rsi, text ; 2-й аргумент: адрес буфера со строкой
mov rdx, len ; 3-й аргумент: сколько байт вывести
syscall ; передать управление ядру
; exit(code=0): номер syscall = 60
mov rax, 60 ; номер системного вызова exit
mov rdi, 0 ; код возврата 0 (успех)
syscall ; программа завершается ЗДЕСЬ (иначе segfault)
; Соглашение syscall (Linux x86-64):
; rax=номер, аргументы: rdi, rsi, rdx, r10, r8, r9; результат -> rax
Работа с памятью и адресация
Квадратные скобки [...] означают "взять значение по адресу".
section .data
arr dq 10, 20, 30, 40 ; массив из четырёх 8-байтовых чисел
section .text
global _start
_start:
lea rbx, [arr] ; rbx = адрес начала массива
mov rax, [rbx] ; rax = arr[0] = 10 (значение по адресу rbx)
mov rax, [rbx + 8] ; rax = arr[1] = 20 (смещение на 8 байт = 1 элемент)
mov rax, [rbx + 16] ; rax = arr[2] = 30
; Масштабируемая адресация: [база + индекс*размер + смещение]
mov rcx, 3 ; хотим элемент с индексом 3
mov rax, [rbx + rcx*8] ; rax = arr[3] = 40 (rcx*8 — каждый элемент 8 байт)
; Размер операнда при записи в "безымянную" память указывают явно:
mov qword [rbx], 99 ; записать 8-байтовое 99 в arr[0]
mov byte [rbx + 8], 7 ; записать 1 байт
; Размеры: byte (1), word (2), dword (4), qword (8)
Вызов функций: call, ret и соглашение о вызовах
Аргументы передаются в rdi, rsi, rdx, rcx, r8, r9, результат — в rax.
section .text
global _start
_start:
mov rdi, 7 ; 1-й аргумент функции
mov rsi, 5 ; 2-й аргумент функции
call add_two ; вызвать: push адрес возврата в стек, прыжок в add_two
; после возврата результат в rax (= 12)
mov rdi, rax ; передать результат как код выхода
mov rax, 60
syscall ; exit(12)
; Функция: складывает rdi и rsi, результат в rax
add_two:
push rbp ; пролог: сохранить старый базовый указатель
mov rbp, rsp ; завести новый кадр стека
mov rax, rdi ; rax = первый аргумент
add rax, rsi ; rax += второй аргумент
mov rsp, rbp ; эпилог: восстановить вершину стека
pop rbp ; вернуть старый rbp
ret ; снять адрес возврата со стека и прыгнуть туда
; Соглашение System V (Linux):
; callee-saved (функция обязана сохранить): rbx, rbp, r12-r15
; caller-saved (может затереться): rax, rcx, rdx, rsi, rdi, r8-r11
Полный пример: вывод строки и сумма чисел
Программа считает сумму 1..10, выводит строку "Hello" и завершается с кодом, равным сумме.
; Сборка и запуск:
; nasm -f elf64 prog.asm -o prog.o
; ld prog.o -o prog
; ./prog ; echo "Код возврата: $?"
section .data
hello db "Hello, Assembly!", 0xA ; строка для вывода + \n
hello_len equ $ - hello ; её длина
section .text
global _start
_start:
; --- 1) Вывести строку в stdout через write ---
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
mov rsi, hello ; адрес строки
mov rdx, hello_len ; длина
syscall
; --- 2) Посчитать сумму чисел от 1 до 10 ---
xor rax, rax ; rax = 0 (накопитель), xor с самим собой = обнуление
mov rcx, 1 ; счётчик i = 1
.sum_loop:
cmp rcx, 10 ; пока i <= 10
jg .sum_done
add rax, rcx ; сумма += i
inc rcx ; i++
jmp .sum_loop
.sum_done:
; rax = 55
; --- 3) Завершиться с кодом возврата = сумма ---
mov rdi, rax ; код выхода = 55
mov rax, 60 ; syscall exit
syscall ; проверить потом: echo $? -> 55