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

Макросы и условная сборка

Разработчику

Контекст: NASM. Макросы обрабатывает ассемблер при сборке, процессор их не выполняет. См. Основы про различие мнемоник и директив.

Макроассемблер — ассемблер с препроцессором — поверх базовых мнемоник доступны макросы, именованные константы (%define, equ), условная сборка (%if, %ifdef) и директивы (секции, выравнивание, global/extern). Набор директив задаёт конкретный транслятор (NASM, GAS, MASM), а не только модель процессора.


Макрос и подпрограмма

Подпрограмма (call)Макрос (%macro)
Когда разворачиваетсяпри запускепри сборке
Размер кодаодин экземпляр в .textкопия на каждый вызов макроса
Стоимостьпереход + стектолько инструкции тела
Отладкаодна меткаповторяющиеся блоки в листинге

Макрос уместен для коротких шаблонов (пролог, обёртка над syscall, развёртка цикла). Длинную логику выносят в call.


Текстовые константы — %define

%define SYS_WRITE 1
%define SYS_EXIT 60
%define STDOUT 1

mov rax, SYS_WRITE
mov rdi, STDOUT

Разбор:

  • %define создаёт текстовые псевдонимы на этапе препроцессора NASM.
  • SYS_WRITE и SYS_EXIT — номера системных вызовов Linux x86-64.
  • STDOUT равен 1, это стандартный дескриптор вывода.
  • mov rax, SYS_WRITE подготавливает номер syscall в RAX.
  • mov rdi, STDOUT подготавливает первый аргумент syscall в RDI.
  • Полный write потребует ещё RSI (адрес буфера) и RDX (длина).

Препроцессор подставляет текст до разбора инструкций. Это не переменная в памяти и не символ линкера.


Параметризованный макрос — %macro

%macro exit 1
mov rax, 60
mov rdi, %1
syscall
%endmacro

_start:
; ...
exit 0

Разбор:

  • %macro exit 1 объявляет макрос exit с одним обязательным аргументом.

  • Внутри макроса %1 заменяется на переданное значение при раскрытии.

  • mov rax, 60 выбирает syscall exit.

  • mov rdi, %1 передаёт код завершения процесса.

  • syscall передаёт управление ядру и завершает процесс.

  • Строка exit 0 в _start разворачивается в три инструкции и не делает call.

  • Имя: exit.

  • Число после имени: обязательное количество аргументов (1).

  • %1, %2 … — подстановка аргументов.

Макрос с необязательными параметрами: %macro name min [-max+].


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

Если макрос содержит метки, при повторном включении имена столкнутся. В NASM используют %% для метки, уникальной на каждое раскрытие:

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

Разбор:

  • Макрос push_callee_saved сохраняет регистры, которые по ABI обычно обязан восстанавливать вызываемый код.
  • Порядок push фиксирует значения на стеке сверху вниз.
  • Макрос pop_callee_saved восстанавливает те же регистры в обратном порядке.
  • Обратный порядок обязателен, иначе регистры получат чужие значения.
  • Такой шаблон часто ставят в пролог/эпилог сложных функций.
  • Аргументов у макросов нет (0), это просто повторно используемые блоки инструкций.

Внутри макроса с циклом:

%macro rep_zero 2
xor rax, rax
%%loop:
mov [%1 + %2 * 8 - 8], rax ; пример: обнулить массив слов
dec %2
jnz %%loop
%endmacro

Разбор:

  • Макрос принимает два аргумента: %1 — база массива, %2 — счётчик элементов.
  • xor rax, rax быстро формирует ноль для последующих записей.
  • %%loop — локальная метка NASM, уникальная для каждого раскрытия макроса.
  • mov [%1 + %2 * 8 - 8], rax записывает 0 в очередной 8-байтовый элемент.
  • dec %2 уменьшает число оставшихся элементов.
  • jnz %%loop повторяет цикл, пока счётчик не станет нулём.

Условная сборка — %if, %ifdef

%define ARCH_BITS 64

%if ARCH_BITS == 64
%define PTR_SIZE 8
%else
%define PTR_SIZE 4
%endif

Разбор:

  • ARCH_BITS задаёт параметр конфигурации на этапе сборки.
  • %if проверяет условие препроцессора, а не исполняется процессором.
  • При значении 64 определяется PTR_SIZE 8.
  • В ветке %else для 32-битного варианта назначается PTR_SIZE 4.
  • %endif завершает блок условной сборки.
  • Такой подход позволяет держать один исходник для нескольких архитектур.

Проверка наличия макроса:

%ifdef DEBUG
; вставить отладочный вывод
%endif

Разбор:

  • %ifdef DEBUG проверяет, объявлен ли символ DEBUG.
  • Если символ задан (например, через ключи сборки), блок включается в итоговый код.
  • Если символ не задан, строки между %ifdef и %endif полностью пропускаются.
  • Этот механизм используют для диагностического кода и feature-flag на этапе сборки.

Так собирают разные варианты одного файла без ручного редактирования (отладка, платформа, размер слова).


%include — подключение файла

%include "macros.inc"
%include "syscall.inc"

Разбор:

  • %include вставляет содержимое файла прямо в текущую точку исходника.
  • Порядок включения важен: второй файл уже "видит" объявления из первого.
  • Обычно в .inc хранят %define, %macro, общие константы и объявления.
  • Такой приём сокращает дублирование кода и делает модули единообразными.

Содержимое включается как будто вставлено в текущую позицию. Удобно для общих макросов и констант; исполняемый код чаще выносят в отдельные .asm с global/extern — см. несколько модулей.


Типичные ошибки

ПроблемаРешение
Метка loop: в макросе, вызванном дважды%%loop
Ожидание, что %1 вычислится в runtimeаргументы макроса — константы сборки
Огромный макрос в горячем циклераздувает I-cache; заменить на call
%define с выражением с запятойиспользовать скобки или %assign для чисел

%assign counter 0 — целочисленная переменная времени сборки (можно %assign counter counter+1 в макросе для генерации таблиц).

Пример — счётчик раскрытий макроса:

%assign table_idx 0

%macro emit_byte 1
db %1
%assign table_idx table_idx + 1
%endmacro

emit_byte 0x10
emit_byte 0x20
; table_idx теперь равен 2 — это число сборки, не переменная в .data

Разбор:

  • %assign хранит целое число на этапе ассемблирования, в отличие от %define.
  • table_idx увеличивается при каждом раскрытии emit_byte.
  • db %1 помещает байт-аргумент в секцию данных в точке вызова макроса.
  • После двух вызовов table_idx равен 2, но в исполняемый файл это значение само по себе не попадает.
  • Такой приём удобен для генерации таблиц, подсчёта смещений и отладочных меток в листинге.

Макрос для системного вызова (Linux x86-64)

%macro syscall3 3
mov rax, %1
mov rdi, %2
mov rsi, %3
syscall
%endmacro

; syscall3 1, 1, msg ; write — для полного write нужен ещё rdx

Разбор:

  • Макрос syscall3 формирует шаблон системного вызова с тремя параметрами.
  • %1 попадает в RAX как номер syscall.
  • %2 и %3 попадают в RDI и RSI как первые аргументы ABI Linux.
  • syscall передаёт управление ядру с подготовленными регистрами.
  • Комментарий показывает пример вызова для write, но для корректного write нужно также установить RDX (длина буфера).
  • Для syscall с большим числом аргументов расширяют макрос под RDX, R10, R8, R9.

Для четырёх и более аргументов расширяют шаблон (rdx, r10 … по ABI). Полная таблица номеров — в справочнике.

Полный макрос write для Linux x86-64:

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

Разбор:

  • Макрос write принимает адрес (%1) и длину (%2) буфера.
  • mov rsi, %1 и mov rdx, %2 закрывают все аргументы syscall write по ABI.
  • hello_len equ $ - hello вычисляет длину строки на этапе сборки.
  • Вызов write hello, hello_len разворачивается в четыре mov и один syscall.
  • После вывода отдельный блок с mov rax, 60 завершает процесс с кодом 0.
  • Это типичный каркас "голой" программы: макрос для I/O + явный exit.

Сборка с отладочным кодом через -d:

nasm -f elf64 -dDEBUG prog.asm -o prog.o
%ifdef DEBUG
%define LOG_LEVEL 1
%else
%define LOG_LEVEL 0
%endif

Разбор:

  • Ключ -dDEBUG в командной строке NASM определяет символ DEBUG на время сборки.
  • В отладочной сборке LOG_LEVEL становится 1, в релизной — 0.
  • Условие обрабатывается препроцессором, лишний код в бинарник не попадает.
  • Так отделяют отладочные константы и блоки без правки исходника вручную.