A Instrução mov e a Cópia de Dados em Assembly NASM
A base de qualquer programa em Assembly é formada por instruções, que são os comandos que definem seu comportamento. A instrução mais fundamental e popular é a mov, responsável pela cópia de dados. De forma análoga ao operador de atribuição (=) em linguagens de programação de alto nível, sua função é copiar um valor de um local para outro. Estima-se que a mov componha entre 25% a 40% de todas as instruções de um programa típico.
Sua sintaxe é simples e direta:
mov destination, sourceA instrução mov opera com dois operandos:
destination(destino): O local onde o dado será armazenado. Pode ser um registrador do processador ou um endereço na memória.source(origem): A fonte do dado a ser copiado. Pode ser um registrador, um endereço de memória ou um valor imediato (um número constante).
Uma regra crucial é que destination e source não podem ser ambos endereços de memória na mesma instrução mov.
Por exemplo, para copiar o número 5 para o registrador rax:
mov rax, 5E para copiar o valor contido em rax para o registrador rbx:
mov rax, 5
mov rbx, rax ; Agora o valor de rbx é igual ao de rax, ou seja, 5Verificando Valores de Forma Simples: O Código de Saída
Para quem está começando, exibir o conteúdo de um registrador na tela pode ser uma tarefa complexa que envolve chamadas de sistema. Uma alternativa mais simples para verificar se nossas operações estão corretas é usar o código de status de saída do programa. Quando um programa termina, ele retorna um número ao sistema operacional. Por convenção, 0 significa sucesso, enquanto outros valores podem indicar erros específicos. Podemos usar esse mecanismo para "exportar" o valor de um registrador e inspecioná-lo.
Exemplo em Linux
No Linux, o código de saída é passado através do registrador rdi antes de chamar a rotina de encerramento do sistema (syscall número 60).
; hello.asm para Linux
global _start ; Torna o rótulo _start visível para o linker
section .text ; Declaração da seção de código executável
_start: ; Ponto de entrada do programa
mov rdi, 23 ; Armazena o valor 23 em rdi para ser o código de saída
mov rax, 60 ; Número da chamada de sistema 'exit' no Linux
syscall ; Pede ao kernel para executar a chamada de sistemaPara compilar e verificar o resultado:
nasm -f elf64 hello.asm -o hello.o ld -o hello hello.o ./hello echo $? 23
O comando echo $? exibe o código de saída do último programa executado, que neste caso é 23.
Exemplo em Windows
No Windows, o código de saída é o valor contido no registrador rax no momento em que o programa termina. O encerramento pode ser feito com a instrução ret.
; hello.asm para Windows
global _start ; Torna o rótulo _start visível para o linker
section .text ; Declaração da seção de código executável
_start: ; Ponto de entrada do programa
mov rax, 23 ; Armazena o valor 23 em rax para ser o código de saída
ret ; Retorna o controle para o sistema operacional, finalizando o programaPara compilar e verificar o resultado no terminal do Windows:
nasm -f win64 hello.asm -o hello.o ld hello.o -o hello.exe hello.exe echo %ERRORLEVEL% 23
O comando echo %ERRORLEVEL% mostra o código de retorno, confirmando o valor que colocamos em rax.
A Regra de Ouro: O Tamanho dos Operandos
A instrução mov exige que os operandos de origem e destino tenham o mesmo tamanho. A arquitetura x86-64 oferece registradores de diferentes tamanhos (64, 32, 16 e 8 bits), e é fundamental respeitar essa correspondência.
- 64 bits:
rax,rbx,rcx, etc. - 32 bits:
eax,ebx,ecx, etc. (correspondem à metade inferior dos registradores de 64 bits) - 16 bits:
ax,bx,cx, etc. - 8 bits:
al,ah,bl,bh, etc.
Operações como as abaixo são válidas, pois os tamanhos coincidem:
mov ecx, 525 ; O valor 525 cabe em um registrador de 32 bits (ecx)
mov eax, ecx ; Válido: copia de um registrador de 32 bits para outroNo entanto, o código a seguir apresenta problemas:
mov cl, 525 ; Inválido: tenta mover 525 para um registrador de 8 bits (cl)
mov eax, cl ; Inválido: tamanhos diferentes (32 bits e 8 bits)Na primeira linha, o registrador cl tem 8 bits e só pode armazenar valores de 0 a 255. Tentar inserir 525 resultará em um aviso de overflow (warning: byte data exceeds bounds) durante a compilação. O NASM truncará o valor, armazenando apenas os 8 bits menos significativos (525 % 256 = 13), o que leva a um erro lógico no programa.
A segunda linha causará um erro de compilação (error: invalid combination of opcode and operands) porque os tamanhos dos operandos (eax de 32 bits e cl de 8 bits) são incompatíveis.
Superando a Limitação de Tamanho: movsx e movzx
E se for necessário mover um valor menor para um registrador maior? Para isso, existem duas instruções especiais que, além de copiar, estendem o valor para o tamanho do destino. A escolha entre elas depende se o número é tratado como um valor com ou sem sinal.
movsx (Move with Sign-Extend)
Esta instrução é usada para números com sinal. Ela copia o valor e preenche o espaço extra no destino repetindo o bit de sinal (o bit mais à esquerda) da origem. Isso preserva o valor numérico do dado (seja ele positivo ou negativo).
mov al, -1 ; Em 8 bits, -1 é 11111111 em binário. O bit de sinal é 1.
movsx rdi, al ; Copia 'al' para 'rdi' e estende o bit de sinal (1).
; 'rdi' se tornará 0xFFFFFFFFFFFFFFFF, que é -1 em 64 bits.movzx (Move with Zero-Extend)
Esta instrução é usada para números sem sinal. Ela copia o valor e preenche o espaço extra no destino com zeros.
mov al, 255 ; Em 8 bits, 255 é 11111111 em binário.
movzx rdi, al ; Copia 'al' para 'rdi' e estende com zeros.
; 'rdi' se tornará 0x00000000000000FF, que é 255 em 64 bits.Uma característica importante da arquitetura x86-64 é que qualquer operação que modifica um registrador de 32 bits (eax, ebx, etc.) automaticamente zera os 32 bits superiores do seu registrador de 64 bits correspondente (rax, rbx, etc.). Isso torna a instrução mov uma forma eficiente de fazer uma extensão com zeros de 32 para 64 bits.
mov rax, -1 ; rax agora é 0xFFFFFFFFFFFFFFFF
mov eax, eax ; Esta operação em 'eax' zera os 32 bits superiores de 'rax'.
; O resultado é que 'rax' se torna 0x00000000FFFFFFFF.Formatos Numéricos no NASM
Por padrão, o NASM interpreta números como decimais. No entanto, é possível especificar outros sistemas de numeração:
Binário: com o prefixo
0bou o sufixob.mov rdi, 0b1011 ; 11 em decimal mov rdi, 1011b ; Também 11 em decimalHexadecimal: com o prefixo
0xou o sufixoh.mov rdi, 0x1F ; 31 em decimal mov rdi, 1Fh ; Também 31 em decimalImportante: Se um número hexadecimal com sufixo
hcomeça com uma letra (A-F), é obrigatório adicionar um0no início (ex:0Fh) para que o assembler não o confunda com um nome de variável ou rótulo.
Resumo
- A instrução
mové a operação fundamental para copiar dados entre registradores e memória. - Os operandos de
movdevem sempre ter o mesmo tamanho para evitar erros de compilação. - Os códigos de saída do programa são uma técnica útil para inspecionar o conteúdo de registradores de forma simples.
- Para mover dados para registradores maiores, as instruções
movsx(com extensão de sinal) emovzx(com extensão de zero) são utilizadas. - O NASM suporta números em formato decimal, binário (
0b...ou...b) e hexadecimal (0x...ou...h).