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

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


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


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


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

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


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

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

Язык ассемблера — низкоуровневый язык для прямого управления 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 вещественных аргументов — в XMM0–XMM7
  • Дополнительные аргументы — через стек
  • Возвращаемое значение — в RAX (или RDX:RAX для 128-битных значений)
  • Регистры RBX, RBP, R12–R15 — callee-saved (вызывающая функция ожидает, что они сохранятся)
  • Регистры RAX, RCX, RDX, R8–R11 — 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, R12–R15

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

  • RAX, RCX, RDX, RSI, RDI, R8–R11

Если функция использует 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.