Макросы наряду с функциями представляют механизм выделения функционала в отдельные блоки. Макрос фактически также представляет метку, с которой связан некоторый код. Когда NASM встречает в коде метку, которая представляет макрос, то заменяет эту метку на код данного макроса. Макросы позволяют заменить длинные повторяющиеся последовательности кода гораздо более короткими последовательностями.
Определение макроса в общем случае имеет следующую форму:
%macro имя_макроса количество_параметров код макроса %endmacro
Сначала идет объявление макроса, которое начинается с директивы %macro. После нее следует имя макроса и количество параметров.
Завершается макрос директивой %endmacro. Между объявлением макроса и %endmacro
располагается собственно код макроса.
Рассмотрим пример макроса, который выводит строку на консоль в программе на Linux:
global _start section .data msg db "Hello METANIT.COM", 10 ; выводимое сообщение msglen equ $-msg ; Длина сообщения section .text ; макрос, который выводит на консоль строку %macro Print 0 mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 syscall %endmacro _start: Print ; вставляем макрос Print mov rdi, 0 mov rax, 60 syscall
В данном случае макрос называется Print
и принимает 0 параметров:
%macro Print 0 mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 ; системный вызов write syscall %endmacro
В коде макросе выполняем системный вызов на вывод строки на консоль
В основной части кода для вставки макроса достаточно написать его имя:
_start: Print ; вставляем макрос Print
После обработки ассемблером программы она фактически приобретет следующий вид:
global _start section .data msg db "Hello METANIT.COM", 10 ; выводимое сообщение msglen equ $-msg ; Длина сообщения section .text _start: mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 syscall mov rdi, 0 mov rax, 60 syscall
То есть на место вызова макроса вставляется его код, который компилируется в объектный файл.
Макросы удобны тем, что они могут определить многократно повторяющийся код, и мы можем многократно вставлять макрос в код программы:
_start: Print ; вставляем макрос Print Print ; вставляем макрос Print Print ; вставляем макрос Print mov rdi, 0 mov rax, 60 syscall
В данном случае три раза вставляем макрос, соответственно мы получим программу со следующим кодом:
_start: mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 syscall mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 syscall mov rdi, 1 ; 1 - дескриптор стандартного вывода mov rsi, msg ; выводимая строка mov rdx, msglen ; длина строки mov rax, 1 syscall mov rdi, 0 mov rax, 60 syscall
Таким образом, сообщение msg будет выводиться 3 раза на консоль.
Аналогичное применение макросов в программе на Windows
; компоновка с помощью линкера от Microsoft: ; link hello.o kernel32.lib /entry:_start /subsystem:console /out:hello2.exe ; компоновка с помощью линкера от GCC: ; ld hello.o -o hello.exe -l kernel32 global _start ; делаем метку метку _start видимой извне extern WriteFile ; подключем функцию WriteFile extern GetStdHandle ; подключем функцию GetStdHandle section .data msg db "Hello METANIT.COM", 10 ; выводимое сообщение msglen equ $-msg ; Длина сообщения section .text ; макрос, который выводит на консоль строку %macro Print 0 sub rsp, 40 ; Для параметров функций WriteFile и GetStdHandle резервируем 40 байт (5 параметров по 8 байт) mov rcx, -11 ; Аргумент для GetStdHandle - STD_OUTPUT call GetStdHandle ; вызываем функцию GetStdHandle mov rcx, rax ; Первый параметр WriteFile - в регистр RCX помещаем дескриптор файла - консоли mov rdx, msg ; выводимая строка mov r8d, msglen ; длина строки xor r9, r9 ; Четвертый параметр WriteFile - адрес для получения записанных байтов mov qword [rsp + 32], 0 ; Пятый параметр WriteFile call WriteFile ; вызываем функцию WriteFile add rsp, 40 %endmacro _start: Print ; вставляем макрос Print ret
Макрос может иметь параметры и получать через них некоторые значения из вне. Выше мы определили макрос Print, который имел 0 параметров (то есть вообще их не имел). Но, допустим, мы захотим, чтобы макрос мог выводить на консоль различные строки. В этом случае мы можем передавать в макрос адрес строки и ее длину через параметры. Для этого рассмотрим следующую программу под Linux:
global _start section .data msg1 db "Hello METANIT.COM", 10 msglen1 equ $-msg1 msg2 db "Hello World", 10 msglen2 equ $-msg2 section .text ; макрос, который выводит на консоль строку %macro Print 2 mov rdi, 1 ; в RDI - 1 - дескриптор стандартного вывода mov rsi, %1 ; в RSI - выводимая строка - первый параметр макроса mov rdx, %2 ; в RDX - длина строки - второй параметр макроса mov rax, 1 syscall %endmacro _start: Print msg1, msglen1 ; передаем параметрам макроса адрес строки msg1 и msglen1 Print msg2, msglen2 ; передаем параметрам макроса адрес строки msg2 и msglen2 mov rdi, 0 mov rax, 60 syscall
Теперь макрос Print принимает два параметра, о чем говорит число 2 после имени макроса:
%macro Print 2
Внутри кода макроса эти параметры доступны через плейсхолдеры %1 (первый параметр) и %2 (второй параметр). Если бы макрос имел бы три параметра, то третий параметр мы могли бы получить через плейсхолдер %3 и так далее. В частности, значение первого параметра (адрес строки для вывода на консоль) передаем в регистр rsi, а значение второго параметра - размер строки - в регистр rdx:
mov rsi, %1 ; в RSI - выводимая строка - первый параметр макроса mov rdx, %2 ; в RDX - длина строки - второй параметр макроса
В секции .data определены две строки - msg1 и msg2 и их длины. И при вызове макроса Print передаем его параметрам данные значения:
Print msg1, msglen1
Передаваемые параметрам значения еще называют аргументы (или фактические параметры). Аргументы передаются параметрам по позиции. То есть
первый аргумент (msg1) будет передаваться первому параметру и будет доступен через плейсхолдер %1
, второй аргумент (msglen1) - второму параметру и будет доступен через
%2
. В итоге данная программа выведет на консоль две строки:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o root@Eugene:~/asm# ld -o hello hello.o root@Eugene:~/asm# ./hello Hello METANIT.COM Hello World root@Eugene:~/asm#
Аналогичная программа для Windows:
; компоновка с помощью линкера от Microsoft: ; link hello.o kernel32.lib /entry:_start /subsystem:console /out:hello2.exe ; компоновка с помощью линкера от GCC: ; ld hello.o -o hello.exe -l kernel32 global _start ; делаем метку метку _start видимой извне extern WriteFile ; подключем функцию WriteFile extern GetStdHandle ; подключем функцию GetStdHandle section .data msg1 db "Hello METANIT.COM", 10 msglen1 equ $-msg1 msg2 db "Hello World", 10 msglen2 equ $-msg2 section .text ; макрос, который выводит на консоль строку %macro Print 2 sub rsp, 40 mov rcx, -11 call GetStdHandle mov rcx, rax mov rdx, %1 ; выводимая строка - первый параметр макроса mov r8d, %2 ; длина строки - второй параметр макроса xor r9, r9 mov qword [rsp + 32], 0 call WriteFile add rsp, 40 %endmacro _start: Print msg1, msglen1 ; передаем параметрам макроса адрес строки msg1 и msglen1 Print msg2, msglen2 ; передаем параметрам макроса адрес строки msg2 и msglen2 ret
Макросы могут иметь метки. Однако поскольку макросы могут многократно вставляться в код программы, то и метки могут дублироваться, что приведет к ошибкам на стадии компиляции. Например, возьмем следующую программу на Linux:
global _start section .data size equ 8 ; размер каждого элемента в байтах arr1 dq 1, 2, 3, 4 arrlen1 equ ($-arr1)/size arr2 dq 5, 6, 7, 8 arrlen2 equ ($-arr2)/size section .text ; макрос, который вычисляет сумму чисел массива ; макрос принимает ссылку на массив и длину массива %macro Sum 2 mov rax, 0 ; в rax будет результат - сумма элементов массива, по умолчанию 0. mov rdi, %1 ; в rdi ссылка на массив mov rcx, %2 ; в rcx - количество элементов массива jmp condition while: dec rcx add rax, [rdi+rcx * size] condition: cmp rcx, 0 jne while %endmacro _start: Sum arr1, arrlen1 mov rdi, rax Sum arr2, arrlen2 add rdi, rax mov rax, 60 syscall
Макрос Sum вычисляет сумму элементов массива, которая помещается в регистр rax. Адрес массива и его длина передаются в макрос через параметры. Для вычисления макрос использует две метки: while и condition.
В основной части программы для теста два раза вызываем макрос Sum, а в rdi получаем сумму сумм элементов обоих массивов. Но при компиляции мы столкнемся с ошибками:
root@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o hello.asm:30: error: label `while' inconsistently redefined hello.asm:18: ... from macro `Sum' defined here hello.asm:30: info: label `while' originally defined here hello.asm:30: error: label `condition' inconsistently redefined hello.asm:21: ... from macro `Sum' defined here hello.asm:30: info: label `condition' originally defined here
И по ошибкам мы видим, что их суть, что метки дублируются. Но NASM предлагает решение - локальные метки на уровне макросов. Названия таких меток начинаются с двойного знака процента %%. Так, перепишем предыдущую программу:
global _start section .data size equ 8 ; размер каждого элемента в байтах arr1 dq 1, 2, 3, 4 arrlen1 equ ($-arr1)/size arr2 dq 5, 6, 7, 8 arrlen2 equ ($-arr2)/size section .text ; макрос, который вычисляет сумму чисел массива ; макрос принимает ссылку на массив и длину массива %macro Sum 2 mov rax, 0 ; в rax будет результат - сумма элементов массива, по умолчанию 0. mov rdi, %1 ; в rdi ссылка на массив mov rcx, %2 ; в rcx - количество элементов массива jmp %%condition %%while: dec rcx add rax, [rdi+rcx * size] %%condition: cmp rcx, 0 jne %%while %endmacro _start: Sum arr1, arrlen1 mov rdi, rax Sum arr2, arrlen2 add rdi, rax mov rax, 60 syscall
В данном случае метки %%while
и %%condition
являются локальными в макросе. При обработке файла NASM добавляет к названию метки префикс
..@
плюс четырехзначное число. При каждом вызове макроса NASM изменяет подобное число, и таким образом будет генерироваться уникальное имя метки. Например,
метка %%while
может заменяться на ..@1771.while
В приведенном выше примере мы могли бы определить макрос вывода строки в виде отдельной процедуры. И тут может возникнуть вопрос: что выбрать - макросы или функции? Минусом макросов является то, что их код вставляется в каждое место, где они используются. Что ведет к общему увеличению объема программы. С другой стороны, макросы позволяют избежать переходов и сохранений/восстановлений адреса следующей инструкции, что положительно влияет на скорость. Макросы немного быстрее, чем вызовы процедур, потому что вы не выполняете вызов и соответствующие инструкции ret. Кроме того, с макросами сам код становится чуть более читабельным. В общем случае рекомендуется применять макросы для коротких, критичных по времени частей программы. Тогда как функции применяются для более длинных блоков кода и когда время выполнения не так критично.