Aprendendo a usar o pré-processador do NASM
O NASM tem um pré-processador de código baseado no pré-processador da linguagem C. O que ele faz basicamente é interpretar instruções específicas do pré-processador para gerar o código fonte final, que será de fato compilado pelo NASM para o código de máquina. É por isso que tem o nome de pré-processador, já que ele processa o código fonte antes do NASM compilar o código.
As diretivas interpretadas pelo pré-processador são prefixadas pelo símbolo %
e dão um poder absurdo para a programação diretamente em Assembly no nasm. Abaixo irei listar as mais básicas e o seu uso.
Assim como a diretiva #define
do C, essa diretiva é usada para definir macros de uma única linha. Seja lá aonde o nome do macro for citado no código fonte ele expandirá para exatamente o conteúdo que você definiu para ele como se você estivesse fazendo uma cópia.
E assim como no C é possível passar argumentos para um macro usando de uma sintaxe muito parecida com uma função. Exemplo de uso:
As linhas 2 e 3 irão expandir para a instrução mov eax, 31
como se tivesse feito uma cópia do valor definido para o macro. Podemos também é claro escrever um macro como parte de uma instrução, por exemplo:
Isso irá expandir a instrução na linha 2 para mov eax, [ebx*2 + 4]
A diferença entre definir um macro dessa forma e definir uma constante é que a constante recebe uma expressão matemática e expande para o valor do resultado. Enquanto o macro expande para qualquer coisa que você definir para ele.
O outro uso do macro, que é mais poderoso, é passando argumentos para ele assim como se é possível fazer em C. Para isso basta definir o nome do macro seguido dos parênteses e, dentro dos parênteses, os nomes dos argumentos que queremos receber separados por vírgula.
No valor definido para o macro os nomes desses argumentos irão expandir para qualquer conteúdo que você passe como argumento na hora que chamar um macro. Veja por exemplo o mesmo macro acima porém desta vez dando a possibilidade de escolher o registrador:
A linha 2 irá expandir para: mov eax, [ebx*2 + 4]
.
A linha 3 irá expandir para: mov eax, [esi*2 + 4]
.
Simplesmente apaga um macro anteriormente declarado por %define
.
Além dos macros de uma única linha existem também os macros de múltiplas linhas que podem ser definidos no NASM.
Após a especificação do nome que queremos dar ao macro podemos especificar o número de argumentos passados para ele. Caso não queira receber argumentos no macro basta definir esse valor para zero. Exemplo:
O %endmacro
sinaliza o final do macro e todas as instruções inseridas entre as duas diretivas serão expandidas quando o macro for citado.
Para usar argumentos com um macro de múltiplas linhas difere de um macro definido com %define
, ao invés do uso de parênteses o macro recebe argumentos seguindo a mesma sintaxe de uma instrução e separando cada um dos argumentos por vírgula. Para usar o argumento dentro do macro basta usar %n
, onde n seria o número do argumento que começa contando em 1.
Também é possível fazer com que o último argumento do macro expanda para todo o conteúdo passado, mesmo que contenha vírgula. Para isso basta adicionar um +
ao número de argumentos. Por exemplo:
A linha 6 expandiria para as instruções:
Enquanto a linha 7 iria acusar erro já que na linha 3 dentro do macro a instrução expandiu para mov esi, edi, edx
, o que está errado.
É possível declarar mais de um macro com o mesmo nome desde que cada um deles tenham um número diferente de argumentos recebidos. O exemplo abaixo é totalmente válido:
Usar um rótulo dentro de um macro é problemático porque se o macro for usado mais de uma vez estaremos redefinindo o mesmo rótulo já que seu nome nunca muda.
Para não ter esse problema existem os rótulos locais de um macro que será expandido para um nome diferente, definido pelo NASM, a cada uso do macro. A sintaxe é simples, basta prefixar o nome do rótulo com %%
. Exemplo:
Apaga um macro anteriormente definido com %macro
. O número de argumentos especificado deve ser o mesmo utilizado na hora de declarar o macro.
Assim como o pré-processador do C, o NASM também suporta diretivas de código condicional. A sintaxe básica é:
Onde o código dentro da diretiva %if
só é compilado se a condição for atendida. Caso não seja é possível usar a diretiva %elif
para fazer o teste de uma nova condição. Enquanto o código na diretiva %else
é expandido caso nenhuma das condições anteriormente testadas sejam atendidas. Por fim é usado a diretiva %endif
para indicar o fim da diretiva %if
.
É possível passar para %if
e %elif
uma expressão matemática afim de testar o resultado de um cálculo com uma constante ou algo semelhante. Se o valor for diferente de zero a expressão será considerada verdadeira e o bloco de código será expandido no código de saída.
Também é possível inverter a lógica das instruções adicionando um 'n', fazendo com que o bloco seja expandido caso a condição não seja atendida. Exemplo:
Além do %if
básico também podemos usar variantes que verificam por uma condição específica ao invés de receber uma expressão e testar seu resultado.
Essas diretivas verificam se um macro de linha única foi declarado por um %define
anteriormente. É possível também usar essas diretivas em forma de negação adicionando o 'n' após o 'if'. Ficando: %ifndef
e %elifndef
, respectivamente.
Mesmo que %ifdef
porém para macros de múltiplas linhas declarados por %macro
. E da mesma que as diretivas anteriores também têm suas versões em negação: %ifnmacro
e %elifnmacro
.
Usando diretivas condicionais as vezes queremos acusar um erro ou emitir um alerta no console para indicar alguma mensagem no processo de compilação de algum projeto.
%error
imprime a mensagem como um erro e finaliza a compilação, enquanto %warning
emite a mensagem como um alerta e a compilação continua normalmente. Podemos por exemplo acusar um erro caso um determinado macro necessário para o código não esteja definido:
Essa diretiva tem o uso parecido com a diretiva #include
da linguagem C e ela faz exatamente a mesma coisa: Copia o conteúdo do arquivo passado como argumento para o exato local aonde ela foi utilizada no arquivo fonte. Seria como você manualmente abrir o arquivo, copiar todo o conteúdo dele e depois colar no código fonte.
Assim como fazemos em um header file incluído por #include
na linguagem C é importante usar as diretivas condicionais para evitar a inclusão duplicada de um mesmo arquivo. Por exemplo:
Dessa forma quando incluirmos o arquivo pela primeira vez o macro _ARQUIVO_ASM
será declarado. Se ele for incluído mais uma vez o macro já estará declarado e o %ifndef
da linha 1 terá uma condição falsa e portanto não expandirá o conteúdo dentro de sua diretiva.
É importante fazer isso para evitar a redeclaração de macros, constantes ou rótulos. Bem como também evita que o mesmo código fique duplicado.