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

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

Язык ассемблера — это текстовая запись машинных команд конкретной архитектуры процессора: одна строка вида mov eax, 60 обычно соответствует одной инструкции, которую процессор выполняет напрямую. Ассемблер в строгом смысле — программа-транслятор (NASM, GNU as, MASM), которая превращает этот текст в машинный код; в разговоре так часто называют и сам язык.

Ниже разберем, как язык связан с машинным кодом, какие регистры есть у 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. Директивы у всех трех отличаются, поэтому исходник между ними переносится не напрямую.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)