Макросы и условная сборка
Контекст: 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выбирает syscallexit. -
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закрывают все аргументы syscallwriteпо 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. - Условие обрабатывается препроцессором, лишний код в бинарник не попадает.
- Так отделяют отладочные константы и блоки без правки исходника вручную.