Язык ассемблера — это текстовая запись машинных команд конкретной архитектуры процессора: одна строка вида mov eax, 60 обычно соответствует одной инструкции, которую процессор выполняет напрямую. Ассемблер в строгом смысле — программа-транслятор (NASM, GNU as, MASM), которая превращает этот текст в машинный код; в разговоре так часто называют и сам язык.
Содержание
- Мини-словарь: пять слов, которые путают
- Два синтаксиса x86: Intel и AT&T
- Регистры x86-64
- Основные команды
- Первая программа: Hello на NASM под Linux
- Цикл и условный переход: сумма от 1 до 10
- Функция: call, ret и соглашение о вызовах
- Если не получилось
- Зачем ассемблер в 2026 году
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже разберем, как язык связан с машинным кодом, какие регистры есть у x86-64, какие команды нужны для первых программ, и соберем три рабочие программы под Linux. Примеры проверены 24.09.2026 в Ubuntu 24.04 (x86-64) с NASM 2.16.01 и GNU ld 2.42 — это версии из пакетов Ubuntu 24.04; у самого NASM уже вышла ветка 3.x, для примеров статьи разницы нет.
Мини-словарь: пять слов, которые путают
- Машинный код — байты, которые исполняет процессор. Например,
B8 3C 00 00 00. - Язык ассемблера — человекочитаемая запись тех же команд:
mov eax, 60. - Ассемблер (транслятор) — программа, переводящая текст в объектный файл (
.o). - Компоновщик (linker) — программа
ld, которая собирает объектные файлы в исполняемый. - Дизассемблер — обратный путь: байты -> мнемоники. Например,
objdump -d.
Сквозная цепочка на одном примере: текст mov eax, 60 -> NASM -> байты B8 3C 00 00 00 -> процессор записывает число 60 в регистр eax. Байты здесь не выдуманы, их показал objdump при сборке программы ниже.
Важная граница: языка ассемблера «вообще» не существует. Набор команд x86-64 (Intel, AMD) и AArch64 (ARM, в том числе Apple Silicon) разный, а у одной архитектуры бывает несколько синтаксисов. Все дальше относится к x86-64 и Linux.
Два синтаксиса x86: Intel и AT&T
Одну и ту же инструкцию можно записать двумя способами. NASM и MASM используют синтаксис Intel, GNU as по умолчанию — AT&T (Intel включается директивой .intel_syntax noprefix).
| Что сравниваем | Intel (NASM) | AT&T (GNU as) |
|---|---|---|
| Порядок операндов | куда, откуда: mov rax, 1 |
откуда, куда: movq $1, %rax |
| Регистры | rax |
%rax |
| Константы | 1 |
$1 |
| Обращение к памяти | [rbx + 8] |
8(%rbx) |
Главная ловушка — порядок операндов: mov a, b в Intel копирует b в a, в AT&T наоборот. Дизассемблер objdump по умолчанию печатает AT&T, а с ключом -M intel — Intel. В статье везде синтаксис Intel.
Регистры x86-64
Регистр — ячейка памяти внутри процессора. Операции с регистрами быстрее, чем с оперативной памятью, поэтому команды в основном работают с ними. У x86-64 шестнадцать 64-битных регистров общего назначения, и к их младшим частям можно обращаться по отдельным именам.
| 64 бита | 32 бита | 16 бит | 8 бит | Типичная роль |
|---|---|---|---|---|
rax |
eax |
ax |
al |
результат функции, номер системного вызова |
rdi, rsi, rdx |
edi, esi, edx |
di, si, dx |
dil, sil, dl |
первые аргументы функций и системных вызовов |
rcx |
ecx |
cx |
cl |
счетчик; портится командой syscall |
rbx, rbp, r12—r15 |
ebx… r12d… |
bx… |
bl… |
сохраняются между вызовами функций |
rsp |
esp |
sp |
spl |
указатель на вершину стека |
r8—r11 |
r8d—r11d |
r8w—r11w |
r8b—r11b |
аргументы и временные значения |
Помимо них есть rip — адрес следующей инструкции (меняют переходы, call и ret, а не mov) и rflags — флаги результата. Для ветвлений важны ZF (результат ноль), SF (знак), CF (перенос, беззнаковое переполнение) и OF (знаковое переполнение).
Неочевидное правило x86-64: запись в 32-битную часть (mov eax, 1) обнуляет старшие 32 бита rax, а запись в 16- или 8-битную часть (mov al, 1) оставляет остальные биты как были. Поэтому NASM вправе закодировать mov rax, 1 как более короткую mov eax, 1 — результат тот же, это видно в дизассемблере ниже.
Основные команды
| Команда | Что делает | Пример |
|---|---|---|
mov |
копирует значение | mov rdi, 1 |
add, sub |
сложение, вычитание (меняют флаги) | add eax, ecx |
inc, dec |
+1, -1 | inc ecx |
xor |
побитовое исключающее ИЛИ; xor edi, edi — быстрый способ обнулить регистр |
xor edi, edi |
cmp |
вычитает без сохранения результата, только выставляет флаги | cmp ecx, 10 |
jmp |
безусловный переход | jmp .loop |
je/jne, jl/jle, jb/jbe |
условный переход по флагам: равно/не равно, меньше со знаком, меньше без знака | jle .loop |
lea |
вычисляет адрес-выражение, но память не читает; удобна для арифметики | lea rax, [rdi + rsi] |
push, pop |
положить в стек, снять со стека | push rbx |
call, ret |
вызвать подпрограмму, вернуться из нее | call add2 |
syscall |
передать управление ядру ОС | syscall |
Пара cmp + условный переход — основа всех if и циклов. Условие выбирается по типу данных: jl/jle сравнивают числа со знаком, jb/jbe — без знака. Для одного и того же набора битов ответ может отличаться.
Первая программа: Hello на NASM под Linux
Программа ниже выводит строку и завершается. Библиотеки языка C она не использует: общается с ядром Linux напрямую через системные вызовы.
; hello.asm - первая программа на NASM, x86-64 Linux
global _start
section .data
msg: db "Hello, Otus!", 10 ; строка и символ перевода строки (10 = \n)
len: equ $ - msg ; длина строки считается при сборке: 13
section .text
_start:
mov rax, 1 ; номер системного вызова write
mov rdi, 1 ; 1-й аргумент: дескриптор stdout
mov rsi, msg ; 2-й аргумент: адрес строки
mov rdx, len ; 3-й аргумент: сколько байт вывести
syscall ; ядро выводит строку, в rax - число записанных байт
mov rax, 60 ; номер системного вызова exit
xor edi, edi ; код возврата 0 (обнуляет весь rdi)
syscall
Сборка в два шага: NASM создает объектный файл, ld делает из него исполняемый.
sudo apt install nasm binutils # Ubuntu/Debian
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello
echo $?
В терминале появится строка Hello, Otus!, а echo $? напечатает 0 — код возврата, который передали в exit.
Разбор по строкам
section .dataиsection .text— секции данных и кода. Это директивы: они управляют сборкой и сами в машинные команды не превращаются, как иdb(определить байты),equ(константа) иglobal.$ - msg— текущий адрес минус адрес начала строки, то есть ее длина. Считает NASM при сборке, а не процессор при запуске._start— точка входа.ldпо умолчанию ищет именно это имя, аglobalделает метку видимой снаружи объектного файла.- Системный вызов в Linux x86-64: номер кладется в
rax, аргументы — вrdi,rsi,rdx,r10,r8,r9, результат возвращается вrax. Командаsyscallпортитrcxиr11.
Вот что получилось в машинном коде (objdump -d -M intel hello):
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov eax,0x1
401005: bf 01 00 00 00 mov edi,0x1
40100a: 48 be 00 20 40 00 00 movabs rsi,0x402000
401011: 00 00 00
401014: ba 0d 00 00 00 mov edx,0xd
401019: 0f 05 syscall
40101b: b8 3c 00 00 00 mov eax,0x3c
401020: 31 ff xor edi,edi
401022: 0f 05 syscall
Видно три вещи. Длина инструкций разная (от 2 до 10 байт) — у x86 это обычно. mov rax, 1 собрана как mov eax, 0x1 по правилу про 32-битную запись. А len превратилась в готовое число 0xd = 13.
Частая путаница из старых учебников: код с int 0x80 и mov eax, 1 для выхода — это 32-битный интерфейс Linux, где номера вызовов другие (exit = 1, write = 4) и аргументы идут в ebx, ecx, edx. В 64-битной программе используйте syscall и 64-битные номера (write = 1, exit = 60). Смешивать интерфейсы не стоит: int 0x80 из 64-битного кода ядро обрабатывает как 32-битный вызов, обрезает адреса до 32 бит (буфер в стеке уже не передать), а в ядре без поддержки 32-битных программ такой вызов не работает вовсе.
Цикл и условный переход: сумма от 1 до 10
; sum.asm - сумма чисел от 1 до 10, результат в коде возврата
global _start
section .text
_start:
xor eax, eax ; eax = 0, здесь копится сумма
mov ecx, 1 ; ecx = 1, счетчик
.loop:
add eax, ecx ; сумма += счетчик
inc ecx ; счетчик += 1
cmp ecx, 10 ; сравнить счетчик с 10 (выставляет флаги)
jle .loop ; если ecx <= 10, повторить
mov edi, eax ; код возврата = сумма
mov eax, 60 ; exit
syscall
После nasm -f elf64 sum.asm -o sum.o && ld sum.o -o sum && ./sum; echo $? терминал покажет 55. Прогон руками: после первого прохода eax = 1, ecx = 2, после второго eax = 3, ecx = 3, и так до ecx = 11, когда jle перестает прыгать назад.
Код возврата здесь — учебный трюк, чтобы не писать перевод числа в строку. У него есть граница: ОС передает родителю только младшие 8 бит, так что суммы больше 255 так уже не увидеть.
Функция: call, ret и соглашение о вызовах
; add.asm - функция add2(a, b) по соглашению System V AMD64 и ее вызов
global _start
section .text
add2: ; a приходит в rdi, b - в rsi
lea rax, [rdi + rsi] ; rax = a + b, результат функции - в rax
ret ; вернуться по адресу, который положил call
_start:
mov rdi, 40 ; первый аргумент
mov rsi, 2 ; второй аргумент
call add2 ; кладет адрес возврата в стек и прыгает в add2
mov rdi, rax ; код возврата = результат (42)
mov rax, 60 ; exit
syscall
./add; echo $? печатает 42. Регистры аргументов выбраны не случайно: так их передает соглашение System V AMD64 ABI, принятое в Linux и macOS (rdi, rsi, rdx, rcx, r8, r9, результат в rax). Поэтому такую функцию можно вызвать и из программы на C. В Windows x64 соглашение другое: первые аргументы идут в rcx, rdx, r8, r9.
Если не получилось
| Симптом | Причина | Что сделать |
|---|---|---|
error: instruction not supported in 32-bit mode |
собрали с -f elf32 |
собирать с -f elf64 |
error: parser: instruction expected |
опечатка в мнемонике (mvo вместо mov) |
проверить строку, номер которой указан в ошибке |
ld: warning: cannot find entry symbol _start |
метка названа иначе или нет global _start |
назвать точку входа _start и объявить ее global; то, что программа при этом иногда запускается, — случайность |
строка вывелась, затем Segmentation fault, код 139 |
нет вызова exit: процессор пошел исполнять байты после кода |
завершать программу системным вызовом 60 |
| программа не запускается на Mac с Apple Silicon или Raspberry Pi | это ARM, а код собран под x86-64 (на macOS к тому же не Linux: другой формат файлов и системные вызовы) | Linux на x86-64: отдельный компьютер, виртуальная машина или WSL2 на ПК с Windows; на ARM — x86-64 Linux под эмуляцией (Rosetta в Linux-ВМ на Mac, QEMU, docker с --platform linux/amd64) |
Код 139 — это 128 + 11, где 11 — номер сигнала SIGSEGV. Точный текст сообщения печатает оболочка, и он может немного отличаться.
Зачем ассемблер в 2026 году
Целые приложения на ассемблере сейчас пишут редко: компиляторы C, C++ и Rust обычно генерируют код не хуже ручного. Зато читать ассемблер нужно в нескольких профессиях:
- реверс-инжиниринг и анализ вредоносного ПО — исходников нет, есть только дизассемблированный бинарник;
- поиск уязвимостей и отладка падений — в отладчике видны инструкции и регистры, а не строки исходника;
- оптимизация горячих участков — проверить, что сгенерировал компилятор (например, векторизовал ли цикл);
- системное и встраиваемое программирование — загрузчики, код старта ОС, работа с железом на микроконтроллерах.
Для новичка разумный путь такой: уверенно освоить C, затем собрать программы из этой статьи, дизассемблировать свои программы на C через objdump -d -M intel и сравнить с исходником.
Выводы
- Язык ассемблера — текстовая запись машинных команд конкретной архитектуры; ассемблер (NASM, GNU as) переводит ее в машинный код,
ldсобирает исполняемый файл. - Код под x86-64 не запустится на ARM напрямую (только под эмуляцией), а у x86 есть два синтаксиса — Intel и AT&T — с противоположным порядком операндов.
- У x86-64 16 регистров общего назначения; запись в 32-битную часть обнуляет старшие биты, в 8- и 16-битную — нет.
- Минимальная программа под Linux — это системные вызовы
write(1) иexit(60) черезsyscall; безexitбудет Segmentation fault. - Ветвления строятся из
cmpи условного перехода, функции — изcall/retи соглашения о вызовах (в Linux — System V AMD64).
Где применяется / связь с практикой
Освойте тему на практике
Чаще всего ассемблер читают, а не пишут: в дизассемблере при анализе чужих бинарников, в отладчике при разборе падения, при поиске уязвимостей. Если хочется перейти от учебных программ к разбору реальных исполняемых файлов, это направление системно разбирается на курсе «Обратная разработка». Бесплатно познакомиться с преподавателями и темами можно на открытых уроках Otus.
FAQ
Ассемблер быстрее C?
Не автоматически. Современный компилятор с оптимизацией обычно дает сопоставимый или лучший код; ручной ассемблер выигрывает в узких местах, и выигрыш надо подтверждать замером.
Можно ли запустить эти примеры в Windows?
Код использует системные вызовы Linux, поэтому в Windows он работает внутри WSL2 или виртуальной машины. Для «родной» Windows-программы нужны другие вызовы (функции WinAPI) и соглашение Windows x64.
Чем NASM отличается от MASM и GNU as?
Это разные трансляторы: NASM и MASM используют синтаксис Intel (MASM — инструмент Microsoft для Windows), GNU as входит в binutils и по умолчанию понимает AT&T. Директивы у всех трех отличаются, поэтому исходник между ними переносится не напрямую.



