Макросы

Последнее обновление: 16.10.2023

Макросы наряду с функциями представляют механизм выделения функционала в отдельные блоки. Макрос фактически также представляет метку, с которой связан некоторый код. Когда 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. Кроме того, с макросами сам код становится чуть более читабельным. В общем случае рекомендуется применять макросы для коротких, критичных по времени частей программы. Тогда как функции применяются для более длинных блоков кода и когда время выполнения не так критично.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850