Перейти к основному содержимому

Справочник по ассемблеру


Назначение

Справочник-шпаргалка по ассемблеру — типы, синтаксис, стандартная библиотека, типовые паттерны. Не заменяет пошаговое обучение. Учебный курс: раздел.

Как использовать эту статью эффективно:

  1. Сначала проходите основной маршрут раздела.
  2. В справочник приходите за точным синтаксисом и быстрым напоминанием.
  3. Если встретился новый термин, возвращайтесь по внутренней ссылке в профильную статью.

Так справочник работает как "оперативная память", а не как перегруженный учебник с нуля.


Краткое пояснение

Компоненты и ключевые особенности языка.


Быстрый старт

nasm -f elf64 func.asm -o func.o
gcc main.c func.o -o program

Навигация по уровню подготовки


Справочные таблицы

Содержание справочника


Архитектурные основы и контекст

Что такое язык ассемблера

Язык ассемблера — низкоуровневый язык для прямого управления CPU. Мнемоника обычно соответствует одной машинной команде; макросы и директивы обрабатывает программа-ассемблер (NASM, GAS). Справочник ориентирован на x86-64, NASM, Linux — для Windows и 32-бит смотрите подписи в курсе. Углублённые темы курса: строковые инструкции, SIMD и float, Windows WinAPI.


Целевые архитектуры

Существует множество архитектур процессоров, каждая из которых имеет собственный набор инструкций (ISA — Instruction Set Architecture). Теория ISA, CISC/RISC и сравнение синтаксиса Intel/AT&T — в отдельной статье. Наиболее распространённые архитектуры, для которых пишут на ассемблере:

  • x86 — 32-битная архитектура, разработанная Intel, используется в персональных компьютерах.
  • x86-64 (AMD64 / Intel 64) — 64-битное расширение x86, доминирующая архитектура настольных и серверных систем.
  • ARM — энергоэффективная архитектура, применяемая в мобильных устройствах, встраиваемых системах и современных ноутбуках.
  • MIPS — учебная и встраиваемая архитектура, часто используется в образовательных целях.
  • RISC-V — открытая архитектура с модульным набором инструкций, набирающая популярность в академической и промышленной среде.

Данный справочник ориентирован преимущественно на x86-64, как наиболее актуальную архитектуру для обучения и практического применения на стандартных ПК.


Регистры процессора

Регистры — это сверхбыстрые ячейки памяти внутри процессора, используемые для хранения операндов, адресов и промежуточных результатов вычислений.


Основные регистры общего назначения (в x86-64)
РегистрРазмерНазначение
RAX64 битАккумулятор, используется в арифметических операциях и возврате значений функций
RBX64 битБазовый регистр, часто используется для хранения базовых адресов
RCX64 битСчётчик, применяется в циклах и сдвигах
RDX64 битРасширенный регистр данных, используется в операциях умножения и деления
RSI64 битИсточник для строковых операций
RDI64 битПриёмник для строковых операций
RBP64 битУказатель базы стека (base pointer)
RSP64 битУказатель вершины стека (stack pointer)

Каждый из этих 64-битных регистров имеет младшие части:

  • 32-битные — EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP
  • 16-битные — AX, BX, CX, DX, SI, DI, BP, SP
  • 8-битные — AL, AH, BL, BH, CL, CH, DL, DH и т.д.

Дополнительные регистры (R8–R15)

Архитектура x86-64 добавляет восемь новых 64-битных регистров общего назначения:

  • R8, R9, R10, R11, R12, R13, R14, R15

Их младшие части:

  • 32-битные — R8D, R9D, ..., R15D
  • 16-битные — R8W, R9W, ..., R15W
  • 8-битные — R8B, R9B, ..., R15B

Сегментные регистры
  • CS — код
  • DS — данные
  • SS — стек
  • ES, FS, GS — дополнительные сегменты

В 64-битном режиме сегментация практически отключена, но FS и GS используются для доступа к данным потока или системной информации.


Регистр флагов (RFLAGS)

Регистр флагов содержит битовые индикаторы состояния процессора после выполнения операций:

  • CF (Carry Flag) — флаг переноса, устанавливается при переполнении беззнаковой арифметики
  • ZF (Zero Flag) — устанавливается, если результат операции равен нулю
  • SF (Sign Flag) — устанавливается, если результат отрицателен (старший бит = 1)
  • OF (Overflow Flag) — флаг переполнения знаковой арифметики
  • PF (Parity Flag) — чётность младшего байта результата
  • AF (Auxiliary Carry Flag) — перенос между битами 3 и 4 (используется в BCD-арифметике)

Эти флаги используются условными переходами (JZ, JNZ, JS, JO и др.).


Модели памяти и адресация

Память в x86-64 адресуется линейно, с использованием 64-битных виртуальных адресов. Реальный физический адрес формируется через многоуровневую таблицу страниц, но программист работает с виртуальными адресами.


Способы адресации операндов
  1. Непосредственная (immediate)
    Значение указано прямо в инструкции:
mov eax, 42
  1. Регистровая (register)
    Операнд находится в регистре:
mov ebx, eax
  1. Прямая (direct memory)
    Операнд по фиксированному адресу:
mov eax, [0x1000]
  1. Косвенная (indirect)
    Адрес хранится в регистре:
mov eax, [rbx]
  1. Базовая + смещение (base + displacement)
mov eax, [rbx + 8]
  1. Масштабируемая индексная (scaled index)
mov eax, [rbx + rsi*4]
  1. База + индекс + смещение
mov eax, [rbx + rsi*4 + 16]

Все эти формы допустимы в x86-64 и позволяют гибко работать с массивами, структурами и стеком.


Стек

Стек — это область памяти, управляемая по принципу LIFO (Last In, First Out). Указатель стека (RSP) всегда указывает на вершину стека.

Основные операции:

  • push reg/mem/imm — помещает значение в стек, уменьшая RSP
  • pop reg — извлекает значение из стека, увеличивая RSP

Стек используется для:

  • передачи аргументов функций
  • хранения возвращаемых адресов при вызове подпрограмм
  • временного хранения регистров

Вызов функций и соглашения о вызовах

В x86-64 на большинстве Unix-подобных систем (Linux, macOS) используется System V AMD64 ABI. В Windows — Microsoft x64 calling convention.


System V AMD64 ABI (Linux/macOS)
  • Первые 6 целочисленных аргументов передаются в регистрах:
    RDI, RSI, RDX, RCX, R8, R9
  • Первые 8 вещественных аргументов — в XMM0XMM7
  • Дополнительные аргументы — через стек
  • Возвращаемое значение — в RAX (или RDX:RAX для 128-битных значений)
  • Регистры RBX, RBP, R12R15 — callee-saved (вызывающая функция ожидает, что они сохранятся)
  • Регистры RAX, RCX, RDX, R8R11 — caller-saved

Пример вызова функции
mov rdi, 10 ; первый аргумент
mov rsi, 20 ; второй аргумент
call add_numbers ; вызов функции
; результат в RAX

Секции программы

Программа на ассемблере состоит из секций (sections), каждая из которых имеет своё назначение:

  • .text — исполняемый код (только чтение, исполняемый)
  • .data — инициализированные данные (чтение/запись)
  • .bss — неинициализированные данные (резервирование памяти)
  • .rodata — константные данные (только чтение)

Пример:

section .data
msg db 'Hello, world!', 0xA
msg_len equ $ - msg

section .bss
buffer resb 256

section .text
global _start
_start:
; код программы

Директивы ассемблера

Директивы — это команды для ассемблера, а не для процессора. Они управляют сборкой программы.

Часто используемые директивы (в NASM):

  • db, dw, dd, dq — определение байтов, слов, двойных слов, четверных слов
  • equ — определение константы
  • resb, resw, resd, resq — резервирование памяти
  • global — экспортирование метки как точки входа
  • extern — объявление внешней метки (например, из libc)
  • times — повторение данных заданное число раз

Системные вызовы (Linux)

В Linux взаимодействие с ядром происходит через системные вызовы. В x86-64 используется инструкция syscall.

Номер системного вызова помещается в RAX, аргументы — в RDI, RSI, RDX, R10, R8, R9.

Пример: вывод строки

mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg ; адрес строки
mov rdx, msg_len ; длина
syscall

Завершение программы:

mov rax, 60 ; sys_exit
mov rdi, 0 ; код возврата
syscall

Инструкции процессора

Инструкции — это элементарные команды, которые понимает и исполняет процессор. В архитектуре x86-64 существует несколько сотен инструкций, но повседневное программирование использует ограниченный набор. Все инструкции делятся на логические группы — передача данных, арифметика, логические операции, управление потоком выполнения, работа со стеком, системные вызовы и специальные операции.


Передача данных

Эти инструкции перемещают данные между регистрами, памятью и непосредственными значениями.


MOV — копирование данных

Синтаксис:

mov приёмник, источник

Примеры:

mov eax, 100 ; загрузка константы
mov ebx, eax ; копирование регистра
mov [var], ecx ; запись в память
mov edx, [buffer] ; чтение из памяти

Ограничения:

  • Нельзя напрямую копировать из памяти в память: mov [dst], [src] — недопустимо.
  • Нельзя загружать непосредственное значение в сегментный регистр (например, mov ds, 0x10 — запрещено в защищённом режиме).

LEA — загрузка эффективного адреса

LEA вычисляет адрес, но не обращается к памяти. Это мощный инструмент для арифметики указателей.

lea rax, [rbx + rcx*4 + 16]

Эта команда помещает в RAX значение RBX + RCX*4 + 16, не читая содержимое по этому адресу. Часто используется для быстрого умножения и сложения.


XCHG — обмен значениями
xchg eax, ebx ; обмен содержимым EAX и EBX

Атомарная операция, полезна в многопоточной среде.


Арифметические операции

Все арифметические инструкции обновляют флаги в RFLAGS.


ADD — сложение
add eax, ebx ; EAX = EAX + EBX
add [counter], 1

SUB — вычитание
sub eax, 5 ; EAX = EAX - 5

INC / DEC — инкремент и декремент
inc eax ; EAX = EAX + 1
dec ebx ; EBX = EBX - 1

Эти инструкции короче по размеру, но не обновляют флаг переноса (CF). В современном коде часто предпочитают add reg, 1 ради единообразия.


MUL / IMUL — умножение
  • MUL — беззнаковое умножение
  • IMUL — знаковое умножение

Однооперандная форма:

mul rbx ; RDX:RAX = RAX * RBX

Двухоперандная форма (результат в первом операнде):

imul eax, ebx ; EAX = EAX * EBX
imul eax, ebx, 5 ; EAX = EBX * 5

DIV / IDIV — деление
  • DIV — беззнаковое
  • IDIV — знаковое

Деление использует пару регистров RDX:RAX как делимое:

mov rax, 100
xor rdx, rdx ; обнулить RDX (остаток)
mov rbx, 7
div rbx ; RAX = 100 / 7, RDX = 100 % 7

Логические и побитовые операции

AND, OR, XOR, NOT
and eax, 0xFF ; маскирование младшего байта
or ebx, 1 ; установка младшего бита
xor ecx, ecx ; обнуление регистра (быстрее, чем mov ecx, 0)
not edx ; побитовая инверсия

XOR с самим собой — идиома обнуления регистра.


TEST — проверка битов без изменения значения
test eax, eax ; устанавливает ZF, если EAX == 0
jz zero_label

Часто используется вместо cmp eax, 0.


CMP — сравнение
cmp eax, ebx ; вычитает EBX из EAX, обновляет флаги, но не сохраняет результат
je equal_label

Управление потоком выполнения

Безусловный переход — JMP
jmp start_loop

Может быть:

  • прямымjmp label
  • косвеннымjmp rax (переход по адресу в регистре)

Условные переходы

Условные переходы проверяют флаги и выполняют переход, если условие истинно.

ИнструкцияУсловиеОписание
JE / JZZF = 1равно / ноль
JNE / JNZZF = 0не равно / не ноль
JL / JNGESF ≠ OFменьше (знаковое)
JLE / JNGZF = 1 или SF ≠ OFменьше или равно
JG / JNLEZF = 0 и SF = OF (знаковое "больше")больше
JGE / JNLSF = OFбольше или равно
JB / JNAECF = 1ниже (беззнаковое)
JBE / JNACF = 1 или ZF = 1ниже или равно
JA / JNBECF = 0 и ZF = 0выше
JAE / JNBCF = 0выше или равно
JSSF = 1отрицательный
JNSSF = 0неотрицательный
JOOF = 1переполнение
JNOOF = 0нет переполнения
JP / JPEPF = 1чётный паритет
JNP / JPOPF = 0нечётный паритет

Циклы

Инструкция LOOP (устаревшая, но иногда используется):

mov rcx, 10
loop_start:
; тело цикла
loop loop_start ; уменьшает RCX, переходит, если RCX ≠ 0

Современный подход — использовать dec + jnz:

mov rcx, 10
loop_start:
; тело
dec rcx
jnz loop_start

Вызов подпрограмм

CALL — вызов функции
call my_function

Выполняет два действия:

  1. Помещает адрес следующей инструкции в стек (push rip)
  2. Загружает в RIP адрес my_function

RET — возврат из функции
ret

Извлекает адрес из стека и присваивает его RIP.


Работа со стеком

PUSH — положить в стек
push rax
push 42
push qword [var]

Уменьшает RSP на 8 (в 64-битном режиме) и записывает значение.


POP — извлечь из стека
pop rbx

Читает значение из стека и увеличивает RSP.


Специальные инструкции

NOP — пустая операция

Занимает один такт, ничего не делает. Используется для выравнивания, отладки, задержек.


HLT — остановка процессора

Останавливает CPU до следующего прерывания. Используется в ядрах ОС.


INT — программное прерывание

В 64-битном режиме почти не используется. Раньше применялось для системных вызовов (int 0x80 в 32-битном Linux).


SYSCALL — современный системный вызов

Как описано ранее, основной способ вызова ядра в x86-64.


Префиксы инструкций

Некоторые инструкции могут иметь префиксы, изменяющие их поведение:

  • LOCK — обеспечивает атомарность (lock inc [var])
  • REP — повторяет строковую операцию (rep movsb)
  • REPE / REPZ — повторяет, пока ZF = 1
  • REPNE / REPNZ — повторяет, пока ZF = 0

Пример копирования блока памяти:

mov rsi, src
mov rdi, dst
mov rcx, count
cld ; направление вперёд
rep movsb ; копировать RCX байт из [RSI] в [RDI]

Работа с памятью и данными

Эффективная работа с памятью — ключевая компетенция программиста на ассемблере. В отличие от высокоуровневых языков, где управление памятью скрыто за абстракциями, ассемблер предоставляет прямой доступ к байтам, словам и структурам. Эта часть описывает, как организовывать и манипулировать данными — массивы, строки, структуры, выравнивание и динамическое распределение.


Организация данных в памяти

Данные в ассемблере определяются в секциях .data (инициализированные) и .bss (неинициализированные). Каждое определение создаёт символ, который можно использовать как адрес.


Базовые директивы определения данных
ДирективаРазмерОписание
db1 байтDefine Byte
dw2 байтаDefine Word
dd4 байтаDefine Doubleword
dq8 байтDefine Quadword
dt10 байтDefine Ten-byte (для расширенных вещественных чисел, редко используется)

Примеры:

section .data
byte_val db 42
word_val dw 1000
dword_val dd 0x12345678
qword_val dq 9223372036854775807
pi dd 3.141592 ; вещественное число (IEEE 754)

Массивы

Массив — это последовательность элементов одного типа, расположенных подряд в памяти.

Однобайтовый массив:

digits db 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Массив слов:

scores dw 100, 200, 300, 400

Массив из 100 нулей:

buffer times 100 db 0

В NASM для повторения используют times N db значение (в MASM/TASM аналог — dup).


Строки

Строка в ассемблере — это массив байтов, завершённый нулём (C-стиль) или имеющий известную длину.

Примеры строк:

msg1 db 'Hello', 0 ; C-строка (с нулевым терминатором)
msg2 db 'World!', 10 ; строка с символом новой строки
msg3 db "Quoted text", 0 ; допустимы двойные кавычки

Длина строки часто вычисляется с помощью метки:

hello db 'Hello, world!', 0xA, 0
hello_len equ $ - hello ; $ — текущая позиция, вычитаем начало

Значение hello_len будет равно 14 (13 символов + \n).


Доступ к элементам массива

Операции (массив в .data — фиксированный размер; индекс с нуля):

ДействиеСинтаксис (x86, NASM)
Прочитать байтmov al, [base + index]
Записать байтmov [base + index], al
Слово (2 байта)[base + index*2]
Двойное слово (4)[base + index*4]
Объявить N элементовbuffer times N db 0

"Добавить в конец" в рантайме — отдельный буфер + счётчик длины и смещение index * размер; стандартной коллекции в языке нет.

Доступ к элементу массива осуществляется через адресацию с масштабированием.


Однобайтовый массив
mov al, [digits + 3] ; AL = 3

Массив слов (2 байта)
mov ax, [scores + 2*1] ; второй элемент (индекс 1): 200

Массив двойных слов (4 байта)
mov eax, [array_dd + 4*2] ; третий элемент

Общая формула:
адрес = база + индекс * размер_элемента

В x86-64 можно использовать гибкую адресацию:

mov eax, [rbx + rsi*4] ; RBX — база, RSI — индекс, 4 — размер элемента

Структуры данных

Ассемблер не имеет встроенного типа "структура", но можно моделировать её через смещения.

Пример: структура точки на плоскости

struc Point
.x resd 1 ; 4 байта для x
.y resd 1 ; 4 байта для y
endstruc

Использование:

section .data
origin Point 0, 0
p1 Point 10, 20

section .text
mov eax, [p1 + Point.x] ; EAX = 10
mov ebx, [p1 + Point.y] ; EBX = 20

Если ассемблер не поддерживает struc (например, в GAS), смещения определяются вручную:

POINT_X equ 0
POINT_Y equ 4

mov eax, [p1 + POINT_X]

Выравнивание данных

Процессор эффективнее читает данные, выровненные по границам, кратным их размеру:

  • 2-байтовые — по чётным адресам
  • 4-байтовые — по адресам, кратным 4
  • 8-байтовые — по адресам, кратным 8

NASM предоставляет директиву align:

section .data
align 8
counter dq 0

Выравнивание особенно важно для SIMD-инструкций (SSE, AVX), требующих 16- или 32-байтное выравнивание.


Работа со строками

Подробный разбор REP, направления CLD/STD и таблиц поиска — в строковых инструкциях. Краткая таблица мнемоник:

Строковые операции ускоряются специальными инструкциями:

ИнструкцияДействие
MOVSBкопирует байт из [RSI] в [RDI], увеличивает/уменьшает RSI/RDI
MOVSWкопирует слово
MOVSDкопирует двойное слово
MOVSQкопирует четверное слово (8 байт)
STOSBзаписывает AL в [RDI]
LODSBзагружает байт из [RSI] в AL
CMPSBсравнивает байты в [RSI] и [RDI]
SCASBсравнивает AL с [RDI]

Направление изменения указателей задаётся флагом DF:

  • CLD — очистка DF, направление вперёд (RSI++, RDI++)
  • STD — установка DF, направление назад (RSI--, RDI--)

Пример: поиск символа в строке

mov rdi, msg
mov al, '!'
mov rcx, msg_len
cld
repne scasb ; ищет '!' в строке
jnz not_found ; если ZF=0 — не найдено
; RDI указывает на символ после '!'

Динамическое выделение памяти

В пользовательском режиме под Linux динамическая память запрашивается через системный вызов mmap или через библиотечную функцию malloc.

Пример с malloc (через libc):

Код ITЗагрузка примера кода…

Чистый системный вызов mmap:

; mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
mov rax, 9 ; sys_mmap
mov rdi, 0 ; addr
mov rsi, 4096 ; length
mov rdx, 3 ; prot: read|write
mov r10, 0x22 ; flags: MAP_PRIVATE|MAP_ANONYMOUS
mov r8, -1 ; fd
mov r9, 0 ; offset
syscall
; RAX = адрес или -1 при ошибке

Управление памятью в стеке

Локальные переменные часто размещаются в стеке:

sub rsp, 16 ; выделить 16 байт под локальные данные
mov [rsp], rax ; сохранить RAX
mov [rsp + 8], rbx ; сохранить RBX

; ... работа ...

add rsp, 16 ; освободить

Это безопасно, так как стек автоматически "растёт" вниз, и ОС гарантирует наличие страницы-сторожа.


Константы и псевдонимы

Константы определяются через equ:

MAX_SIZE equ 1000
FLAG_ON equ 1
FLAG_OFF equ 0

Они заменяются на этапе сборки и не занимают места в памяти.


Функции, рекурсия и организация стекового фрейма

Функции (подпрограммы) — основной способ структурирования кода даже в ассемблере. Они позволяют повторно использовать логику, изолировать задачи и управлять сложностью программы. В отличие от высокоуровневых языков, где вызов функции выглядит как простая конструкция, в ассемблере программист полностью отвечает за передачу аргументов, сохранение контекста, управление стеком и возврат результата.


Основы вызова функции

Вызов функции в x86-64 выполняется с помощью инструкции call, которая автоматически помещает адрес возврата в стек и передаёт управление на метку функции. Завершение функции происходит через ret, которая извлекает адрес возврата из стека и продолжает выполнение.

Пример простой функции:

Код ITЗагрузка примера кода…

Эта функция вычисляет факториал числа, переданного в RDI, и возвращает результат в RAX.


Стековый фрейм (stack frame)

Стековый фрейм — это область стека, выделенная под одну активацию функции. Он содержит:

  • адрес возврата
  • сохранённые регистры
  • локальные переменные
  • аргументы (если передаются через стек)

В x86-64 принято использовать RBP как указатель базы фрейма (frame pointer), хотя современные компиляторы часто его опускают для оптимизации.


Типичный пролог и эпилог функции

Пролог (начало функции):

push rbp ; сохраняем старый RBP
mov rbp, rsp ; устанавливаем новый RBP = текущий RSP
sub rsp, 32 ; выделяем 32 байта под локальные переменные

Эпилог (конец функции):

mov rsp, rbp ; восстанавливаем RSP
pop rbp ; восстанавливаем старый RBP
ret

После установки RBP, смещения относительно него имеют фиксированный смысл:

  • [rbp + 8] — адрес возврата
  • [rbp + 16] — первый аргумент, переданный через стек
  • [rbp] — предыдущий RBP
  • [rbp - 8], [rbp - 16] — локальные переменные

Пример с фреймом:

Код ITЗагрузка примера кода…


Передача аргументов

Согласно System V AMD64 ABI, первые шесть целочисленных аргументов передаются в регистрах:

  • RDI, RSI, RDX, RCX, R8, R9

Если аргументов больше шести, остальные размещаются в стеке справа налево (последний аргумент — ближе к вершине стека).

Пример функции с семью аргументами:

; C-прототип: int func(a, b, c, d, e, f, g)
; a → RDI, b → RSI, ..., f → R9, g → [RSP + 8]
func:
; g доступен как [rbp + 16], если используется RBP
mov rax, [rsp + 8] ; или напрямую через RSP
add rax, rdi
ret

Возвращаемые значения

  • Целые числа и указатели возвращаются в RAX
  • 128-битные значения — в RDX:RAX
  • Вещественные числа — в XMM0

Сохранение регистров (callee-saved и caller-saved)

ABI определяет, какие регистры обязан сохранять вызываемая функция (callee-saved), а какие — вызывающая (caller-saved).

Callee-saved (функция обязана сохранить и восстановить):

  • RBX, RBP, R12R15

Caller-saved (вызывающая сторона не ожидает их сохранения):

  • RAX, RCX, RDX, RSI, RDI, R8R11

Если функция использует RBX, она должна сохранить его в начале и восстановить перед ret:

my_func:
push rbx
; ... используем RBX ...
pop rbx
ret

Рекурсия

Рекурсия в ассемблере работает так же, как и в других языках: функция вызывает саму себя. Каждый вызов создаёт новый стековый фрейм, что позволяет хранить независимые копии аргументов и локальных переменных.

Условие завершения (базовый случай) обязательно, иначе стек переполнится (stack overflow).

Пример: вычисление суммы чисел от 1 до n

sum_to_n:
cmp rdi, 0
je .base
push rdi
dec rdi
call sum_to_n
pop rbx
add rax, rbx
ret
.base:
xor rax, rax
ret

Хвостовая рекурсия и оптимизация

Хвостовая рекурсия — это вызов функции в самом конце, без дополнительных операций после возврата. Такой вызов можно заменить на цикл, избегая роста стека.

Пример хвостовой рекурсии:

; sum_tail(n, acc)
sum_tail:
cmp rdi, 0
je .done
add rsi, rdi ; acc += n
dec rdi ; n--
jmp sum_tail ; хвостовой вызов → заменён на JMP
.done:
mov rax, rsi
ret

Здесь jmp вместо call предотвращает накопление фреймов.


Локальные массивы и структуры

Большие локальные данные также размещаются в стеке:

process_data:
push rbp
mov rbp, rsp
sub rsp, 256 ; 256 байт под буфер

lea rax, [rbp - 256] ; RAX = адрес начала буфера
mov [rax], byte 0

; ... обработка ...

mov rsp, rbp
pop rbp
ret

Важно: стек ограничен (обычно 8 МБ в Linux), поэтому очень большие массивы следует выделять динамически.


Обработка ошибок и возврат статусов

Функции могут возвращать коды ошибок через RAX. Например, -1 означает ошибку, 0 — успех.

safe_divide:
test rsi, rsi ; делитель = 0?
jz .error
mov rax, rdi
xor rdx, rdx
div rsi
ret
.error:
mov rax, -1
ret

Взаимодействие с C

Ассемблерные функции можно вызывать из C, если соблюдать ABI.

Пример объявления в C:

extern long my_asm_func(long a, long b);

Ассемблерная реализация:

global my_asm_func
my_asm_func:
add rdi, rsi
mov rax, rdi
ret

Сборка:

nasm -f elf64 func.asm -o func.o
gcc main.c func.o -o program

Взаимодействие с операционной системой

Чтобы читать файлы, выводить текст, получать ввод от пользователя или завершать выполнение, программа на ассемблере взаимодействует с операционной системой. В Unix-подобных системах (Linux, macOS) это происходит через системные вызовы — специальные точки входа в ядро. В Windows используется другая модель (WinAPI), но в рамках данного справочника фокус сделан на Linux x86-64, как наиболее доступной и документированной среде для обучения.


Механизм системных вызовов в x86-64

В 64-битном режиме Linux системные вызовы выполняются через инструкцию syscall. Процессор переключается в привилегированный режим, управление передаётся ядру, которое выполняет запрошенную операцию и возвращает результат.


Регистры для системных вызовов (System V ABI)
РегистрНазначение
RAXномер системного вызова
RDI1-й аргумент
RSI2-й аргумент
RDX3-й аргумент
R104-й аргумент
R85-й аргумент
R96-й аргумент

Важно — четвёртый аргумент передаётся в R10, а не в RCX, потому что syscall разрушает значение RCX.

Результат возвращается в RAX:

  • неотрицательное значение — успешный результат
  • отрицательное значение — код ошибки (-errno)

Основные системные вызовы

1. sys_write — запись данных

Номер: 1
Сигнатура — ssize_t write(int fd, const void *buf, size_t count)

Пример: вывод строки в стандартный вывод

section .data
msg db 'Hello from assembly!', 0xA
msg_len equ $ - msg

section .text
global _start

_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg ; адрес буфера
mov rdx, msg_len ; количество байт
syscall

2. sys_read — чтение данных

Номер: 0
Сигнатура — ssize_t read(int fd, void *buf, size_t count)

Пример: чтение одной строки с клавиатуры

section .bss
input resb 256

section .text
; ...
mov rax, 0 ; sys_read
mov rdi, 0 ; stdin
mov rsi, input ; буфер
mov rdx, 256 ; максимум байт
syscall
; RAX = количество прочитанных байт

3. sys_exit — завершение программы

Номер: 60
Сигнатура: void _exit(int status)

mov rax, 60
mov rdi, 0 ; код возврата
syscall

4. sys_open — открытие файла

Номер: 2
Сигнатура — int open(const char *pathname, int flags, mode_t mode)

Флаги (часто используемые):

  • O_RDONLY = 0
  • O_WRONLY = 1
  • O_RDWR = 2
  • O_CREAT = 0x40
  • O_TRUNC = 0x200

Режим (если создаётся файл): 0o644 (восьмеричное) → 0x1A4

Пример: открытие файла для чтения

section .data
filename db 'data.txt', 0

section .text
mov rax, 2 ; sys_open
mov rdi, filename ; путь
mov rsi, 0 ; O_RDONLY
mov rdx, 0 ; режим не нужен при чтении
syscall
; RAX = дескриптор файла или -1 при ошибке

5. sys_close — закрытие файла

Номер: 3
Сигнатура: int close(int fd)

mov rax, 3
mov rdi, r12 ; предположим, дескриптор в R12
syscall

6. sys_lseek — перемещение указателя в файле

Номер: 8
Сигнатура — off_t lseek(int fd, off_t offset, int whence)

whence:

  • SEEK_SET = 0 (от начала)
  • SEEK_CUR = 1 (от текущей позиции)
  • SEEK_END = 2 (от конца)

Пример: перейти в конец файла

mov rax, 8
mov rdi, fd
mov rsi, 0
mov rdx, 2 ; SEEK_END
syscall

7. sys_mmap — отображение файла в память

Номер: 9
Позволяет отобразить файл или выделить анонимную память.

Пример: выделение 4 КБ анонимной памяти

mov rax, 9
mov rdi, 0 ; addr (NULL)
mov rsi, 4096 ; length
mov rdx, 3 ; PROT_READ | PROT_WRITE
mov r10, 0x22 ; MAP_PRIVATE | MAP_ANONYMOUS
mov r8, -1 ; fd = -1
mov r9, 0 ; offset
syscall
; RAX = адрес или -1

Обработка ошибок

После системного вызова проверяйте знак результата:

syscall
cmp rax, 0
jl .error_handler

Код ошибки можно получить как -rax и использовать для диагностики (например, -2 = ENOENT — файл не найден).


Работа с несколькими файлами

Пример: копирование содержимого одного файла в другой

Код ITЗагрузка примера кода…


Сигналы

Ассемблер позволяет устанавливать обработчики сигналов через sys_rt_sigaction (номер 13), но это сложная тема. В простых программах сигналы обычно игнорируются или обрабатываются по умолчанию (например, SIGINT завершает программу).


Процессы и потоки

  • sys_fork (57) — создаёт новый процесс
  • sys_execve (59) — заменяет текущий образ памяти новой программой
  • sys_clone (56) — создаёт поток (низкоуровневый аналог pthread_create)

Пример запуска другой программы:

section .data
prog db '/bin/ls', 0
argv dq prog, 0
envp dq 0

section .text
mov rax, 59 ; sys_execve
mov rdi, prog
mov rsi, argv
mov rdx, envp
syscall
; Если execve вернулся — произошла ошибка

Время и задержки

  • sys_nanosleep (35) — приостанавливает выполнение
section .data
ts dq 1, 500000000 ; 1 секунда + 500 млн наносекунд = 1.5 сек

section .text
mov rax, 35
mov rdi, ts
mov rsi, 0
syscall

Использование libc вместо прямых системных вызовов

Для сложных задач (например, форматированный вывод) удобнее вызывать функции из стандартной библиотеки C (printf, fopen, malloc и т.д.).

Пример с printf:

Код ITЗагрузка примера кода…

Сборка:

nasm -f elf64 program.asm -o program.o
gcc program.o -o program

Преимущества libc:

  • удобство (printf, scanf)
  • портируемость
  • обработка ошибок
  • работа с локалью

Недостатки:

  • зависимость от библиотеки
  • больший размер исполняемого файла
  • менее прямой контроль

Отладка, анализ и инструменты

Написание программ на ассемблере требует тесного взаимодействия с инструментами сборки, анализа и отладки. В отличие от высокоуровневых языков, где ошибки часто ловятся на этапе компиляции или через исключения, в ассемблере даже небольшая ошибка в адресации или управлении стеком может привести к молчаливому повреждению памяти или аварийному завершению. Поэтому знание инструментов — необходимость.


Основной инструментарий для x86-64 (Linux)

ИнструментНазначение
NASMАссемблер (Netwide Ассемблер) — преобразует .asm в объектные файлы
GASАссемблер из GNU Binutils, использует AT&T-синтаксис
LDКомпоновщик (linker) — объединяет объектные файлы в исполняемый
GCCМожет выступать как компоновщик, автоматически подключая libc
GDBОтладчик — позволяет шагать по инструкциям, проверять регистры и память
objdumpДизассемблер — показывает машинный код и символы
straceТрассировщик системных вызовов
ltraceТрассировщик вызовов библиотечных функций
readelfАнализ ELF-файлов (заголовки, секции, символы)

Сборка программы без libc (чистый системный вызов)

Файл hello.asm:

Код ITЗагрузка примера кода…

Сборка:

nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello

Получаем минимальный исполняемый файл (~300–500 байт).


Сборка с использованием libc

Файл hello_libc.asm:

Код ITЗагрузка примера кода…

Сборка:

nasm -f elf64 hello_libc.asm -o hello_libc.o
gcc hello_libc.o -o hello_libc

Здесь gcc автоматически вызывает ld, подключает libc, crt0.o и другие необходимые компоненты.


Отладка с помощью GDB

GDB — мощный отладчик, поддерживающий низкоуровневую работу.


Запуск отладки
gdb ./hello

Основные команды
КомандаДействие
layout asmПоказать ассемблерный код в реальном времени
break _startУстановить точку останова
runЗапустить программу
stepi (si)Выполнить одну инструкцию
nexti (ni)Выполнить инструкцию, не заходя в вызовы
info registers (i r)Показать все регистры
print/x $raxПоказать RAX в шестнадцатеричном виде
x/10xb $rspПоказать 10 байт памяти по адресу RSP
disassembleДизассемблировать текущую функцию
quitВыйти

Пример сессии
(gdb) break _start
(gdb) run
(gdb) layout asm
(gdb) stepi
(gdb) i r rax rdi rsi rdx
(gdb) x/s $rsi ; показать строку по адресу RSI

GDB особенно полезен при отладке переполнения стека, неправильной адресации или ошибок системных вызовов.


Анализ исполняемого файла — objdump и readelf

objdump — дизассемблирование
objdump -d hello

Выводит машинный код и соответствующие ассемблерные инструкции:

0000000000401000 <_start>:
401000: 48 c7 c0 01 00 00 00 mov $0x1,%rax
401007: 48 c7 c7 01 00 00 00 mov $0x1,%rdi
...

Опции:

  • -d — дизассемблировать исполняемые секции
  • -t — показать таблицу символов
  • -s — показать содержимое секций в hex

readelf — анализ структуры ELF
readelf -h hello # заголовок ELF
readelf -S hello # секции
readelf -s hello # символы

Позволяет убедиться, что секции .text, .data присутствуют, точки входа корректны.


Трассировка выполнения — strace и ltrace

strace — отслеживание системных вызовов
strace ./hello

Вывод:

write(1, "Hello, world!\n", 14) = 14
exit(0) = ?
+++ exited with 0 +++

Полезно для:

  • проверки, какие файлы открываются
  • диагностики ошибок (-1 ENOENT)
  • понимания поведения программы без исходного кода

ltrace — отслеживание вызовов libc
ltrace ./hello_libc

Вывод:

printf("Hello from libc!\n") = 18
exit(0) = ?

Используется, когда программа активно применяет стандартную библиотеку.


Поиск ошибок — типичные проблемы и решения

1. Segmentation fault (ошибка сегментации)

Причины:

  • обращение по недопустимому адресу (mov eax, [0])
  • запись в .text-секцию
  • порча стека (pop без push, непарный ret)

Решение:

  • запустить под gdb, выполнить до падения, проверить регистры и стек
  • использовать strace, чтобы увидеть последний системный вызов

2. Программа завершается, но ничего не выводит

Проверьте:

  • правильно ли задана длина строки (не забыт ли \n или терминатор)
  • используется ли stdout (fd=1), а не stderr (fd=2)
  • не перепутаны RDX и RSI в sys_write

3. Стек не выровнен (ошибка при вызове libc)

При вызове функций из libc стек должен быть выровнен по 16-байтной границе перед call.

Если в main вы делаете push rbp, стек смещается на 8 байт. Чтобы выровнять:

main:
push rbp
mov rbp, rsp
sub rsp, 8 ; теперь RSP % 16 == 0
; ... вызовы printf ...
add rsp, 8
pop rbp
ret

Или используйте чётное количество push.


4. Неправильный номер системного вызова

Номера отличаются между архитектурами. Для x86-64 Linux актуальные номера можно найти в:

  • /usr/include/asm/unistd_64.h
  • онлайн-справочниках (например, syscalls.kernelgrok.com)

Автоматизация — Makefile

Для проектов с несколькими файлами удобно использовать Makefile:

Код ITЗагрузка примера кода…

Команда make соберёт обе версии программы.


Практические примеры

Теория становится полезной, когда применяется на практике. В этой заключительной части представлены законченные, рабочие примеры программ на ассемблере для x86-64 Linux. Каждый пример демонстрирует ключевые концепции — управление памятью, арифметика, рекурсия, работа с файлами, обработка ввода и оптимизация. Все программы написаны в синтаксисе NASM и могут быть собраны стандартными инструментами.


Пример 1 — Рекурсивный факториал (чистый системный вызов)

Эта программа вычисляет факториал числа, переданного как аргумент командной строки (упрощённо — фиксированное значение), и выводит результат в виде десятичного числа.

Код ITЗагрузка примера кода…

Особенности:

  • Ручная конвертация числа в строку (itoa)
  • Рекурсивный вызов с сохранением контекста
  • Чистый вывод через sys_write

Пример 2 — Обход массива и поиск максимума

Программа ищет максимальное значение в статическом массиве и выводит его.

Код ITЗагрузка примера кода…

Особенности:

  • Использование loop для итерации
  • Сравнение знаковых чисел
  • Модульный вывод через подпрограмму

Пример 3 — Чтение файла и подсчёт байтов

Программа открывает файл, читает его по блокам и выводит общий размер.

Код ITЗагрузка примера кода…

Особенности:

  • Работа с файловыми дескрипторами
  • Циклическое чтение большого файла
  • Обработка ошибок открытия

Пример 4 — Оптимизированный цикл — сумма массива

Сравним два подхода: через loop и через dec/jnz.

Код ITЗагрузка примера кода…

На современных процессорах loop не оптимизирован так же хорошо, как dec/jnz, поэтому второй вариант предпочтителен.


Пример 5 — Использование SIMD (SSE) для ускорения

Теория IEEE 754, movss/addps, AVX и x87 — в статье про float и SIMD. Сложение массива с использованием 128-битных регистров:

Код ITЗагрузка примера кода…

Требования:

  • Данные выровнены по 16 байтам
  • Поддержка SSE у процессора

В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

СправочникиСправочник по visual-basic, Справочник по языку С, Справочник по Pascal, Справочник по нотации BPMN 2.0, Справочник по Lisp, Справочник по Terraform.