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

Основы ассемблера

Разработчику Архитектору

Основы ассемблера

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

Язык ассемблера — это язык программирования со следующими особенностями:

  • Типизация — без системы типов в привычном смысле — размер и смысл данных (байт, слово, QWORD) задаёт программист; статических или динамических проверок при сборке нет.
  • Парадигма — императивный, процедурный; каждая мнемоника обычно соответствует одной машинной команде; встроенных ООП-, функциональных и управляющих конструкций высокого уровня нет.
  • Уровень — низкоуровневый (язык второго поколения; ближе к машинному коду, чем к C или Python).
  • Выполнение — компилируемый — ассемблер (NASM, GAS, MASM) переводит исходник в объектный файл, линкер собирает нативный исполняемый модуль.
  • Память — ручное управление — регистры, стек, секции .data/.bss, соглашения о вызовах; GC, RAII и автоматический подсчёт ссылок отсутствуют.
  • Платформа — привязан к конкретной архитектуре ISA (x86, ARM, RISC-V и др.); не кроссплатформенный без переписывания; транслируется в машинный код CPU, а не в другой язык.
  • Формат разработки — не скриптовый: исходник .asm/.s, явные секции, точка входа, сборка и линковка; отдельные фрагменты вставляют как inline assembly в C/C++.
  • Направление — системное программирование, встраиваемые системы, драйверы и ядра, узкие оптимизированные участки, отладка и дизассемблирование.
  • REPL — нет: код собирают и запускают; интерактивная работа — через отладчики GDB, LLDB, x64dbg (пошаговое исполнение, просмотр регистров и памяти).
  • Поколение — старый, классический (с 1949 г., EDSAC); в нишевых задачах по-прежнему актуален.
  • Параллелизм и асинхронность — на уровне языка отсутствуют: потоки и процессы — через ОС; доступны атомарные инструкции CPU и SIMD; async/await нет.
  • Безопасность — опасный: нет проверки границ, типов и null; ошибка в инструкции или адресе может привести к сбою процесса или неопределённому поведению.

Если какой-то пункт из списка непонятен — подробные определения и примеры в Язык программирования.

Контекст примеров в этой статье: синтаксис Intel, ассемблер NASM, если не указано иное. Листинги смешивают 16/32-битные мнемоники (AX, EAX) для иллюстрации идей и отдельные фрагменты под Linux i386 (int 0x80). Эталонная сборка "сегодня" — Linux x86-64 + syscall: Первая программа и справочник.

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


Язык ассемблера и программа-ассемблер

Язык ассемблера — представление команд процессора в форме, доступной для чтения человеком. В ISO/IEC и учебной традиции его относят к языкам низкого уровня (второе поколение после чистого машинного кода) — выше — Fortran, C, Python и другие высокоуровневые языки.

Язык ассемблера — текст из мнемоник, операндов и директив (.data, global, equ). Ассемблер (например, NASM, GAS, MASM) — утилита-транслятор, которая переводит этот текст в объектный файл (.o); линкер (ld, link.exe) собирает исполняемый модуль. Компилятор C/C++ часто выдает промежуточный .s — тот же язык, тот же ассемблер на следующем шаге. См. о разделе про различие языка, ассемблера и дизассемблера.

Язык низкого уровня: каждая мнемоника обычно отображается в одну машинную инструкцию целевой ISA. Макросы (%macro), директивы резервирования памяти и некоторые псевдо-инструкции разворачиваются в несколько команд или только в данные — это слой транслятора, а не CPU. Язык служит мостом между двоичным кодом и человекочитаемой записью.

Общеупотребительного единого синтаксиса нет — у каждой архитектуры (x86, ARM, RISC-V) свой набор мнемоник, а у каждого ассемблера — свой диалект (Intel и AT&T для x86, директивы NASM и GAS). Файлы исходников традиционно имеют расширение .asm или .s.

Элементы выразительности

Помимо "голых" инструкций CPU, язык и ассемблер дают вспомогательные конструкции:

ЭлементНазначениеВ NASM (примеры)
МеткаИмя адреса для переходов, вызовов, данныхloop_start:, _start
Именованная константаСмысловое имя числа, подстановка при сборке%define SYS_WRITE 1, len equ $ - msg
КомментарийПояснение для человека; в asm важнее, чем в Python; комментарий
МакросШаблон команд, копируемый при каждом вызове%macro%endmacro — см. Макросы
ДирективаУправление секциями, данными, режимом сборкиsection .data, db, global

При сборке метка превращается в адрес или смещение, константа — в число, макрос — в развёрнутый текст инструкций.

Колонки листинга

Ассемблерный исходник и листинг после сборки часто оформляют колонками (как в учебниках и в выводе objdump):

  1. Адрес (опционально) — где лежит инструкция или данные.
  2. Метка — имя точки в программе.
  3. Мнемоника — операция процессора.
  4. Операнды — регистры, память, константы.
  5. Комментарий.

На уровне машинного кода программа линейна: переход может вести в любую метку. Структура if / for — соглашение программиста из cmp, jmp и меток (см. ниже в этой статье).

Литералы и системы счисления

В asm часто пишут числа не в десятичной, а в шестнадцатеричной или двоичной форме — так удобнее читать биты и байты. В NASM (Intel-синтаксис) типичны записи:

  • десятичное: 42;
  • шестнадцатеричное: 0x2A или 2Ah (буквы A–F в начале — с ведущим нулём, 0FFh);
  • двоичное: 0b00101010;
  • символьная константа: 'A'.

Способы записи различаются у ассемблеров; при переносе листинга между NASM, GAS (AT&T) и MASM сверяйте руководство транслятора.

Программы на Ассемблере пишутся с учётом конкретной архитектуры процессора — например, x86, ARM, MIPS или RISC-V. Это означает, что код, написанный под одну архитектуру, не будет работать на другой без переписывания. Такая привязка к железу даёт полный контроль над поведением системы — разработчик управляет регистрами, памятью, флагами состояния и потоком выполнения на уровне, недоступном в высокоуровневых языках.

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


Синтаксис и структура программы

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

Стандартная структура строки включает четыре возможных элемента:

  • Метка — идентификатор, обозначающий адрес в памяти. Метки используются для организации переходов и вызовов.
  • Мнемоника — символьное имя машинной команды, например MOV, ADD, JMP.
  • Операнды — данные или адреса, над которыми выполняется операция. Их количество и тип зависят от конкретной инструкции.
  • Комментарий — пояснение для человека, игнорируемое ассемблером.

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

start: MOV AX, 5

Разбор:

  • start — метка, ассемблер привязывает к ней адрес текущей инструкции, чтобы затем можно было перейти через jmp start или вызвать как точку входа.
  • MOV — инструкция копирования данных между операндами; здесь она загружает немедленное значение в регистр.
  • AX — 16-битный регистр общего назначения, то есть операция затрагивает только младшие 16 бит соответствующего семейства регистра.
  • 5 — непосредственный литерал (immediate), который кодируется прямо в машинной инструкции.
  • В этой строке нет обращения к памяти, поэтому операция выполняется только на уровне регистра и не зависит от состояния RAM.

Здесь start — метка, MOV — мнемоника, AX и 5 — операнды. Эта строка означает: "загрузить значение 5 в регистр AX".

Для x86 существуют два распространённых диалекта записи одной и той же системы команд — Intel и AT&T. В Intel mov eax, 10 читается как "в EAX положить 10"; в AT&T та же команда — movl $10, %eax (сначала источник, затем приёмник). NASM и MASM используют Intel; GAS и objdump на Linux по умолчанию — AT&T.

На старте в этом разделе держитесь Intel + NASM. Таблица отличий, примеры пар строк и настройки GDB/objdump — в Система команд и Intel/AT&T.


Символические мнемоники вместо байтов

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

Мнемоники отражают семантику операции. Например:

  • MOV — переместить данные из одного места в другое.
  • ADD — сложить два значения.
  • JMP — передать управление на указанную метку.
  • CMP — сравнить два значения и установить флаги результата.
  • CALL — вызвать подпрограмму.

Каждая мнемоника имеет строго определённое поведение, заданное спецификацией процессора. Ассемблер преобразует эти мнемоники в соответствующие байты машинного кода во время сборки. Этот процесс называется трансляцией.

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


Соответствие мнемоник машинному коду

Строка с мнемоникой (не директивой и не макросом) обычно даёт одну машинную инструкцию. Отсюда предсказуемость — в листинге ассемблера (objdump -d, -S у GCC) видно, что реально выполнит CPU. Точное число тактов зависит от микроархитектуры, кэша и конвейера — но порядок инструкций под контролем программиста.

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

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


Отсутствие высокоуровневых конструкций

Язык Ассемблер не содержит таких привычных элементов, как переменные с именами, циклы for или while, условные операторы if-else, функции в общепринятом смысле. Вместо этого всё строится из базовых компонентов — регистров, ячеек памяти, меток и явных переходов.

Переменные реализуются через выделение участков памяти или использование регистров. Циклы организуются с помощью меток и инструкций условного или безусловного перехода. Условные ветвления достигаются комбинацией команд сравнения (CMP) и условных переходов (JE, JNE, JG и другие). Подпрограммы создаются с использованием меток и команд CALL/RET, при этом передача параметров и возврат результата осуществляются вручную — обычно через стек или регистры.

Это делает код на Ассемблере более длинным и трудоёмким в написании, но одновременно даёт абсолютную прозрачность. Разработчик видит каждый шаг выполнения программы и может контролировать каждый байт данных.


Регистры

Регистры — это самые быстрые и ограниченные по объёму области памяти, встроенные непосредственно в процессор. Они служат основным местом хранения данных во время выполнения инструкций. Каждый регистр имеет фиксированный размер, определяемый разрядностью архитектуры — 8, 16, 32 или 64 бита.

В архитектуре x86, например, выделяют несколько групп регистров:

  • Общего назначенияAX, BX, CX, DX и их расширения (EAX, RAX и так далее). Эти регистры используются для арифметических операций, адресации, временного хранения данных.
  • Указатель стека: SP (Stack Pointer) — содержит адрес вершины стека.
  • Указатель базы: BP (Base Pointer) — помогает обращаться к параметрам и локальным переменным в стеке.
  • Индексные регистры: SI (Source Index) и DI (Destination Index) — применяются при работе с массивами и блоками памяти.
  • Регистр флаговFLAGS — хранит информацию о результатах предыдущих операций — был ли результат нулём, произошло ли переполнение, был ли перенос и так далее.

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


Память и адресация

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

Адресация — это способ указания места в памяти, с которым работает инструкция. Существует несколько режимов адресации:

  • Непосредственная (immediate): значение задано прямо в команде. Например, MOV AX, 42.
  • Регистровая: операнд находится в регистре. Например, MOV BX, AX.
  • Прямая (absolute): используется конкретный адрес памяти. Например, MOV AX, [0x1000].
  • Косвенная через регистр: адрес хранится в регистре. Например, MOV AX, [BX].
  • Базовая со смещением: адрес вычисляется как сумма значения регистра и константы. Например, MOV AX, [BP + 4].
  • Масштабированная индексная: используется для работы с массивами. Например, MOV EAX, [ESI + EDI * 4].

Выбор режима адресации влияет на длину машинной инструкции и время её выполнения. Программист выбирает наиболее подходящий вариант в зависимости от структуры данных и требований к производительности.

Несколько режимов в одном фрагменте:

mov ax, 42 ; immediate: константа в регистре
mov bx, ax ; register: копирование между регистрами
mov cx, [bx] ; indirect: читаем слово по адресу из BX
mov dx, [bp + 4] ; base+displacement: параметр в стековом кадре

Разбор:

  • mov ax, 42 не обращается к памяти: значение зашито в саму инструкцию.
  • mov bx, ax копирует уже загруженное значение в другой регистр.
  • mov cx, [bx] трактует содержимое BX как адрес и читает 16 бит из RAM.
  • mov dx, [bp + 4] типичен для чтения аргумента функции относительно базового указателя кадра.
  • Скобки [...] в Intel-синтаксисе означают "взять значение по адресу", а не "адрес как число".

Стек

Стек — это участок памяти, организованный по принципу "последним пришёл — первым ушёл" (LIFO). Он играет ключевую роль в передаче параметров в подпрограммы, хранении возвращаемых адресов и размещении локальных переменных.

Две основные операции со стеком:

  • PUSH — помещает значение на вершину стека и уменьшает указатель стека.
  • POP — извлекает значение с вершины стека и увеличивает указатель стека.

При вызове подпрограммы с помощью CALL процессор автоматически помещает в стек адрес следующей инструкции. При выполнении RET этот адрес извлекается, и управление возвращается в точку вызова. Так обеспечивается корректное завершение функций и возврат к вызывающему коду.

Стек также используется для временного сохранения регистров перед вызовом других функций, чтобы не потерять их содержимое. Это особенно важно при соблюдении соглашений о вызовах (calling conventions), которые определяют, какие регистры должен сохранять вызывающий код, а какие — вызываемый.

Мини-пример работы со стеком:

mov ax, 100
push ax ; положить AX на вершину, SP уменьшится
mov ax, 0 ; AX перезаписан — старое значение в стеке
pop bx ; вернуть 100 уже в BX, SP восстановится

Разбор:

  • push ax записывает копию AX по адресу SP, затем уменьшает SP на размер операнда (2 байта в 16-битном режиме).
  • После push исходное значение живёт в памяти стека, даже если регистр изменить.
  • pop bx снимает верх стека в BX и увеличивает SP.
  • Порядок push/pop должен быть симметричным: иначе стек "съедет" и ret вернётся не туда.

Организация данных

Программа на Ассемблере состоит не только из исполняемого кода, но и из данных. Для их разделения используются специальные секции:

  • .text — исполняемые инструкции.
  • .data — инициализированные данные (строки, константы).
  • .bss — неинициализированные данные (резерв при загрузке).

Внутри этих секций применяются директивы ассемблера для описания данных:

  • DB — определить байт.
  • DW — определить слово (2 байта).
  • DD — определить двойное слово (4 байта).
  • DQ — определить четверное слово (8 байт).
  • RESB, RESW, RESD — зарезервировать указанное количество байтов, слов или двойных слов без инициализации.

Пример:

section .data
message db 'Hello, world!', 0
counter dd 100

section .bss
buffer resb 256

Разбор:

  • section .data открывает сегмент инициализированных данных — всё, что объявлено здесь, попадает в образ программы с уже заданными байтами.
  • message db 'Hello, world!', 0 создаёт массив байтов со строкой и нулевым терминатором для C-совместимых функций.
  • counter dd 100 объявляет 32-битное значение (double word) и записывает в него константу 100 на этапе загрузки.
  • section .bss выделяет память под неинициализированные данные; размер фиксируется, но сами байты не занимают место в файле как явные литералы.
  • buffer resb 256 резервирует 256 байт рабочего буфера, который программа может заполнить во время выполнения.

Этот фрагмент выделяет строку с завершающим нулём, 32-битную переменную counter и буфер размером 256 байт. Такая явная декларация данных даёт полный контроль над их размещением и форматом.


Простые программы

Первая программа часто выводит текст. Ниже — Linux, 32-бит, int 0x80 (исторический, но наглядный контракт; на современном 64-bit Linux используйте syscall — см. Первая программа):

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

Разбор:

  • В section .data объявляются строка msg и символическая длина len, вычисленная как разность текущего адреса и адреса начала строки.
  • global _start экспортирует метку точки входа, чтобы линкер/загрузчик знали, откуда начинать исполнение.
  • Блок mov eax,4 / mov ebx,1 / mov ecx,msg / mov edx,len подготавливает аргументы write(fd, buf, count) для Linux i386 ABI.
  • int 0x80 вызывает ядро через программное прерывание и выполняет системный вызов с номером из EAX.
  • Вторая последовательность с eax=1 и ebx=0 завершает процесс через sys_exit(0).
  • Этот фрагмент одновременно показывает жизненный цикл минимальной программы — подготовка данных, вывод, корректное завершение.

Сборка (пример): nasm -f elf32 hello.asm && ld -m elf_i386 hello.o -o hello.

Программа показывает секции .data/.text, передачу аргументов в регистрах и завершение через ядро. На x86-64 номера другие (sys_write = 1, sys_exit = 60) и вызов идёт инструкцией syscall.

Тот же "Hello" для Linux x86-64 (современный вариант):

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

Разбор:

  • sys_write на 64-битном Linux имеет номер 1 (не 4, как в i386).
  • Аргументы идут в RDI, RSI, RDX по System V AMD64 ABI.
  • lea rsi, [rel msg] формирует адрес строки для позиционно-независимого кода (PIE).
  • syscall — единая инструкция входа в ядро на x86-64 вместо int 0x80.
  • xor rdi, rdi задаёт код выхода 0 быстрее, чем mov rdi, 0.

Арифметические операции выполняются напрямую:

MOV EAX, 10
ADD EAX, 5 ; EAX = 15
SUB EAX, 3 ; EAX = 12
IMUL EAX, 2 ; EAX = 24

Разбор:

  • MOV EAX, 10 инициализирует рабочий регистр исходным значением для цепочки вычислений.
  • ADD EAX, 5 складывает с литералом и обновляет флаги (ZF, SF, CF, OF), которые могут быть использованы следующими условными переходами.
  • SUB EAX, 3 выполняет вычитание и снова обновляет флаги; текущее значение становится 12.
  • IMUL EAX, 2 делает знаковое умножение текущего значения на 2 и помещает результат обратно в EAX.
  • Последовательность демонстрирует in-place вычисления: один и тот же регистр хранит промежуточные результаты без лишних обращений к памяти.

Каждая команда изменяет не только содержимое регистра, но и флаги в регистре FLAGS. Эти флаги затем используются условными переходами для организации логики программы.


Отладка и анализ

Отладка программ на Ассемблере требует специализированных инструментов, таких как GDB, LLDB или x64dbg. Они позволяют выполнять код пошагово, просматривать содержимое регистров и памяти, устанавливать точки останова и анализировать состояние процессора на каждом этапе.

Особое внимание уделяется проверке корректности адресов, значений флагов и содержимого стека. Ошибки в этих областях часто приводят к аварийному завершению программы или неопределённому поведению.

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

  1. Поставить брейкпоинт на _start или первую метку своей функции.
  2. Выполнить 5-10 инструкций по одной (stepi) и после каждой смотреть RAX/RSP/RIP.
  3. Проверить, что адреса, передаваемые в системный вызов, реально указывают на ожидаемые данные.
  4. Сверить логику флагов после CMP перед JE/JNE/JG/....

Этот ритм быстро формирует навык чтения машинной логики без догадок.


Условные переходы и логика программы

Управление потоком выполнения в Ассемблере строится на инструкциях перехода. Безусловный переход JMP немедленно передаёт управление на указанную метку, прерывая последовательное выполнение. Условные переходы зависят от состояния флагов процессора, установленных предыдущими операциями — чаще всего командой CMP (сравнение) или арифметическими инструкциями.

Команда CMP A, B вычитает значение B из A, не сохраняя результат, но устанавливая флаги в зависимости от исхода — был ли результат нулём, положительным, отрицательным, произошёл ли перенос или переполнение. После этого можно использовать условные переходы:

  • JE / JZ — переход, если значения равны (флаг нуля установлен).
  • JNE / JNZ — переход, если значения не равны.
  • JG, JL, JGE, JLE — переходы для знаковых сравнений (greater, less и их варианты).
  • JA, JB, JAE, JBE — переходы для беззнаковых сравнений (above, below и их варианты).

Пример (if (eax > 10)):

cmp eax, 10
jle skip ; если eax <= 10 — перейти мимо тела
; тело: eax > 10
skip:
; продолжение

Разбор:

  • cmp eax, 10 выполняет внутреннее вычитание eax - 10, но не сохраняет результат, а только выставляет флаги состояния.
  • jle skip читает комбинацию флагов для знакового сравнения и прыгает, если eax меньше либо равно 10.
  • Комментарий после jle описывает "guard"-паттерн: при ложном условии выполнение перескакивает блок тела.
  • Метка skip задаёт точку продолжения потока исполнения после условной ветки.
  • Такая форма является прямым asm-эквивалентом if (eax > 10) { ... }.

CMP выставляет флаги; знаковые переходы (JG, JLE, …) интерпретируют операнды как числа со знаком, беззнаковые (JA, JB, …) — иначе (см. Типы данных и регистры). Вся логика программы — от простых проверок до сложных алгоритмов — строится из комбинаций таких переходов.


Циклы

Циклы в Ассемблере реализуются вручную с использованием меток и условных или безусловных переходов. Типичная структура цикла включает инициализацию счётчика, тело цикла, изменение счётчика и проверку условия продолжения.

Пример цикла, выполняющегося 5 раз:

MOV ECX, 5 ; инициализация счётчика
loop_start:
; тело цикла — любые инструкции
DEC ECX ; уменьшить счётчик
JNZ loop_start ; перейти, если ECX ≠ 0

Разбор:

  • MOV ECX, 5 задаёт количество итераций в регистре-счётчике.
  • Метка loop_start маркирует начало повторяемого блока.
  • DEC ECX уменьшает счётчик и автоматически обновляет флаг нуля (ZF), когда счётчик достигает 0.
  • JNZ loop_start проверяет ZF: пока он не установлен, выполняется прыжок назад и цикл продолжается.
  • Конструкция DEC + JNZ формирует компактный цикл без отдельной инструкции CMP.

Этот шаблон соответствует циклу for (i = 5; i > 0; i--) в языках высокого уровня. Архитектура x86 даже предоставляет специальную инструкцию LOOP, которая автоматически уменьшает ECX и переходит, если он не ноль, но её использование сегодня редко встречается из-за ограниченной гибкости.

Циклы могут быть вложенными, иметь сложные условия выхода или управляться не только счётчиками, но и флагами, содержимым памяти или внешними событиями.


Подпрограммы

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

Внутри подпрограммы обычно выполняются следующие действия:

  1. Сохранение регистров, которые будут изменяться (если требуется по соглашению о вызовах).
  2. Выделение места в стеке для локальных переменных (при необходимости).
  3. Выполнение основной логики.
  4. Восстановление сохранённых регистров.
  5. Очистка стека (в некоторых моделях вызова).
  6. Возврат управления вызывающему коду с помощью RET.

Пример простой подпрограммы:

add_numbers:
PUSH EBP
MOV EBP, ESP
MOV EAX, [EBP + 8] ; первый аргумент
ADD EAX, [EBP + 12] ; второй аргумент
POP EBP
RET

Разбор:

  • Метка add_numbers объявляет точку входа в подпрограмму, вызываемую через CALL.
  • PUSH EBP / MOV EBP, ESP создают стековый кадр, фиксируя базу, относительно которой удобно читать аргументы.
  • [EBP + 8] и [EBP + 12] — первый и второй аргументы функции в классической 32-битной стековой модели.
  • ADD EAX, [EBP + 12] складывает второй аргумент с первым, уже загруженным в EAX.
  • POP EBP восстанавливает предыдущий базовый указатель, чтобы не повредить контекст вызывающей стороны.
  • RET извлекает адрес возврата из стека и передаёт управление обратно после CALL.

Эта функция складывает два 32-битных целых числа, переданных через стек. Она использует стандартное соглашение о вызовах, при котором аргументы размещаются в стеке, а возвращаемое значение передаётся через регистр EAX.


Соглашения о вызовах

Соглашения о вызовах определяют, как функции получают параметры, возвращают результаты и управляют стеком. Наиболее распространённые соглашения в x86:

  • cdecl — параметры передаются через стек справа налево, вызывающий код очищает стек после вызова. Используется в C по умолчанию.
  • stdcall — параметры также передаются через стек, но вызываемая функция сама очищает стек. Применяется в Windows API.
  • fastcall — часть параметров передаётся через регистры (ECX, EDX), остальные — через стек. Позволяет ускорить вызовы.
  • System V AMD64 ABI — стандарт для Unix на x86-64 — первые шесть целочисленных аргументов — в RDI, RSI, RDX, RCX, R8, R9 (подробнее в Команды и подпрограммы).

Знание соглашения необходимо при написании совместимого кода, особенно при взаимодействии с библиотеками или компиляторами других языков.


Взаимодействие с высокоуровневыми языками

Ассемблер часто используется внутри проектов, написанных на C, C++ или других языках, для критических по производительности участков кода. Современные компиляторы позволяют встраивать ассемблерные вставки (inline assembly) или подключать отдельные .asm файлы.

При таком подходе важно соблюдать соглашения о вызовах, правильно объявлять экспортируемые символы и учитывать особенности ABI (Application Binary Interface) целевой платформы. Например, в GCC для объявления глобальной метки, видимой из C, используется директива global.

Обратная ситуация — вызов C-функций из чистого Ассемблера — также возможна. Для этого нужно связать объектный файл с библиотекой времени выполнения и правильно подготовить аргументы в соответствии с ABI.


Зачем изучать asm и где он уместен сегодня

Язык ассемблера даёт прямой контроль над инструкциями процессора — проще оптимизировать узкие места, работать с регистрами особых расширений (SIMD, криптоинструкции) и читать листинги компилятора. Цена — больше текста, ручное управление стеком и регистрами, отсутствие встроенной проверки типов (смысл байта в памяти задаёт программист).

Сильные стороныОграничения
Предсказуемое соответствие мнемоника ↔ машинная командаПеренос на другую архитектуру — переписывание
Доступ к аппаратным возможностям и ABI "в лоб"Крупные проекты целиком на asm почти не пишут
Полезен в отладке и дизассемблированииСовременный оптимизирующий компилятор часто обгоняет "средний" ручной код

На практике большую часть программы пишут на C, Rust, C++ и вставляют asm только в критичных фрагментах — см. раздел про встроенный ассемблер. Для школьного курса достаточно понимать место asm в таблице уровней языков — глава 4 базовой информатики.


Куда двигаться после этой базы

Если материал статьи уже понятен, удобный маршрут углубления такой:

  1. Система команд и Intel/AT&T — ISA и чтение листингов GNU.
  2. Архитектура ассемблерных программ — взаимодействие с ОС, секции и исполнение.
  3. Типы данных и регистры — знаковые/беззнаковые сравнения, адресация, endianness.
  4. Управляющие конструкции — ветвления, циклы и CALL/RET.
  5. Команды и подпрограммы — ABI и стабильные шаблоны функций.
  6. Первая программа — закрепление через запуск и отладку.