Macros no Pré-processador do Assembly NASM
Assim como as funções, as macros são um mecanismo para organizar a funcionalidade em blocos de código reutilizáveis. Uma macro é essencialmente um rótulo associado a um trecho de código. Quando o pré-processador do NASM encontra o nome de uma macro no código fonte, ele o substitui pelo código contido dentro da macro. Esse processo é chamado de expansão de macro.
As macros são ideais para substituir sequências de código longas e repetitivas por chamadas curtas e legíveis.
A definição de uma macro tem a seguinte forma geral:
%macro nome_da_macro numero_de_parametros
; código da macro
%endmacroA definição começa com a diretiva %macro, seguida pelo nome da macro e o número de parâmetros que ela aceita. O bloco de código é delimitado pela diretiva de fechamento %endmacro.
Macro Simples sem Parâmetros
Vamos considerar um exemplo de macro que imprime uma string no console em um programa para Linux.
global _start
section .data
msg db "Hello programicio.com", 10
msglen equ $-msg
section .text
; Macro que imprime uma string fixa no console
%macro Print 0
mov rax, 1 ; Chamada de sistema (write)
mov rdi, 1 ; Descritor de arquivo (stdout)
mov rsi, msg ; Endereço da string
mov rdx, msglen ; Comprimento da string
syscall
%endmacro
_start:
Print ; Expande a macro Print aqui
mov rax, 60 ; Chamada de sistema (exit)
xor rdi, rdi ; Código de saída 0
syscallNeste caso, a macro se chama Print e não aceita parâmetros, indicado pelo 0. Para usar a macro, basta escrever seu nome. Após o pré-processamento, o código que o montador irá compilar será:
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, msglen
syscall
mov rax, 60
xor rdi, rdi
syscallO código da macro é inserido diretamente no local da chamada. Uma grande vantagem é que podemos chamar a macro múltiplas vezes para repetir o código.
_start:
Print ; Primeira impressão
Print ; Segunda impressão
Print ; Terceira impressão
; Finaliza o programa corretamente
mov rax, 60 ; Chamada de sistema (exit)
xor rdi, rdi ; Código de saída 0
syscallIsso resultará na expansão do código da macro três vezes, imprimindo a mensagem "Hello programicio.com" três vezes no console.
A seguir, um exemplo análogo de macro sem parâmetros para Windows.
global _start
extern WriteFile
extern GetStdHandle
section .data
msg db "Hello programicio.com", 10
msglen equ $-msg
section .text
; Macro para imprimir uma string fixa no console (Windows)
%macro Print 0
sub rsp, 40
mov rcx, -11
call GetStdHandle
mov rcx, rax
mov rdx, msg
mov r8d, msglen
xor r9, r9
mov qword [rsp + 32], 0
call WriteFile
add rsp, 40
%endmacro
_start:
Print
retMacros com Parâmetros
Macros se tornam muito mais flexíveis quando aceitam parâmetros. Isso permite que o mesmo bloco de código opere sobre dados diferentes. Vamos modificar a macro Print para que ela possa imprimir qualquer string.
global _start
section .data
msg1 db "Hello programicio.com", 10
msglen1 equ $-msg1
msg2 db "Hello World", 10
msglen2 equ $-msg2
section .text
; Macro que imprime uma string passada por parâmetro
%macro Print 2
mov rax, 1
mov rdi, 1
mov rsi, %1 ; %1 representa o primeiro parâmetro
mov rdx, %2 ; %2 representa o segundo parâmetro
syscall
%endmacro
_start:
Print msg1, msglen1 ; Passa o endereço de msg1 e seu tamanho
Print msg2, msglen2 ; Passa o endereço de msg2 e seu tamanho
mov rax, 60
xor rdi, rdi
syscallAgora, a macro Print é definida para aceitar 2 parâmetros. Dentro da macro, esses parâmetros são acessados pelos placeholders %1, %2, e assim por diante. Ao chamar a macro, os valores (argumentos) são passados posicionalmente: msg1 é associado a %1, e msglen1 a %2.
Este programa imprimirá as duas strings diferentes no console.
$ nasm -f elf64 hello.asm -o hello.o $ ld hello.o -o hello $ ./hello Hello programicio.com Hello World
Rótulos em Macros
Macros podem conter rótulos, mas isso apresenta um problema: se a macro for expandida várias vezes, o mesmo rótulo será definido múltiplas vezes, causando um erro de "redefinição de rótulo" durante a montagem.
; Exemplo com erro
%macro Sum 2
; ...
jmp condition
while:
; ...
condition:
cmp rcx, 0
jne while
%endmacro
_start:
Sum arr1, arrlen1
Sum arr2, arrlen2 ; ERRO: 'while' e 'condition' serão redefinidosPara resolver isso, o NASM oferece rótulos locais de macro, que são prefixados com %%. O NASM garante que cada expansão da macro gere um nome de rótulo único.
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 para somar os elementos de um array
%macro Sum 2
mov rax, 0
mov rdi, %1
mov rcx, %2
jmp %%condition ; Salta para o rótulo local
%%while: ; Definição do rótulo local
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
syscallAgora, os rótulos %%while e %%condition são locais para cada expansão da macro. O NASM os converterá internamente para nomes únicos (como ..@1771.while), evitando conflitos.
Macros ou Funções?
A escolha entre usar uma macro ou uma função (procedimento) envolve um trade-off:
Macros:
- Vantagem: Mais rápidas. O código é inserido diretamente (inlining), evitando a sobrecarga de uma chamada de função (
call/ret). O código pode se tornar mais legível. - Desvantagem: Aumentam o tamanho do executável final, pois o código é duplicado em cada local de chamada.
- Vantagem: Mais rápidas. O código é inserido diretamente (inlining), evitando a sobrecarga de uma chamada de função (
Funções:
- Vantagem: Reduzem o tamanho do executável, pois o código existe em um único local.
- Desvantagem: Um pouco mais lentas devido à sobrecarga das instruções
callerete à necessidade de gerenciar a pilha.
Recomendação geral: use macros para trechos de código curtos e críticos em termos de desempenho. Use funções para blocos de código mais longos ou quando o tamanho do programa é uma preocupação maior que a velocidade.
Resumo
- Macros são blocos de código reutilizáveis que são expandidos pelo pré-processador diretamente no local da chamada.
- Elas podem ser definidas sem parâmetros ou com múltiplos parâmetros, acessados por
%1,%2, etc. - Para evitar conflitos de nomes, rótulos dentro de macros devem ser locais, prefixados com
%%. - Macros oferecem melhor desempenho que funções ao custo de um executável maior.