Montagem - Guia Rápido

O que é linguagem Assembly?

Cada computador pessoal possui um microprocessador que gerencia as atividades aritméticas, lógicas e de controle do computador.

Cada família de processadores tem seu próprio conjunto de instruções para lidar com várias operações, como obter dados do teclado, exibir informações na tela e realizar vários outros trabalhos. Esse conjunto de instruções é chamado de 'instruções em linguagem de máquina'.

Um processador entende apenas instruções em linguagem de máquina, que são cadeias de 1's e 0's. No entanto, a linguagem de máquina é muito obscura e complexa para ser usada no desenvolvimento de software. Portanto, a linguagem assembly de baixo nível é projetada para uma família específica de processadores que representa várias instruções em código simbólico e de uma forma mais compreensível.

Vantagens da linguagem assembly

Ter uma compreensão da linguagem assembly torna-o ciente de -

  • Como os programas fazem interface com o sistema operacional, processador e BIOS;
  • Como os dados são representados na memória e em outros dispositivos externos;
  • Como o processador acessa e executa a instrução;
  • Como as instruções acessam e processam os dados;
  • Como um programa acessa dispositivos externos.

Outras vantagens de usar a linguagem assembly são -

  • Requer menos memória e tempo de execução;

  • Ele permite trabalhos complexos específicos de hardware de uma maneira mais fácil;

  • É adequado para trabalhos urgentes;

  • É mais adequado para escrever rotinas de serviço de interrupção e outros programas residentes na memória.

Recursos básicos do hardware do PC

O hardware interno principal de um PC consiste em processador, memória e registros. Os registros são componentes do processador que armazenam dados e endereços. Para executar um programa, o sistema o copia do dispositivo externo para a memória interna. O processador executa as instruções do programa.

A unidade fundamental de armazenamento do computador é um pouco; pode ser ON (1) ou OFF (0) e um grupo de 8 bits relacionados cria um byte na maioria dos computadores modernos.

Portanto, o bit de paridade é usado para tornar ímpar o número de bits em um byte. Se a paridade for uniforme, o sistema presume que houve um erro de paridade (embora raro), que pode ter sido causado devido a falha de hardware ou distúrbio elétrico.

O processador suporta os seguintes tamanhos de dados -

  • Word: um item de dados de 2 bytes
  • Palavra dupla: um item de dados de 4 bytes (32 bits)
  • Quadword: um item de dados de 8 bytes (64 bits)
  • Parágrafo: uma área de 16 bytes (128 bits)
  • Quilobyte: 1024 bytes
  • Megabyte: 1.048.576 bytes

Sistema de número binário

Todo sistema numérico usa notação posicional, ou seja, cada posição em que um dígito é escrito tem um valor posicional diferente. Cada posição é a potência da base, que é 2 para o sistema numérico binário, e essas potências começam em 0 e aumentam em 1.

A tabela a seguir mostra os valores posicionais para um número binário de 8 bits, onde todos os bits são definidos como ON.

Valor de bit 1 1 1 1 1 1 1 1
Valor da posição como uma potência de base 2 128 64 32 16 8 4 2 1
Número de bits 7 6 5 4 3 2 1 0

O valor de um número binário é baseado na presença de 1 bits e seu valor posicional. Portanto, o valor de um determinado número binário é -

1 + 2 + 4 + 8 +16 + 32 + 64 + 128 = 255

que é o mesmo que 2 8 - 1.

Sistema numérico hexadecimal

O sistema numérico hexadecimal usa a base 16. Os dígitos neste sistema variam de 0 a 15. Por convenção, as letras de A a F são usadas para representar os dígitos hexadecimais correspondentes aos valores decimais de 10 a 15.

Os números hexadecimais na computação são usados ​​para abreviar representações binárias extensas. Basicamente, o sistema numérico hexadecimal representa um dado binário dividindo cada byte pela metade e expressando o valor de cada meio byte. A tabela a seguir fornece os equivalentes decimais, binários e hexadecimais -

Número decimal Representação binária Representação hexadecimal
0 0 0
1 1 1
2 10 2
3 11 3
4 100 4
5 101 5
6 110 6
7 111 7
8 1000 8
9 1001 9
10 1010 UMA
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Para converter um número binário em seu equivalente hexadecimal, divida-o em grupos de 4 grupos consecutivos cada, começando da direita, e escreva esses grupos sobre os dígitos correspondentes do número hexadecimal.

Example - O número binário 1000 1100 1101 0001 é equivalente a hexadecimal - 8CD1

Para converter um número hexadecimal em binário, basta escrever cada dígito hexadecimal em seu equivalente binário de 4 dígitos.

Example - O número hexadecimal FAD8 é equivalente ao binário - 1111 1010 1101 1000

Aritmética Binária

A tabela a seguir ilustra quatro regras simples para adição binária -

(Eu) (ii) (iii) (iv)
1
0 1 1 1
+0 +0 +1 +1
= 0 = 1 = 10 = 11

As regras (iii) e (iv) mostram um transporte de 1 bit para a próxima posição à esquerda.

Example

Decimal Binário
60 00111100
+42 00101010
102 01100110

Um valor binário negativo é expresso em two's complement notation. De acordo com essa regra, converter um número binário em seu valor negativo é reverter seus valores de bit e adicionar 1 .

Example

Número 53 00110101
Inverta os bits 11001010
Adicionar 1 0000000 1
Número -53 11001011

Para subtrair um valor de outro, converta o número que está sendo subtraído para o formato de complemento de dois e some os números .

Example

Subtraia 42 de 53

Número 53 00110101
Número 42 00101010
Inverta os bits de 42 11010101
Adicionar 1 0000000 1
Número -42 11010110
53 - 42 = 11 00001011

O estouro do último 1 bit é perdido.

Endereçando dados na memória

O processo pelo qual o processador controla a execução das instruções é conhecido como o fetch-decode-execute cycle ou o execution cycle. Consiste em três etapas contínuas -

  • Buscando a instrução da memória
  • Decodificando ou identificando a instrução
  • Executando a instrução

O processador pode acessar um ou mais bytes de memória por vez. Vamos considerar um número hexadecimal 0725H. Este número exigirá dois bytes de memória. O byte de ordem superior ou byte mais significativo é 07 e o byte de ordem inferior é 25.

O processador armazena dados em seqüência de byte reverso, ou seja, um byte de ordem inferior é armazenado em um endereço de memória baixa e um byte de ordem alta no endereço de memória alta. Portanto, se o processador traz o valor 0725H do registro para a memória, ele irá transferir 25 primeiro para o endereço de memória inferior e 07 para o próximo endereço de memória.

x: endereço de memória

Quando o processador obtém os dados numéricos da memória para registrar, ele reverte os bytes novamente. Existem dois tipos de endereços de memória -

  • Endereço absoluto - uma referência direta do local específico.

  • Endereço do segmento (ou deslocamento) - endereço inicial de um segmento de memória com o valor do deslocamento.

Configuração de ambiente local

A linguagem assembly depende do conjunto de instruções e da arquitetura do processador. Neste tutorial, nos concentramos em processadores Intel-32 como o Pentium. Para seguir este tutorial, você precisará de -

  • Um IBM PC ou qualquer computador compatível equivalente
  • Uma cópia do sistema operacional Linux
  • Uma cópia do programa NASM assembler

Existem muitos programas bons em assembler, como -

  • Microsoft Assembler (MASM)
  • Borland Turbo Assembler (TASM)
  • O GNU assembler (GAS)

Usaremos o montador NASM, como ele é -

  • Livre. Você pode baixá-lo de várias fontes da web.
  • Bem documentado e você obterá muitas informações na rede.
  • Pode ser usado em Linux e Windows.

Instalando NASM

Se você selecionar "Ferramentas de Desenvolvimento" durante a instalação do Linux, poderá obter o NASM instalado junto com o sistema operacional Linux e não precisará fazer o download e instalá-lo separadamente. Para verificar se você já tem NASM instalado, execute as seguintes etapas -

  • Abra um terminal Linux.

  • Tipo whereis nasm e pressione ENTER.

  • Se já estiver instalado, uma linha como nasm: / usr / bin / nasm aparecerá. Caso contrário, você verá apenas nasm:, então você precisa instalar o NASM.

Para instalar o NASM, execute as seguintes etapas -

  • Verifique o site do The netwide assembler (NASM) para obter a versão mais recente.

  • Baixe o arquivo de origem do Linux nasm-X.XX.ta.gz, onde X.XXé o número da versão NASM no arquivo.

  • Descompacte o arquivo em um diretório que crie um subdiretório nasm-X. XX.

  • cd para nasm-X.XXe digite./configure. Este script de shell encontrará o melhor compilador C para usar e configurar os Makefiles de acordo.

  • Tipo make para construir os binários nasm e ndisasm.

  • Tipo make install para instalar o nasm e ndisasm em / usr / local / bin e para instalar as páginas de manual.

Isso deve instalar o NASM em seu sistema. Alternativamente, você pode usar uma distribuição RPM para o Fedora Linux. Esta versão é mais simples de instalar, basta clicar duas vezes no arquivo RPM.

Um programa de montagem pode ser dividido em três seções -

  • o data seção,

  • o bss seção, e

  • o text seção.

A seção de dados

o dataseção é usada para declarar dados ou constantes inicializados. Esses dados não mudam em tempo de execução. Você pode declarar vários valores constantes, nomes de arquivo ou tamanho do buffer, etc., nesta seção.

A sintaxe para declarar a seção de dados é -

section.data

A seção bss

o bssseção é usada para declarar variáveis. A sintaxe para declarar a seção bss é -

section.bss

A seção de texto

o textseção é usada para manter o código real. Esta seção deve começar com a declaraçãoglobal _start, que informa ao kernel onde a execução do programa começa.

A sintaxe para declarar a seção de texto é -

section.text
   global _start
_start:

Comentários

O comentário da linguagem assembly começa com um ponto e vírgula (;). Ele pode conter qualquer caractere imprimível, incluindo espaços em branco. Ele pode aparecer em uma linha sozinho, como -

; This program displays a message on screen

ou, na mesma linha junto com uma instrução, como -

add eax, ebx     ; adds ebx to eax

Declarações da linguagem assembly

Os programas de linguagem assembly consistem em três tipos de declarações -

  • Instruções executáveis ​​ou instruções,
  • Diretivas Assembler ou pseudo-ops, e
  • Macros.

o executable instructions ou simplesmente instructionsdiga ao processador o que fazer. Cada instrução consiste em umoperation code(Código de operação). Cada instrução executável gera uma instrução em linguagem de máquina.

o assembler directives ou pseudo-opsdiga ao montador sobre os vários aspectos do processo de montagem. Eles não são executáveis ​​e não geram instruções em linguagem de máquina.

Macros são basicamente um mecanismo de substituição de texto.

Sintaxe de declarações da linguagem Assembly

As instruções da linguagem assembly são inseridas uma instrução por linha. Cada declaração segue o seguinte formato -

[label]   mnemonic   [operands]   [;comment]

Os campos entre colchetes são opcionais. Uma instrução básica possui duas partes, a primeira é o nome da instrução (ou o mnemônico), que deve ser executada, e a segunda são os operandos ou parâmetros do comando.

A seguir estão alguns exemplos de declarações típicas de linguagem assembly -

INC COUNT        ; Increment the memory variable COUNT

MOV TOTAL, 48    ; Transfer the value 48 in the 
                 ; memory variable TOTAL
					  
ADD AH, BH       ; Add the content of the 
                 ; BH register into the AH register
					  
AND MASK1, 128   ; Perform AND operation on the 
                 ; variable MASK1 and 128
					  
ADD MARKS, 10    ; Add 10 to the variable MARKS
MOV AL, 10       ; Transfer the value 10 to the AL register

O Programa Hello World em Assembleia

O seguinte código de linguagem assembly exibe a string 'Hello World' na tela -

section	.text
   global _start     ;must be declared for linker (ld)
	
_start:	            ;tells linker entry point
   mov	edx,len     ;message length
   mov	ecx,msg     ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	eax,1       ;system call number (sys_exit)
   int	0x80        ;call kernel

section	.data
msg db 'Hello, world!', 0xa  ;string to be printed
len equ $ - msg     ;length of the string

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Hello, world!

Compilando e vinculando um programa de montagem em NASM

Certifique-se de ter definido o caminho de nasm e ldbinários em sua variável de ambiente PATH. Agora, execute as seguintes etapas para compilar e vincular o programa acima -

  • Digite o código acima usando um editor de texto e salve-o como hello.asm.

  • Certifique-se de que você está no mesmo diretório onde salvou hello.asm.

  • Para montar o programa, digite nasm -f elf hello.asm

  • Se houver algum erro, você será avisado sobre isso nesta fase. Caso contrário, um arquivo de objeto do seu programa chamadohello.o Será criado.

  • Para vincular o arquivo objeto e criar um arquivo executável chamado hello, digite ld -m elf_i386 -s -o hello hello.o

  • Execute o programa digitando ./hello

Se você fez tudo corretamente, será exibido 'Olá, mundo!' na tela.

Já discutimos as três seções de um programa de montagem. Essas seções também representam vários segmentos de memória.

Curiosamente, se você substituir a palavra-chave da seção por segmento, obterá o mesmo resultado. Experimente o seguinte código -

segment .text	   ;code segment
   global _start    ;must be declared for linker 
	
_start:	           ;tell linker entry point
   mov edx,len	   ;message length
   mov ecx,msg     ;message to write
   mov ebx,1	   ;file descriptor (stdout)
   mov eax,4	   ;system call number (sys_write)
   int 0x80	   ;call kernel

   mov eax,1       ;system call number (sys_exit)
   int 0x80	   ;call kernel

segment .data      ;data segment
msg	db 'Hello, world!',0xa   ;our dear string
len	equ	$ - msg          ;length of our dear string

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Hello, world!

Segmentos de Memória

Um modelo de memória segmentada divide a memória do sistema em grupos de segmentos independentes referenciados por ponteiros localizados nos registradores de segmento. Cada segmento é usado para conter um tipo específico de dados. Um segmento é usado para conter códigos de instrução, outro segmento armazena os elementos de dados e um terceiro segmento mantém a pilha do programa.

À luz da discussão acima, podemos especificar vários segmentos de memória como -

  • Data segment - É representado por .data seção e o .bss. A seção .data é usada para declarar a região da memória, onde os elementos de dados são armazenados para o programa. Esta seção não pode ser expandida depois que os elementos de dados são declarados e permanece estática em todo o programa.

    A seção .bss também é uma seção de memória estática que contém buffers para os dados a serem declarados posteriormente no programa. Esta memória buffer é preenchida com zeros.

  • Code segment - É representado por .textseção. Isso define uma área na memória que armazena os códigos de instrução. Esta também é uma área fixa.

  • Stack - Este segmento contém valores de dados passados ​​para funções e procedimentos dentro do programa.

As operações do processador envolvem principalmente o processamento de dados. Esses dados podem ser armazenados na memória e acessados ​​a partir dela. No entanto, ler e armazenar dados na memória retarda o processador, pois envolve processos complicados de enviar a solicitação de dados pelo barramento de controle e para a unidade de armazenamento de memória e obter os dados pelo mesmo canal.

Para acelerar as operações do processador, o processador inclui alguns locais de armazenamento de memória interna, chamados registers.

Os registradores armazenam elementos de dados para processamento sem ter que acessar a memória. Um número limitado de registros é integrado ao chip do processador.

Registros do processador

Existem dez registros de processador de 32 bits e seis de 16 bits na arquitetura IA-32. Os registros são agrupados em três categorias -

  • Registros Gerais,
  • Registros de controle e
  • Registros de segmento.

Os registros gerais são divididos nos seguintes grupos -

  • Registros de dados,
  • O ponteiro registra e
  • Registradores de índice.

Registros de dados

Quatro registradores de dados de 32 bits são usados ​​para operações aritméticas, lógicas e outras. Esses registros de 32 bits podem ser usados ​​de três maneiras -

  • Como registradores de dados completos de 32 bits: EAX, EBX, ECX, EDX.

  • As metades inferiores dos registros de 32 bits podem ser usadas como quatro registros de dados de 16 bits: AX, BX, CX e DX.

  • As metades inferior e superior dos quatro registros de 16 bits mencionados acima podem ser usadas como oito registros de dados de 8 bits: AH, AL, BH, BL, CH, CL, DH e DL.

Alguns desses registros de dados têm uso específico em operações aritméticas.

AX is the primary accumulator; ele é usado na entrada / saída e na maioria das instruções aritméticas. Por exemplo, na operação de multiplicação, um operando é armazenado no registro EAX ou AX ou AL de acordo com o tamanho do operando.

BX is known as the base register, pois poderia ser usado no endereçamento indexado.

CX is known as the count register, como o ECX, os registradores CX armazenam a contagem do loop em operações iterativas.

DX is known as the data register. Ele também é usado em operações de entrada / saída. Ele também é usado com o registro AX junto com DX para operações de multiplicação e divisão envolvendo grandes valores.

Pointer Registers

Os registros de ponteiro são registros EIP, ESP e EBP de 32 bits e as partes direitas de 16 bits correspondentes IP, SP e BP. Existem três categorias de registradores de ponteiro -

  • Instruction Pointer (IP)- O registro IP de 16 bits armazena o endereço de deslocamento da próxima instrução a ser executada. IP em associação com o registro CS (como CS: IP) fornece o endereço completo da instrução atual no segmento de código.

  • Stack Pointer (SP)- O registro SP de 16 bits fornece o valor de deslocamento dentro da pilha do programa. SP em associação com o registro SS (SS: SP) refere-se à posição atual dos dados ou endereço na pilha do programa.

  • Base Pointer (BP)- O registro BP de 16 bits ajuda principalmente a referenciar as variáveis ​​de parâmetro passadas para uma sub-rotina. O endereço no registro SS é combinado com o deslocamento no BP para obter a localização do parâmetro. BP também pode ser combinado com DI e SI como base registradora para endereçamento especial.

Registros de índice

Os registradores de índice de 32 bits, ESI e EDI, e suas partes mais à direita de 16 bits. SI e DI são usados ​​para endereçamento indexado e às vezes usados ​​em adição e subtração. Existem dois conjuntos de indicadores de índice -

  • Source Index (SI) - É usado como índice de origem para operações de string.

  • Destination Index (DI) - É usado como índice de destino para operações de string.

Registros de controle

O registrador de ponteiro de instrução de 32 bits e o registrador de sinalizadores de 32 bits combinados são considerados os registradores de controle.

Muitas instruções envolvem comparações e cálculos matemáticos e mudam o status dos sinalizadores e algumas outras instruções condicionais testam o valor desses sinalizadores de status para levar o fluxo de controle para outro local.

Os bits de sinalização comuns são:

  • Overflow Flag (OF) - Indica o estouro de um bit de ordem superior (bit mais à esquerda) de dados após uma operação aritmética assinada.

  • Direction Flag (DF)- Determina a direção esquerda ou direita para mover ou comparar os dados da string. Quando o valor DF é 0, a operação da string segue a direção da esquerda para a direita e quando o valor é definido como 1, a operação da string segue a direção da direita para a esquerda.

  • Interrupt Flag (IF)- Determina se as interrupções externas como entrada de teclado, etc., devem ser ignoradas ou processadas. Desabilita a interrupção externa quando o valor é 0 e habilita interrupções quando ajustado para 1.

  • Trap Flag (TF)- Permite configurar o funcionamento do processador em modo de passo único. O programa DEBUG que usamos define o sinalizador de trap, para que pudéssemos avançar na execução de uma instrução por vez.

  • Sign Flag (SF)- Mostra o sinal do resultado de uma operação aritmética. Este sinalizador é definido de acordo com o sinal de um item de dados após a operação aritmética. O sinal é indicado pela ordem superior do bit mais à esquerda. Um resultado positivo limpa o valor de SF para 0 e um resultado negativo define para 1.

  • Zero Flag (ZF)- Indica o resultado de uma operação aritmética ou de comparação. Um resultado diferente de zero limpa o sinalizador zero para 0 e um resultado zero define-o como 1.

  • Auxiliary Carry Flag (AF)- Contém o transporte do bit 3 para o bit 4 após uma operação aritmética; usado para aritmética especializada. O AF é definido quando uma operação aritmética de 1 byte causa um transporte do bit 3 para o bit 4.

  • Parity Flag (PF)- Indica o número total de bits 1 no resultado obtido em uma operação aritmética. Um número par de bits 1 limpa o sinalizador de paridade para 0 e um número ímpar de bits 1 define o sinalizador de paridade para 1.

  • Carry Flag (CF)- Contém o transporte de 0 ou 1 de um bit de ordem superior (mais à esquerda) após uma operação aritmética. Ele também armazena o conteúdo do último bit de uma operação de deslocamento ou rotação .

A tabela a seguir indica a posição dos bits de sinalização no registro Sinalizadores de 16 bits:

Bandeira: O D Eu T S Z UMA P C
Bit não: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

Registros de segmento

Os segmentos são áreas específicas definidas em um programa para conter dados, código e pilha. Existem três segmentos principais -

  • Code Segment- Contém todas as instruções a serem executadas. Um registro de segmento de código de 16 bits ou registro CS armazena o endereço inicial do segmento de código.

  • Data Segment- Contém dados, constantes e áreas de trabalho. Um registro de segmento de dados de 16 bits ou registro DS armazena o endereço inicial do segmento de dados.

  • Stack Segment- Contém dados e endereços de retorno de procedimentos ou sub-rotinas. É implementado como uma estrutura de dados 'pilha'. O registrador Stack Segment ou SS armazena o endereço inicial da pilha.

Além dos registros DS, CS e SS, existem outros registros de segmento extra - ES (segmento extra), FS e GS, que fornecem segmentos adicionais para armazenamento de dados.

Na programação de montagem, um programa precisa acessar os locais de memória. Todos os locais de memória dentro de um segmento são relativos ao endereço inicial do segmento. Um segmento começa em um endereço divisível uniformemente por 16 ou hexadecimal 10. Portanto, o dígito hexadecimal mais à direita em todos esses endereços de memória é 0, que geralmente não é armazenado nos registradores de segmento.

O registrador de segmento armazena os endereços iniciais de um segmento. Para obter a localização exata dos dados ou instrução dentro de um segmento, um valor de deslocamento (ou deslocamento) é necessário. Para fazer referência a qualquer localização da memória em um segmento, o processador combina o endereço do segmento no registro do segmento com o valor de deslocamento da localização.

Exemplo

Veja o programa simples a seguir para entender o uso de registradores na programação de assembly. Este programa exibe 9 estrelas na tela junto com uma mensagem simples -

section	.text
   global _start	 ;must be declared for linker (gcc)
	
_start:	         ;tell linker entry point
   mov	edx,len  ;message length
   mov	ecx,msg  ;message to write
   mov	ebx,1    ;file descriptor (stdout)
   mov	eax,4    ;system call number (sys_write)
   int	0x80     ;call kernel
	
   mov	edx,9    ;message length
   mov	ecx,s2   ;message to write
   mov	ebx,1    ;file descriptor (stdout)
   mov	eax,4    ;system call number (sys_write)
   int	0x80     ;call kernel
	
   mov	eax,1    ;system call number (sys_exit)
   int	0x80     ;call kernel
	
section	.data
msg db 'Displaying 9 stars',0xa ;a message
len equ $ - msg  ;length of message
s2 times 9 db '*'

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Displaying 9 stars
*********

As chamadas do sistema são APIs para a interface entre o espaço do usuário e o espaço do kernel. Já usamos as chamadas de sistema. sys_write e sys_exit, para escrever na tela e sair do programa, respectivamente.

Chamadas de sistema Linux

Você pode fazer uso de chamadas de sistema Linux em seus programas de montagem. Você precisa seguir os seguintes passos para usar chamadas de sistema Linux em seu programa -

  • Coloque o número de chamada do sistema no registro EAX.
  • Armazene os argumentos para a chamada do sistema nos registros EBX, ECX, etc.
  • Ligue para a interrupção relevante (80h).
  • O resultado geralmente é retornado no registro EAX.

Existem seis registradores que armazenam os argumentos da chamada de sistema usada. São EBX, ECX, EDX, ESI, EDI e EBP. Esses registros recebem os argumentos consecutivos, começando com o registro EBX. Se houver mais de seis argumentos, a localização da memória do primeiro argumento é armazenada no registrador EBX.

O seguinte snippet de código mostra o uso da chamada do sistema sys_exit -

mov	eax,1		; system call number (sys_exit)
int	0x80		; call kernel

O seguinte snippet de código mostra o uso da chamada de sistema sys_write -

mov	edx,4		; message length
mov	ecx,msg		; message to write
mov	ebx,1		; file descriptor (stdout)
mov	eax,4		; system call number (sys_write)
int	0x80		; call kernel

Todas as syscalls estão listadas em /usr/include/asm/unistd.h , junto com seus números (o valor a ser colocado em EAX antes de chamar int 80h).

A tabela a seguir mostra algumas das chamadas do sistema usadas neste tutorial -

% eax Nome % ebx % ecx % edx % esx % edi
1 sys_exit int - - - -
2 sys_fork struct pt_regs - - - -
3 sys_read int não assinado Caracteres * size_t - -
4 sys_write int não assinado const char * size_t - -
5 sys_open const char * int int - -
6 sys_close int não assinado - - - -

Exemplo

O exemplo a seguir lê um número do teclado e o exibe na tela -

section .data                           ;Data segment
   userMsg db 'Please enter a number: ' ;Ask the user to enter a number
   lenUserMsg equ $-userMsg             ;The length of the message
   dispMsg db 'You have entered: '
   lenDispMsg equ $-dispMsg                 

section .bss           ;Uninitialized data
   num resb 5
	
section .text          ;Code Segment
   global _start
	
_start:                ;User prompt
   mov eax, 4
   mov ebx, 1
   mov ecx, userMsg
   mov edx, lenUserMsg
   int 80h

   ;Read and store the user input
   mov eax, 3
   mov ebx, 2
   mov ecx, num  
   mov edx, 5          ;5 bytes (numeric, 1 for sign) of that information
   int 80h
	
   ;Output the message 'The entered number is: '
   mov eax, 4
   mov ebx, 1
   mov ecx, dispMsg
   mov edx, lenDispMsg
   int 80h  

   ;Output the number entered
   mov eax, 4
   mov ebx, 1
   mov ecx, num
   mov edx, 5
   int 80h  
    
   ; Exit code
   mov eax, 1
   mov ebx, 0
   int 80h

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Please enter a number:
1234  
You have entered:1234

A maioria das instruções em linguagem assembly requer que operandos sejam processados. Um endereço de operando fornece o local onde os dados a serem processados ​​são armazenados. Algumas instruções não exigem um operando, enquanto outras instruções podem exigir um, dois ou três operandos.

Quando uma instrução requer dois operandos, o primeiro operando é geralmente o destino, que contém dados em um registro ou local de memória e o segundo operando é a fonte. A origem contém os dados a serem entregues (endereçamento imediato) ou o endereço (no registro ou na memória) dos dados. Geralmente, os dados de origem permanecem inalterados após a operação.

Os três modos básicos de endereçamento são -

  • Registrar endereçamento
  • Endereçamento imediato
  • Endereçamento de memória

Registrar Endereçamento

Neste modo de endereçamento, um registro contém o operando. Dependendo da instrução, o registro pode ser o primeiro operando, o segundo operando ou ambos.

Por exemplo,

MOV DX, TAX_RATE   ; Register in first operand
MOV COUNT, CX	   ; Register in second operand
MOV EAX, EBX	   ; Both the operands are in registers

Como o processamento de dados entre registradores não envolve memória, ele fornece processamento de dados mais rápido.

Endereçamento Imediato

Um operando imediato possui um valor constante ou uma expressão. Quando uma instrução com dois operandos usa endereçamento imediato, o primeiro operando pode ser um registro ou localização na memória, e o segundo operando é uma constante imediata. O primeiro operando define o comprimento dos dados.

Por exemplo,

BYTE_VALUE  DB  150    ; A byte value is defined
WORD_VALUE  DW  300    ; A word value is defined
ADD  BYTE_VALUE, 65    ; An immediate operand 65 is added
MOV  AX, 45H           ; Immediate constant 45H is transferred to AX

Endereçamento Direto de Memória

Quando operandos são especificados no modo de endereçamento de memória, é necessário acesso direto à memória principal, geralmente ao segmento de dados. Essa forma de abordar resulta em um processamento mais lento dos dados. Para localizar a localização exata dos dados na memória, precisamos do endereço inicial do segmento, que normalmente é encontrado no registro DS e um valor de deslocamento. Este valor de deslocamento também é chamadoeffective address.

No modo de endereçamento direto, o valor de deslocamento é especificado diretamente como parte da instrução, geralmente indicado pelo nome da variável. O montador calcula o valor de deslocamento e mantém uma tabela de símbolos, que armazena os valores de deslocamento de todas as variáveis ​​usadas no programa.

No endereçamento de memória direto, um dos operandos se refere a uma localização de memória e o outro operando faz referência a um registrador.

Por exemplo,

ADD	BYTE_VALUE, DL	; Adds the register in the memory location
MOV	BX, WORD_VALUE	; Operand from the memory is added to register

Endereçamento de deslocamento direto

Este modo de endereçamento usa os operadores aritméticos para modificar um endereço. Por exemplo, observe as seguintes definições que definem tabelas de dados -

BYTE_TABLE DB  14, 15, 22, 45      ; Tables of bytes
WORD_TABLE DW  134, 345, 564, 123  ; Tables of words

As seguintes operações acessam dados das tabelas na memória em registros -

MOV CL, BYTE_TABLE[2]	; Gets the 3rd element of the BYTE_TABLE
MOV CL, BYTE_TABLE + 2	; Gets the 3rd element of the BYTE_TABLE
MOV CX, WORD_TABLE[3]	; Gets the 4th element of the WORD_TABLE
MOV CX, WORD_TABLE + 3	; Gets the 4th element of the WORD_TABLE

Endereçamento de memória indireta

Este modo de endereçamento utiliza a capacidade do computador de Segmento: Endereçamento de deslocamento . Geralmente, os registradores de base EBX, EBP (ou BX, BP) e os registradores de índice (DI, SI), codificados entre colchetes para referências de memória, são usados ​​para esse propósito.

O endereçamento indireto é geralmente usado para variáveis ​​que contêm vários elementos, como arrays. O endereço inicial do array é armazenado, digamos, no registrador EBX.

O trecho de código a seguir mostra como acessar diferentes elementos da variável.

MY_TABLE TIMES 10 DW 0  ; Allocates 10 words (2 bytes) each initialized to 0
MOV EBX, [MY_TABLE]     ; Effective Address of MY_TABLE in EBX
MOV [EBX], 110          ; MY_TABLE[0] = 110
ADD EBX, 2              ; EBX = EBX +2
MOV [EBX], 123          ; MY_TABLE[1] = 123

A Instrução MOV

Já usamos a instrução MOV que é usada para mover dados de um espaço de armazenamento para outro. A instrução MOV leva dois operandos.

Sintaxe

A sintaxe da instrução MOV é -

MOV  destination, source

A instrução MOV pode ter uma das seguintes cinco formas -

MOV  register, register
MOV  register, immediate
MOV  memory, immediate
MOV  register, memory
MOV  memory, register

Observe que -

  • Ambos os operandos em operação MOV devem ser do mesmo tamanho
  • O valor do operando de origem permanece inalterado

A instrução MOV às vezes causa ambigüidade. Por exemplo, olhe para as declarações -

MOV  EBX, [MY_TABLE]  ; Effective Address of MY_TABLE in EBX
MOV  [EBX], 110	      ; MY_TABLE[0] = 110

Não está claro se você deseja mover um equivalente de byte ou equivalente de palavra do número 110. Nesses casos, é aconselhável usar um type specifier.

A tabela a seguir mostra alguns dos especificadores de tipo comuns -

Especificador de tipo Bytes endereçados
BYTE 1
PALAVRA 2
DWORD 4
QWORD 8
TBYTE 10

Exemplo

O programa a seguir ilustra alguns dos conceitos discutidos acima. Ele armazena um nome 'Zara Ali' na seção de dados da memória e, em seguida, altera seu valor para outro nome 'Nuha Ali' programaticamente e exibe ambos os nomes.

section	.text
   global _start     ;must be declared for linker (ld)
_start:             ;tell linker entry point
	
   ;writing the name 'Zara Ali'
   mov	edx,9       ;message length
   mov	ecx, name   ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	[name],  dword 'Nuha'    ; Changed the name to Nuha Ali
	
   ;writing the name 'Nuha Ali'
   mov	edx,8       ;message length
   mov	ecx,name    ;message to write
   mov	ebx,1       ;file descriptor (stdout)
   mov	eax,4       ;system call number (sys_write)
   int	0x80        ;call kernel
	
   mov	eax,1       ;system call number (sys_exit)
   int	0x80        ;call kernel

section	.data
name db 'Zara Ali '

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Zara Ali Nuha Ali

NASM fornece vários define directivespara reservar espaço de armazenamento para variáveis. A diretiva define assembler é usada para alocação de espaço de armazenamento. Pode ser usado para reservar e também para inicializar um ou mais bytes.

Alocando espaço de armazenamento para dados inicializados

A sintaxe para declaração de alocação de armazenamento para dados inicializados é -

[variable-name]    define-directive    initial-value   [,initial-value]...

Onde, nome-da-variável é o identificador de cada espaço de armazenamento. O montador associa um valor de deslocamento para cada nome de variável definido no segmento de dados.

Existem cinco formas básicas da diretiva define -

Diretriz Objetivo Espaço de armazenamento
DB Definir Byte aloca 1 byte
DW Definir palavra aloca 2 bytes
DD Definir palavra dupla aloca 4 bytes
DQ Definir Quadword aloca 8 bytes
DT Defina dez bytes aloca 10 bytes

A seguir estão alguns exemplos de uso de diretivas de definição -

choice		DB	'y'
number		DW	12345
neg_number	DW	-12345
big_number	DQ	123456789
real_number1	DD	1.234
real_number2	DQ	123.456

Observe que -

  • Cada byte de caractere é armazenado como seu valor ASCII em hexadecimal.

  • Cada valor decimal é automaticamente convertido em seu equivalente binário de 16 bits e armazenado como um número hexadecimal.

  • O processador usa a ordem de bytes little-endian.

  • Os números negativos são convertidos em sua representação de complemento de 2.

  • Números de ponto flutuante curtos e longos são representados usando 32 ou 64 bits, respectivamente.

O programa a seguir mostra o uso da diretiva define -

section .text
   global _start          ;must be declared for linker (gcc)
	
_start:                   ;tell linker entry point
   mov	edx,1		  ;message length
   mov	ecx,choice        ;message to write
   mov	ebx,1		  ;file descriptor (stdout)
   mov	eax,4		  ;system call number (sys_write)
   int	0x80		  ;call kernel

   mov	eax,1		  ;system call number (sys_exit)
   int	0x80		  ;call kernel

section .data
choice DB 'y'

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

y

Alocando espaço de armazenamento para dados não inicializados

As diretivas de reserva são usadas para reservar espaço para dados não inicializados. As diretivas de reserva usam um único operando que especifica o número de unidades de espaço a serem reservadas. Cada diretiva define tem uma diretiva de reserva relacionada.

Existem cinco formas básicas da diretiva de reserva -

Diretriz Objetivo
RESB Reserve um Byte
RESW Reserve uma palavra
RESD Reserve uma palavra dupla
RESQ Reserve um Quadword
DESCANSAR Reserve dez bytes

Múltiplas Definições

Você pode ter várias instruções de definição de dados em um programa. Por exemplo -

choice	  DB 	'Y' 		 ;ASCII of y = 79H
number1	  DW 	12345 	 ;12345D = 3039H
number2    DD  12345679  ;123456789D = 75BCD15H

O montador aloca memória contígua para múltiplas definições de variáveis.

Múltiplas inicializações

A diretiva TIMES permite várias inicializações com o mesmo valor. Por exemplo, uma matriz chamada marcas de tamanho 9 pode ser definida e inicializada como zero usando a seguinte instrução -

marks  TIMES  9  DW  0

A diretiva TIMES é útil na definição de arrays e tabelas. O programa a seguir exibe 9 asteriscos na tela -

section	.text
   global _start        ;must be declared for linker (ld)
	
_start:                 ;tell linker entry point
   mov	edx,9		;message length
   mov	ecx, stars	;message to write
   mov	ebx,1		;file descriptor (stdout)
   mov	eax,4		;system call number (sys_write)
   int	0x80		;call kernel

   mov	eax,1		;system call number (sys_exit)
   int	0x80		;call kernel

section	.data
stars   times 9 db '*'

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

*********

Existem várias diretivas fornecidas pelo NASM que definem constantes. Já usamos a diretiva EQU em capítulos anteriores. Vamos discutir particularmente três diretivas -

  • EQU
  • %assign
  • %define

A Diretiva EQU

o EQUdiretiva é usada para definir constantes. A sintaxe da diretiva EQU é a seguinte -

CONSTANT_NAME EQU expression

Por exemplo,

TOTAL_STUDENTS equ 50

Você pode então usar este valor constante em seu código, como -

mov  ecx,  TOTAL_STUDENTS 
cmp  eax,  TOTAL_STUDENTS

O operando de uma instrução EQU pode ser uma expressão -

LENGTH equ 20
WIDTH  equ 10
AREA   equ length * width

O segmento de código acima definiria ÁREA como 200.

Exemplo

O exemplo a seguir ilustra o uso da diretiva EQU -

SYS_EXIT  equ 1
SYS_WRITE equ 4
STDIN     equ 0
STDOUT    equ 1
section	 .text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg1         
   mov edx, len1 
   int 0x80                
	
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg2         
   mov edx, len2 
   int 0x80 
	
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg3         
   mov edx, len3 
   int 0x80
   
   mov eax,SYS_EXIT    ;system call number (sys_exit)
   int 0x80            ;call kernel

section	 .data
msg1 db	'Hello, programmers!',0xA,0xD 	
len1 equ $ - msg1			

msg2 db 'Welcome to the world of,', 0xA,0xD 
len2 equ $ - msg2 msg3 db 'Linux assembly programming! ' len3 equ $- msg3

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Hello, programmers!
Welcome to the world of,
Linux assembly programming!

A% designar diretiva

o %assignpode ser usada para definir constantes numéricas como a diretiva EQU. Esta diretiva permite redefinição. Por exemplo, você pode definir o TOTAL constante como -

%assign TOTAL 10

Posteriormente no código, você pode redefini-lo como -

%assign  TOTAL  20

Esta diretiva diferencia maiúsculas de minúsculas.

A% define a diretiva

o %definediretiva permite definir constantes numéricas e de string. Esta diretiva é semelhante a #define em C. Por exemplo, você pode definir o PTR constante como -

%define PTR [EBP+4]

O código acima substitui PTR por [EBP + 4].

Essa diretiva também permite a redefinição e faz distinção entre maiúsculas e minúsculas.

A Instrução INC

A instrução INC é usada para incrementar um operando em um. Ele funciona em um único operando que pode estar em um registro ou na memória.

Sintaxe

A instrução INC tem a seguinte sintaxe -

INC destination

O destino do operando pode ser um operando de 8, 16 ou 32 bits.

Exemplo

INC EBX	     ; Increments 32-bit register
INC DL       ; Increments 8-bit register
INC [count]  ; Increments the count variable

A Instrução DEC

A instrução DEC é usada para diminuir um operando em um. Ele funciona em um único operando que pode estar em um registro ou na memória.

Sintaxe

A instrução DEC tem a seguinte sintaxe -

DEC destination

O destino do operando pode ser um operando de 8, 16 ou 32 bits.

Exemplo

segment .data
   count dw  0
   value db  15
	
segment .text
   inc [count]
   dec [value]
	
   mov ebx, count
   inc word [ebx]
	
   mov esi, value
   dec byte [esi]

As instruções ADD e SUB

As instruções ADD e SUB são usadas para realizar adição / subtração simples de dados binários em tamanho de byte, palavra e palavra dupla, ou seja, para adicionar ou subtrair operandos de 8, 16 ou 32 bits, respectivamente.

Sintaxe

As instruções ADD e SUB têm a seguinte sintaxe -

ADD/SUB	destination, source

A instrução ADD / SUB pode ocorrer entre -

  • Cadastre-se para se registrar
  • Memória para registrar
  • Registre-se na memória
  • Registre-se para dados constantes
  • Memória para dados constantes

No entanto, como outras instruções, as operações de memória para memória não são possíveis usando as instruções ADD / SUB. Uma operação ADD ou SUB define ou limpa os sinalizadores de overflow e carry.

Exemplo

O exemplo a seguir pedirá dois dígitos ao usuário, armazenará os dígitos no registrador EAX e EBX, respectivamente, adicionará os valores, armazenará o resultado em um local de memória ' res ' e finalmente exibirá o resultado.

SYS_EXIT  equ 1
SYS_READ  equ 3
SYS_WRITE equ 4
STDIN     equ 0
STDOUT    equ 1

segment .data 

   msg1 db "Enter a digit ", 0xA,0xD 
   len1 equ $- msg1 msg2 db "Please enter a second digit", 0xA,0xD len2 equ $- msg2 

   msg3 db "The sum is: "
   len3 equ $- msg3

segment .bss

   num1 resb 2 
   num2 resb 2 
   res resb 1    

section	.text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point
   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg1         
   mov edx, len1 
   int 0x80                

   mov eax, SYS_READ 
   mov ebx, STDIN  
   mov ecx, num1 
   mov edx, 2
   int 0x80            

   mov eax, SYS_WRITE        
   mov ebx, STDOUT         
   mov ecx, msg2          
   mov edx, len2         
   int 0x80

   mov eax, SYS_READ  
   mov ebx, STDIN  
   mov ecx, num2 
   mov edx, 2
   int 0x80        

   mov eax, SYS_WRITE         
   mov ebx, STDOUT         
   mov ecx, msg3          
   mov edx, len3         
   int 0x80

   ; moving the first number to eax register and second number to ebx
   ; and subtracting ascii '0' to convert it into a decimal number
	
   mov eax, [num1]
   sub eax, '0'
	
   mov ebx, [num2]
   sub ebx, '0'

   ; add eax and ebx
   add eax, ebx
   ; add '0' to to convert the sum from decimal to ASCII
   add eax, '0'

   ; storing the sum in memory location res
   mov [res], eax

   ; print the sum 
   mov eax, SYS_WRITE        
   mov ebx, STDOUT
   mov ecx, res         
   mov edx, 1        
   int 0x80

exit:    
   
   mov eax, SYS_EXIT   
   xor ebx, ebx 
   int 0x80

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Enter a digit:
3
Please enter a second digit:
4
The sum is:
7

The program with hardcoded variables −

section	.text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point
   mov	eax,'3'
   sub     eax, '0'
	
   mov 	ebx, '4'
   sub     ebx, '0'
   add 	eax, ebx
   add	eax, '0'
	
   mov 	[sum], eax
   mov	ecx,msg	
   mov	edx, len
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	ecx,sum
   mov	edx, 1
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	eax,1	;system call number (sys_exit)
   int	0x80	;call kernel
	
section .data
   msg db "The sum is:", 0xA,0xD 
   len equ $ - msg   
   segment .bss
   sum resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The sum is:
7

A Instrução MUL / IMUL

Existem duas instruções para multiplicar dados binários. A instrução MUL (Multiply) lida com dados não assinados e o IMUL (Integer Multiply) lida com dados assinados. Ambas as instruções afetam o sinalizador Carry e Overflow.

Sintaxe

A sintaxe para as instruções MUL / IMUL é a seguinte -

MUL/IMUL multiplier

Multiplicando em ambos os casos estará em um acumulador, dependendo do tamanho do multiplicando e do multiplicador e o produto gerado também é armazenado em dois registradores dependendo do tamanho dos operandos. A seção a seguir explica as instruções do MUL com três casos diferentes -

Sr. Não. Cenários
1

When two bytes are multiplied −

O multiplicando está no registrador AL, e o multiplicador é um byte na memória ou em outro registrador. O produto está em AX. Os 8 bits de ordem superior do produto são armazenados em AH e os 8 bits de ordem inferior são armazenados em AL.

2

When two one-word values are multiplied −

O multiplicando deve estar no registrador AX, e o multiplicador é uma palavra na memória ou outro registrador. Por exemplo, para uma instrução como MUL DX, você deve armazenar o multiplicador em DX e o multiplicando em AX.

O produto resultante é uma palavra dupla, que precisará de dois registros. A parte de ordem superior (extrema esquerda) é armazenada em DX e a parte de ordem inferior (extrema direita) é armazenada em AX.

3

When two doubleword values are multiplied −

Quando dois valores de palavra dupla são multiplicados, o multiplicando deve estar em EAX e o multiplicador é um valor de palavra dupla armazenado na memória ou em outro registro. O produto gerado é armazenado nos registros EDX: EAX, ou seja, os 32 bits de ordem superior são armazenados no registro EDX e os 32 bits de ordem inferior são armazenados no registro EAX.

Exemplo

MOV AL, 10
MOV DL, 25
MUL DL
...
MOV DL, 0FFH	; DL= -1
MOV AL, 0BEH	; AL = -66
IMUL DL

Exemplo

O exemplo a seguir multiplica 3 por 2 e exibe o resultado -

section	.text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point

   mov	al,'3'
   sub     al, '0'
	
   mov 	bl, '2'
   sub     bl, '0'
   mul 	bl
   add	al, '0'
	
   mov 	[res], al
   mov	ecx,msg	
   mov	edx, len
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	ecx,res
   mov	edx, 1
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	eax,1	;system call number (sys_exit)
   int	0x80	;call kernel

section .data
msg db "The result is:", 0xA,0xD 
len equ $- msg   
segment .bss
res resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The result is:
6

As instruções DIV / IDIV

A operação de divisão gera dois elementos - um quotient e um remainder. No caso de multiplicação, o estouro não ocorre porque registros de comprimento duplo são usados ​​para manter o produto. No entanto, em caso de divisão, pode ocorrer estouro. O processador gera uma interrupção se ocorrer estouro.

A instrução DIV (Divide) é usada para dados sem sinal e o IDIV (Integer Divide) é usado para dados assinados.

Sintaxe

O formato da instrução DIV / IDIV -

DIV/IDIV	divisor

O dividendo está em um acumulador. Ambas as instruções podem funcionar com operandos de 8, 16 ou 32 bits. A operação afeta todos os seis sinalizadores de status. A seção a seguir explica três casos de divisão com diferentes tamanhos de operando -

Sr. Não. Cenários
1

When the divisor is 1 byte −

O dividendo é assumido como estando no registrador AX (16 bits). Após a divisão, o quociente vai para o registrador AL e o restante vai para o registrador AH.

2

When the divisor is 1 word −

O dividendo é assumido como tendo 32 bits de comprimento e nos registros DX: AX. Os 16 bits de ordem superior estão em DX e os 16 bits de ordem inferior estão em AX. Após a divisão, o quociente de 16 bits vai para o registrador AX e o restante de 16 bits vai para o registrador DX.

3

When the divisor is doubleword −

O dividendo é assumido como tendo 64 bits de comprimento e nos registradores EDX: EAX. Os 32 bits de ordem superior estão em EDX e os 32 bits de ordem inferior estão em EAX. Após a divisão, o quociente de 32 bits vai para o registrador EAX e o restante de 32 bits vai para o registrador EDX.

Exemplo

O exemplo a seguir divide 8 com 2. O dividend 8 é armazenado no 16-bit AX register e a divisor 2 é armazenado no 8-bit BL register.

section	.text
   global _start    ;must be declared for using gcc
	
_start:             ;tell linker entry point
   mov	ax,'8'
   sub     ax, '0'
	
   mov 	bl, '2'
   sub     bl, '0'
   div 	bl
   add	ax, '0'
	
   mov 	[res], ax
   mov	ecx,msg	
   mov	edx, len
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	ecx,res
   mov	edx, 1
   mov	ebx,1	;file descriptor (stdout)
   mov	eax,4	;system call number (sys_write)
   int	0x80	;call kernel
	
   mov	eax,1	;system call number (sys_exit)
   int	0x80	;call kernel
	
section .data
msg db "The result is:", 0xA,0xD 
len equ $- msg   
segment .bss
res resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The result is:
4

O conjunto de instruções do processador fornece as instruções AND, OR, XOR, TEST e NOT lógica booleana, que testa, configura e apaga os bits de acordo com a necessidade do programa.

O formato dessas instruções -

Sr. Não. Instrução Formato
1 E AND operand1, operand2
2 OU OU operando1, operando2
3 XOR Operando 1 XOR, operando 2
4 TESTE TEST operando 1, operando 2
5 NÃO NÃO operando1

O primeiro operando em todos os casos pode estar no registro ou na memória. O segundo operando pode estar no registro / memória ou em um valor imediato (constante). No entanto, as operações de memória para memória não são possíveis. Essas instruções comparam ou combinam bits dos operandos e definem os sinalizadores CF, OF, PF, SF e ZF.

A instrução AND

A instrução AND é usada para oferecer suporte a expressões lógicas, executando a operação AND bit a bit. A operação AND bit a bit retorna 1, se os bits correspondentes de ambos os operandos são 1, caso contrário, retorna 0. Por exemplo -

Operand1: 	0101
             Operand2: 	0011
----------------------------
After AND -> Operand1:	0001

A operação AND pode ser usada para limpar um ou mais bits. Por exemplo, digamos que o registrador BL contenha 0011 1010. Se você precisar limpar os bits de ordem superior para zero, faça o AND com 0FH.

AND	BL,   0FH   ; This sets BL to 0000 1010

Vamos pegar outro exemplo. Se você quiser verificar se um determinado número é ímpar ou par, um teste simples seria verificar o bit menos significativo do número. Se for 1, o número é ímpar, caso contrário, o número é par.

Supondo que o número esteja no registro AL, podemos escrever -

AND	AL, 01H     ; ANDing with 0000 0001
JZ    EVEN_NUMBER

O programa a seguir ilustra isso -

Exemplo

section .text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   mov   ax,   8h           ;getting 8 in the ax 
   and   ax, 1              ;and ax with 1
   jz    evnn
   mov   eax, 4             ;system call number (sys_write)
   mov   ebx, 1             ;file descriptor (stdout)
   mov   ecx, odd_msg       ;message to write
   mov   edx, len2          ;length of message
   int   0x80               ;call kernel
   jmp   outprog

evnn:   
  
   mov   ah,  09h
   mov   eax, 4             ;system call number (sys_write)
   mov   ebx, 1             ;file descriptor (stdout)
   mov   ecx, even_msg      ;message to write
   mov   edx, len1          ;length of message
   int   0x80               ;call kernel

outprog:

   mov   eax,1              ;system call number (sys_exit)
   int   0x80               ;call kernel

section   .data
even_msg  db  'Even Number!' ;message showing even number
len1  equ  $ - even_msg odd_msg db 'Odd Number!' ;message showing odd number len2 equ $ - odd_msg

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Even Number!

Altere o valor no registro ax com um dígito ímpar, como -

mov  ax, 9h                  ; getting 9 in the ax

O programa exibiria:

Odd Number!

Da mesma forma, para limpar todo o registro, você pode fazer o AND com 00H.

A instrução OR

A instrução OR é usada para dar suporte à expressão lógica executando a operação OR bit a bit. O operador OR bit a bit retorna 1, se os bits correspondentes de um ou de ambos os operandos forem um. Ele retorna 0, se ambos os bits forem zero.

Por exemplo,

Operand1:     0101
             Operand2:     0011
----------------------------
After OR -> Operand1:    0111

A operação OR pode ser usada para definir um ou mais bits. Por exemplo, vamos supor que o registro AL contém 0011 1010, você precisa definir os quatro bits de ordem inferior, você pode fazer OR com um valor 0000 1111, ou seja, FH.

OR BL, 0FH                   ; This sets BL to  0011 1111

Exemplo

O exemplo a seguir demonstra a instrução OR. Vamos armazenar os valores 5 e 3 nos registradores AL e BL, respectivamente, então a instrução,

OR AL, BL

deve armazenar 7 no registro AL -

section .text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   mov    al, 5             ;getting 5 in the al
   mov    bl, 3             ;getting 3 in the bl
   or     al, bl            ;or al and bl registers, result should be 7
   add    al, byte '0'      ;converting decimal to ascii
	
   mov    [result],  al
   mov    eax, 4
   mov    ebx, 1
   mov    ecx, result
   mov    edx, 1 
   int    0x80
    
outprog:
   mov    eax,1             ;system call number (sys_exit)
   int    0x80              ;call kernel
	
section    .bss
result resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

7

A Instrução XOR

A instrução XOR implementa a operação XOR bit a bit. A operação XOR define o bit resultante para 1, se e somente se os bits dos operandos forem diferentes. Se os bits dos operandos forem iguais (ambos 0 ou 1), o bit resultante é zerado para 0.

Por exemplo,

Operand1:     0101
             Operand2:     0011
----------------------------
After XOR -> Operand1:    0110

XORing um operando com ele mesmo muda o operando para 0. Isso é usado para limpar um registro.

XOR     EAX, EAX

A Instrução TEST

A instrução TEST funciona da mesma forma que a operação AND, mas ao contrário da instrução AND, ela não altera o primeiro operando. Portanto, se precisarmos verificar se um número em um registro é par ou ímpar, também podemos fazer isso usando a instrução TEST sem alterar o número original.

TEST    AL, 01H
JZ      EVEN_NUMBER

A instrução NOT

A instrução NOT implementa a operação NOT bit a bit. A operação NOT inverte os bits em um operando. O operando pode estar em um registro ou na memória.

Por exemplo,

Operand1:    0101 0011
After NOT -> Operand1:    1010 1100

A execução condicional em linguagem assembly é realizada por várias instruções de loop e ramificação. Essas instruções podem alterar o fluxo de controle em um programa. A execução condicional é observada em dois cenários -

Sr. Não. Instruções Condicionais
1

Unconditional jump

Isso é executado pela instrução JMP. A execução condicional frequentemente envolve uma transferência de controle para o endereço de uma instrução que não segue a instrução atualmente em execução. A transferência de controle pode ser direta, para executar um novo conjunto de instruções, ou para trás, para reexecutar as mesmas etapas.

2

Conditional jump

Isso é executado por um conjunto de instruções de salto j <condição> dependendo da condição. As instruções condicionais transferem o controle interrompendo o fluxo sequencial e o fazem alterando o valor de deslocamento no IP.

Vamos discutir a instrução CMP antes de discutir as instruções condicionais.

Instrução CMP

A instrução CMP compara dois operandos. Geralmente é usado em execução condicional. Essa instrução basicamente subtrai um operando do outro para comparar se os operandos são iguais ou não. Não perturba os operandos de destino ou origem. É usado junto com a instrução de salto condicional para a tomada de decisão.

Sintaxe

CMP destination, source

CMP compara dois campos de dados numéricos. O operando de destino pode estar no registro ou na memória. O operando de origem pode ser um dado constante (imediato), registro ou memória.

Exemplo

CMP DX,	00  ; Compare the DX value with zero
JE  L7      ; If yes, then jump to label L7
.
.
L7: ...

CMP é freqüentemente usado para comparar se um valor de contador atingiu o número de vezes que um loop precisa ser executado. Considere a seguinte condição típica -

INC	EDX
CMP	EDX, 10	; Compares whether the counter has reached 10
JLE	LP1     ; If it is less than or equal to 10, then jump to LP1

Salto Incondicional

Conforme mencionado anteriormente, isso é executado pela instrução JMP. A execução condicional frequentemente envolve uma transferência de controle para o endereço de uma instrução que não segue a instrução atualmente em execução. A transferência de controle pode ser direta, para executar um novo conjunto de instruções, ou para trás, para reexecutar as mesmas etapas.

Sintaxe

A instrução JMP fornece um nome de rótulo onde o fluxo de controle é transferido imediatamente. A sintaxe da instrução JMP é -

JMP	label

Exemplo

O seguinte trecho de código ilustra a instrução JMP -

MOV  AX, 00    ; Initializing AX to 0
MOV  BX, 00    ; Initializing BX to 0
MOV  CX, 01    ; Initializing CX to 1
L20:
ADD  AX, 01    ; Increment AX
ADD  BX, AX    ; Add AX to BX
SHL  CX, 1     ; shift left CX, this in turn doubles the CX value
JMP  L20       ; repeats the statements

Salto Condicional

Se alguma condição especificada for satisfeita no salto condicional, o fluxo de controle é transferido para uma instrução de destino. Existem várias instruções de salto condicional, dependendo da condição e dos dados.

A seguir estão as instruções de salto condicional usadas em dados assinados usados ​​para operações aritméticas -

Instrução Descrição Bandeiras testadas
JE / JZ Jump Equal ou Jump Zero ZF
JNE / JNZ Jump Not Equal ou Jump Not Zero ZF
JG / JNLE Jump Greater or Jump Not Less / Equal OF, SF, ZF
JGE / JNL Salto maior / igual ou não menor OF, SF
JL / JNGE Salte menos ou não salte maior / igual OF, SF
JLE / JNG Salto menos / igual ou não maior OF, SF, ZF

A seguir estão as instruções de salto condicional usadas em dados não assinados usados ​​para operações lógicas -

Instrução Descrição Bandeiras testadas
JE / JZ Jump Equal ou Jump Zero ZF
JNE / JNZ Jump Not Equal ou Jump Not Zero ZF
JA / JNBE Saltar acima ou não abaixo / igual CF, ZF
JAE / JNB Saltar acima / igual ou não abaixo CF
JB / JNAE Saltar abaixo ou não acima / igual CF
JBE / JNA Saltar abaixo / igual ou não acima AF, CF

As seguintes instruções de salto condicional têm usos especiais e verificam o valor dos sinalizadores -

Instrução Descrição Bandeiras testadas
JXCZ Pule se CX for Zero Nenhum
JC Jump If Carry CF
JNC Pule se não houver transporte CF
JO Saltar se estouro DO
JNO Saltar se não houver estouro DO
JP / JPE Jump Parity ou Jump Parity Even PF
JNP / JPO Jump No Parity ou Jump Parity Odd PF
JS Sinal de salto (valor negativo) SF
JNS Salto sem sinal (valor positivo) SF

A sintaxe para o conjunto de instruções J <condition> -

Exemplo,

CMP	AL, BL
JE	EQUAL
CMP	AL, BH
JE	EQUAL
CMP	AL, CL
JE	EQUAL
NON_EQUAL: ...
EQUAL: ...

Exemplo

O programa a seguir exibe a maior das três variáveis. As variáveis ​​são variáveis ​​de dois dígitos. As três variáveis ​​num1, num2 e num3 têm valores 47, 22 e 31, respectivamente -

section	.text
   global _start         ;must be declared for using gcc

_start:	                 ;tell linker entry point
   mov   ecx, [num1]
   cmp   ecx, [num2]
   jg    check_third_num
   mov   ecx, [num2]
   
	check_third_num:

   cmp   ecx, [num3]
   jg    _exit
   mov   ecx, [num3]
   
	_exit:
   
   mov   [largest], ecx
   mov   ecx,msg
   mov   edx, len
   mov   ebx,1	;file descriptor (stdout)
   mov   eax,4	;system call number (sys_write)
   int   0x80	;call kernel
	
   mov   ecx,largest
   mov   edx, 2
   mov   ebx,1	;file descriptor (stdout)
   mov   eax,4	;system call number (sys_write)
   int   0x80	;call kernel
    
   mov   eax, 1
   int   80h

section	.data
   
   msg db "The largest digit is: ", 0xA,0xD 
   len equ $- msg 
   num1 dd '47'
   num2 dd '22'
   num3 dd '31'

segment .bss
   largest resb 2

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The largest digit is: 
47

A instrução JMP pode ser usada para implementar loops. Por exemplo, o seguinte trecho de código pode ser usado para executar o corpo do loop 10 vezes.

MOV	CL, 10
L1:
<LOOP-BODY>
DEC	CL
JNZ	L1

O conjunto de instruções do processador, no entanto, inclui um grupo de instruções de loop para implementar a iteração. A instrução LOOP básica tem a seguinte sintaxe -

LOOP 	label

Onde, rótulo é o rótulo de destino que identifica a instrução de destino como nas instruções de salto. A instrução LOOP assume que oECX register contains the loop count. Quando a instrução de loop é executada, o registro ECX é decrementado e o controle salta para o rótulo de destino, até o valor do registro ECX, ou seja, o contador atinge o valor zero.

O snippet de código acima pode ser escrito como -

mov ECX,10
l1:
<loop body>
loop l1

Exemplo

O programa a seguir imprime o número de 1 a 9 na tela -

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov ecx,10
   mov eax, '1'
	
l1:
   mov [num], eax
   mov eax, 4
   mov ebx, 1
   push ecx
	
   mov ecx, num        
   mov edx, 1        
   int 0x80
	
   mov eax, [num]
   sub eax, '0'
   inc eax
   add eax, '0'
   pop ecx
   loop l1
	
   mov eax,1             ;system call number (sys_exit)
   int 0x80              ;call kernel
section	.bss
num resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

123456789:

Os dados numéricos são geralmente representados no sistema binário. As instruções aritméticas operam em dados binários. Quando os números são exibidos na tela ou inseridos no teclado, eles estão no formato ASCII.

Até agora, convertemos esses dados de entrada no formato ASCII para binário para cálculos aritméticos e convertemos o resultado de volta para binário. O código a seguir mostra isso -

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov	eax,'3'
   sub     eax, '0'
	
   mov 	ebx, '4'
   sub     ebx, '0'
   add 	eax, ebx
   add	eax, '0'
	
   mov 	[sum], eax
   mov	ecx,msg	
   mov	edx, len
   mov	ebx,1	         ;file descriptor (stdout)
   mov	eax,4	         ;system call number (sys_write)
   int	0x80	         ;call kernel
	
   mov	ecx,sum
   mov	edx, 1
   mov	ebx,1	         ;file descriptor (stdout)
   mov	eax,4	         ;system call number (sys_write)
   int	0x80	         ;call kernel
	
   mov	eax,1	         ;system call number (sys_exit)
   int	0x80	         ;call kernel
	
section .data
msg db "The sum is:", 0xA,0xD 
len equ $ - msg   
segment .bss
sum resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The sum is:
7

Essas conversões, no entanto, têm uma sobrecarga, e a programação em linguagem assembly permite o processamento de números de maneira mais eficiente, na forma binária. Os números decimais podem ser representados em duas formas -

  • Formulário ASCII
  • BCD ou forma decimal codificada em binário

Representação ASCII

Na representação ASCII, os números decimais são armazenados como string de caracteres ASCII. Por exemplo, o valor decimal 1234 é armazenado como -

31	32	33	34H

Onde, 31H é o valor ASCII para 1, 32H é o valor ASCII para 2 e assim por diante. Existem quatro instruções para processar números na representação ASCII -

  • AAA - Ajuste ASCII após a adição

  • AAS - Ajuste ASCII após a subtração

  • AAM - Ajuste ASCII após a multiplicação

  • AAD - Ajuste ASCII antes da divisão

Essas instruções não levam nenhum operando e assumem que o operando necessário está no registro AL.

O exemplo a seguir usa a instrução AAS para demonstrar o conceito -

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   sub     ah, ah
   mov     al, '9'
   sub     al, '3'
   aas
   or      al, 30h
   mov     [res], ax
	
   mov	edx,len	        ;message length
   mov	ecx,msg	        ;message to write
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	edx,1	        ;message length
   mov	ecx,res	        ;message to write
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel
	
section	.data
msg db 'The Result is:',0xa	
len equ $ - msg			
section .bss
res resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The Result is:
6

Representação BCD

Existem dois tipos de representação BCD -

  • Representação BCD descompactada
  • Representação BCD embalada

Na representação BCD não compactada, cada byte armazena o equivalente binário de um dígito decimal. Por exemplo, o número 1234 é armazenado como -

01	02	03	04H

Existem duas instruções para processar esses números -

  • AAM - Ajuste ASCII após a multiplicação

  • AAD - Ajuste ASCII antes da divisão

As quatro instruções de ajuste ASCII, AAA, AAS, AAM e AAD, também podem ser usadas com representação BCD desempacotada. Na representação BCD compactada, cada dígito é armazenado usando quatro bits. Dois dígitos decimais são compactados em um byte. Por exemplo, o número 1234 é armazenado como -

12	34H

Existem duas instruções para processar esses números -

  • DAA - Ajuste decimal após adição

  • DAS - Ajuste decimal após a subtração

Não há suporte para multiplicação e divisão na representação BCD compactada.

Exemplo

O programa a seguir adiciona dois números decimais de 5 dígitos e exibe a soma. Ele usa os conceitos acima -

section	.text
   global _start        ;must be declared for using gcc

_start:	                ;tell linker entry point

   mov     esi, 4       ;pointing to the rightmost digit
   mov     ecx, 5       ;num of digits
   clc
add_loop:  
   mov 	al, [num1 + esi]
   adc 	al, [num2 + esi]
   aaa
   pushf
   or 	al, 30h
   popf
	
   mov	[sum + esi], al
   dec	esi
   loop	add_loop
	
   mov	edx,len	        ;message length
   mov	ecx,msg	        ;message to write
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	edx,5	        ;message length
   mov	ecx,sum	        ;message to write
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel

section	.data
msg db 'The Sum is:',0xa	
len equ $ - msg			
num1 db '12345'
num2 db '23456'
sum db '     '

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The Sum is:
35801

Já usamos strings de comprimento variável em nossos exemplos anteriores. As strings de comprimento variável podem ter quantos caracteres forem necessários. Geralmente, especificamos o comprimento da string por uma das duas maneiras -

  • Armazenar explicitamente o comprimento da corda
  • Usando um personagem sentinela

Podemos armazenar o comprimento da string explicitamente usando o símbolo do contador $ location que representa o valor atual do contador local. No exemplo a seguir -

msg  db  'Hello, world!',0xa ;our dear string
len  equ  $ - msg            ;length of our dear string

$ aponta para o byte após o último caractere da variável de string msg . Portanto,$-msgdá o comprimento da corda. Nós também podemos escrever

msg db 'Hello, world!',0xa ;our dear string
len equ 13                 ;length of our dear string

Como alternativa, você pode armazenar strings com um caractere de sentinela final para delimitar uma string em vez de armazenar o comprimento da string explicitamente. O caractere sentinela deve ser um caractere especial que não aparece em uma string.

Por exemplo -

message DB 'I am loving it!', 0

Instruções de string

Cada instrução de string pode exigir um operando de origem, um operando de destino ou ambos. Para segmentos de 32 bits, as instruções de string usam registradores ESI e EDI para apontar para os operandos de origem e destino, respectivamente.

Para segmentos de 16 bits, entretanto, os registros SI e DI são usados ​​para apontar para a origem e o destino, respectivamente.

Existem cinco instruções básicas para o processamento de strings. Eles são -

  • MOVS - Esta instrução move 1 byte, palavra ou palavra dupla de dados do local da memória para outro.

  • LODS- Esta instrução é carregada da memória. Se o operando for de um byte, ele é carregado no registro AL, se o operando for uma palavra, ele é carregado no registro AX e uma palavra dupla é carregada no registro EAX.

  • STOS - Esta instrução armazena dados do registrador (AL, AX ou EAX) na memória.

  • CMPS- Esta instrução compara dois itens de dados na memória. Os dados podem ser de tamanho de byte, palavra ou palavra dupla.

  • SCAS - Esta instrução compara o conteúdo de um registro (AL, AX ou EAX) com o conteúdo de um item na memória.

Cada uma das instruções acima tem uma versão de byte, palavra e palavra dupla, e as instruções de string podem ser repetidas usando um prefixo de repetição.

Essas instruções usam o par de registradores ES: DI e DS: SI, onde os registradores DI e SI contêm endereços de deslocamento válidos que se referem aos bytes armazenados na memória. SI está normalmente associado a DS (segmento de dados) e DI está sempre associado a ES (segmento extra).

Os registradores DS: SI (ou ESI) e ES: DI (ou EDI) apontam para os operandos origem e destino, respectivamente. Presume-se que o operando de origem esteja em DS: SI (ou ESI) e o operando de destino em ES: DI (ou EDI) na memória.

Para endereços de 16 bits, os registros SI e DI são usados, e para endereços de 32 bits, os registros ESI e EDI são usados.

A tabela a seguir fornece várias versões de instruções de string e o espaço assumido dos operandos.

Instrução Básica Operandos em Operação de Byte Operação de Palavra Operação de palavra dupla
MOVS ES: DI, DS: SI MOVSB MOVSW MOVSD
LODS AX, DS: SI LODSB LODSW LODSD
STOS ES: DI, AX STOSB STOSW STOSD
CMPS DS: SI, ES: DI CMPSB CMPSW CMPSD
SCAS ES: DI, AX SCASB SCASW SCASD

Prefixos de repetição

O prefixo REP, quando definido antes de uma instrução de string, por exemplo - REP MOVSB, causa a repetição da instrução com base em um contador colocado no registrador CX. REP executa a instrução, diminui CX em 1 e verifica se CX é zero. Ele repete o processamento da instrução até que CX seja zero.

A Bandeira de Direção (DF) determina a direção da operação.

  • Use CLD (Clear Direction Flag, DF = 0) para fazer a operação da esquerda para a direita.
  • Use STD (Definir Bandeira de Direção, DF = 1) para fazer a operação da direita para a esquerda.

O prefixo REP também tem as seguintes variações:

  • REP: É a repetição incondicional. Ele repete a operação até que CX seja zero.

  • REPE ou REPZ: É uma repetição condicional. Ele repete a operação enquanto o sinalizador zero indica igual / zero. Ele para quando ZF indica diferente de / zero ou quando CX é zero.

  • REPNE ou REPNZ: Também é uma repetição condicional. Ele repete a operação enquanto o sinalizador zero indica diferente de / zero. Ele para quando ZF indica igual / zero ou quando CX é decrementado para zero.

Já discutimos que as diretivas de definição de dados para o montador são usadas para alocar armazenamento para variáveis. A variável também pode ser inicializada com algum valor específico. O valor inicializado pode ser especificado na forma hexadecimal, decimal ou binária.

Por exemplo, podemos definir uma variável de palavra 'meses' de uma das seguintes maneiras -

MONTHS	DW	12
MONTHS	DW	0CH
MONTHS	DW	0110B

As diretivas de definição de dados também podem ser usadas para definir uma matriz unidimensional. Vamos definir uma matriz unidimensional de números.

NUMBERS	DW  34,  45,  56,  67,  75, 89

A definição acima declara uma matriz de seis palavras, cada uma inicializada com os números 34, 45, 56, 67, 75, 89. Isso aloca 2x6 = 12 bytes de espaço de memória consecutivo. O endereço simbólico do primeiro número será NÚMEROS e o do segundo número será NÚMEROS + 2 e assim por diante.

Tomemos outro exemplo. Você pode definir um array denominado inventário de tamanho 8 e inicializar todos os valores com zero, como -

INVENTORY   DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0
            DW  0

Que pode ser abreviado como -

INVENTORY   DW  0, 0 , 0 , 0 , 0 , 0 , 0 , 0

A diretiva TIMES também pode ser usada para várias inicializações com o mesmo valor. Usando TIMES, a matriz INVENTORY pode ser definida como:

INVENTORY TIMES 8 DW 0

Exemplo

O exemplo a seguir demonstra os conceitos acima definindo uma matriz x de 3 elementos, que armazena três valores: 2, 3 e 4. Ele adiciona os valores na matriz e exibe a soma 9 -

section	.text
   global _start   ;must be declared for linker (ld)
	
_start:	
 		
   mov  eax,3      ;number bytes to be summed 
   mov  ebx,0      ;EBX will store the sum
   mov  ecx, x     ;ECX will point to the current element to be summed

top:  add  ebx, [ecx]

   add  ecx,1      ;move pointer to next element
   dec  eax        ;decrement counter
   jnz  top        ;if counter not 0, then loop again

done: 

   add   ebx, '0'
   mov  [sum], ebx ;done, store result in "sum"

display:

   mov  edx,1      ;message length
   mov  ecx, sum   ;message to write
   mov  ebx, 1     ;file descriptor (stdout)
   mov  eax, 4     ;system call number (sys_write)
   int  0x80       ;call kernel
	
   mov  eax, 1     ;system call number (sys_exit)
   int  0x80       ;call kernel

section	.data
global x
x:    
   db  2
   db  4
   db  3

sum: 
   db  0

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

9

Os procedimentos ou sub-rotinas são muito importantes na linguagem assembly, pois os programas em linguagem assembly tendem a ser grandes. Os procedimentos são identificados por um nome. Seguindo esse nome, é descrito o corpo do procedimento que realiza um trabalho bem definido. O fim do procedimento é indicado por uma instrução de retorno.

Sintaxe

A seguir está a sintaxe para definir um procedimento -

proc_name:
   procedure body
   ...
   ret

O procedimento é chamado de outra função usando a instrução CALL. A instrução CALL deve ter o nome do procedimento chamado como um argumento conforme mostrado abaixo -

CALL proc_name

O procedimento chamado retorna o controle para o procedimento de chamada usando a instrução RET.

Exemplo

Vamos escrever um procedimento muito simples denominado soma que adiciona as variáveis ​​armazenadas no registro ECX e EDX e retorna a soma no registro EAX -

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   mov	ecx,'4'
   sub     ecx, '0'
	
   mov 	edx, '5'
   sub     edx, '0'
	
   call    sum          ;call sum procedure
   mov 	[res], eax
   mov	ecx, msg	
   mov	edx, len
   mov	ebx,1	        ;file descriptor (stdout)
   mov	eax,4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	ecx, res
   mov	edx, 1
   mov	ebx, 1	        ;file descriptor (stdout)
   mov	eax, 4	        ;system call number (sys_write)
   int	0x80	        ;call kernel
	
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel
sum:
   mov     eax, ecx
   add     eax, edx
   add     eax, '0'
   ret
	
section .data
msg db "The sum is:", 0xA,0xD 
len equ $- msg   

segment .bss
res resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

The sum is:
9

Estrutura de dados de pilhas

Uma pilha é uma estrutura de dados semelhante a um array na memória na qual os dados podem ser armazenados e removidos de um local denominado 'topo' da pilha. Os dados que precisam ser armazenados são 'colocados' na pilha e os dados a serem recuperados são 'retirados' da pilha. Stack é uma estrutura de dados LIFO, ou seja, os dados armazenados primeiro são recuperados por último.

A linguagem assembly fornece duas instruções para operações de pilha: PUSH e POP. Essas instruções têm sintaxes como -

PUSH    operand
POP     address/register

O espaço de memória reservado no segmento da pilha é usado para implementar a pilha. Os registros SS e ESP (ou SP) são usados ​​para implementar a pilha. O topo da pilha, que aponta para o último item de dados inserido na pilha, é apontado pelo registro SS: ESP, onde o registro SS aponta para o início do segmento da pilha e o SP (ou ESP) fornece o deslocamento para o segmento da pilha.

A implementação da pilha tem as seguintes características -

  • Somente words ou doublewords pode ser salvo na pilha, não em um byte.

  • A pilha cresce na direção inversa, ou seja, em direção ao endereço de memória inferior

  • O topo da pilha aponta para o último item inserido na pilha; ele aponta para o byte inferior da última palavra inserida.

Conforme discutimos sobre como armazenar os valores dos registradores na pilha antes de usá-los para algum uso; isso pode ser feito da seguinte maneira -

; Save the AX and BX registers in the stack
PUSH    AX
PUSH    BX

; Use the registers for other purpose
MOV	AX, VALUE1
MOV 	BX, VALUE2
...
MOV 	VALUE1, AX
MOV	VALUE2, BX

; Restore the original values
POP	BX
POP	AX

Exemplo

O programa a seguir exibe todo o conjunto de caracteres ASCII. O programa principal chama um procedimento denominado display , que exibe o conjunto de caracteres ASCII.

section	.text
   global _start        ;must be declared for using gcc
	
_start:	                ;tell linker entry point
   call    display
   mov	eax,1	        ;system call number (sys_exit)
   int	0x80	        ;call kernel
	
display:
   mov    ecx, 256
	
next:
   push    ecx
   mov     eax, 4
   mov     ebx, 1
   mov     ecx, achar
   mov     edx, 1
   int     80h
	
   pop     ecx	
   mov	dx, [achar]
   cmp	byte [achar], 0dh
   inc	byte [achar]
   loop    next
   ret
	
section .data
achar db '0'

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
...
...

Um procedimento recursivo é aquele que chama a si mesmo. Existem dois tipos de recursão: direta e indireta. Na recursão direta, o procedimento chama a si mesmo e na recursão indireta, o primeiro procedimento chama um segundo procedimento, que por sua vez chama o primeiro procedimento.

A recursão pode ser observada em vários algoritmos matemáticos. Por exemplo, considere o caso de calcular o fatorial de um número. O fatorial de um número é dado pela equação -

Fact (n) = n * fact (n-1) for n > 0

Por exemplo: fatorial de 5 é 1 x 2 x 3 x 4 x 5 = 5 x fatorial de 4 e este pode ser um bom exemplo de como mostrar um procedimento recursivo. Todo algoritmo recursivo deve ter uma condição de término, ou seja, a chamada recursiva do programa deve ser interrompida quando uma condição for satisfeita. No caso do algoritmo fatorial, a condição final é alcançada quando n é 0.

O programa a seguir mostra como o fatorial n é implementado em linguagem assembly. Para manter o programa simples, calcularemos o fatorial 3.

section	.text
   global _start         ;must be declared for using gcc
	
_start:                  ;tell linker entry point

   mov bx, 3             ;for calculating factorial 3
   call  proc_fact
   add   ax, 30h
   mov  [fact], ax
    
   mov	  edx,len        ;message length
   mov	  ecx,msg        ;message to write
   mov	  ebx,1          ;file descriptor (stdout)
   mov	  eax,4          ;system call number (sys_write)
   int	  0x80           ;call kernel

   mov   edx,1            ;message length
   mov	  ecx,fact       ;message to write
   mov	  ebx,1          ;file descriptor (stdout)
   mov	  eax,4          ;system call number (sys_write)
   int	  0x80           ;call kernel
    
   mov	  eax,1          ;system call number (sys_exit)
   int	  0x80           ;call kernel
	
proc_fact:
   cmp   bl, 1
   jg    do_calculation
   mov   ax, 1
   ret
	
do_calculation:
   dec   bl
   call  proc_fact
   inc   bl
   mul   bl        ;ax = al * bl
   ret

section	.data
msg db 'Factorial 3 is:',0xa	
len equ $ - msg			

section .bss
fact resb 1

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Factorial 3 is:
6

Escrever uma macro é outra maneira de garantir a programação modular em linguagem assembly.

  • Uma macro é uma sequência de instruções, atribuída por um nome e pode ser usada em qualquer parte do programa.

  • No NASM, as macros são definidas com %macro e %endmacro diretivas.

  • A macro começa com a diretiva% macro e termina com a diretiva% endmacro.

A sintaxe para definição de macro -

%macro macro_name  number_of_params
<macro body>
%endmacro

Onde, number_of_params especifica os parâmetros numéricos, macro_name especifica o nome da macro.

A macro é chamada usando o nome da macro junto com os parâmetros necessários. Quando você precisa usar alguma sequência de instruções muitas vezes em um programa, pode colocar essas instruções em uma macro e usá-la em vez de escrever as instruções o tempo todo.

Por exemplo, uma necessidade muito comum de programas é escrever uma sequência de caracteres na tela. Para exibir uma sequência de caracteres, você precisa da seguinte sequência de instruções -

mov	edx,len	    ;message length
mov	ecx,msg	    ;message to write
mov	ebx,1       ;file descriptor (stdout)
mov	eax,4       ;system call number (sys_write)
int	0x80        ;call kernel

No exemplo acima de exibição de uma string de caracteres, os registros EAX, EBX, ECX e EDX foram usados ​​pela chamada de função INT 80H. Portanto, cada vez que você precisa exibir na tela, você precisa salvar esses registros na pilha, invocar INT 80H e, em seguida, restaurar o valor original dos registros da pilha. Portanto, pode ser útil escrever duas macros para salvar e restaurar dados.

Observamos que, algumas instruções como IMUL, IDIV, INT, etc., precisam que algumas das informações sejam armazenadas em alguns registros particulares e até mesmo retornem valores em alguns registros específicos. Se o programa já estava usando esses registros para manter dados importantes, os dados existentes desses registros devem ser salvos na pilha e restaurados após a execução da instrução.

Exemplo

O exemplo a seguir mostra como definir e usar macros -

; A macro with two parameters
; Implements the write system call
   %macro write_string 2 
      mov   eax, 4
      mov   ebx, 1
      mov   ecx, %1
      mov   edx, %2
      int   80h
   %endmacro
 
section	.text
   global _start            ;must be declared for using gcc
	
_start:                     ;tell linker entry point
   write_string msg1, len1               
   write_string msg2, len2    
   write_string msg3, len3  
	
   mov eax,1                ;system call number (sys_exit)
   int 0x80                 ;call kernel

section	.data
msg1 db	'Hello, programmers!',0xA,0xD 	
len1 equ $ - msg1 msg2 db 'Welcome to the world of,', 0xA,0xD len2 equ $- msg2 

msg3 db 'Linux assembly programming! '
len3 equ $- msg3

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Hello, programmers!
Welcome to the world of,
Linux assembly programming!

O sistema considera qualquer entrada ou saída de dados como fluxo de bytes. Existem três fluxos de arquivo padrão -

  • Entrada padrão (stdin),
  • Saída padrão (stdout) e
  • Erro padrão (stderr).

Descritor de arquivo

UMA file descriptoré um inteiro de 16 bits atribuído a um arquivo como um id de arquivo. Quando um novo arquivo é criado ou um arquivo existente é aberto, o descritor de arquivo é usado para acessar o arquivo.

Descritor de arquivo dos fluxos de arquivo padrão - stdin, stdout e stderr são 0, 1 e 2, respectivamente.

Ponteiro de Arquivo

UMA file pointerespecifica o local para uma operação de leitura / gravação subsequente no arquivo em termos de bytes. Cada arquivo é considerado uma sequência de bytes. Cada arquivo aberto está associado a um ponteiro de arquivo que especifica um deslocamento em bytes, em relação ao início do arquivo. Quando um arquivo é aberto, o ponteiro do arquivo é definido como zero.

Arquivo de tratamento de chamadas do sistema

A tabela a seguir descreve resumidamente as chamadas de sistema relacionadas ao tratamento de arquivos -

% eax Nome % ebx % ecx % edx
2 sys_fork struct pt_regs - -
3 sys_read int não assinado Caracteres * size_t
4 sys_write int não assinado const char * size_t
5 sys_open const char * int int
6 sys_close int não assinado - -
8 sys_creat const char * int -
19 sys_lseek int não assinado off_t int não assinado

As etapas necessárias para usar as chamadas do sistema são as mesmas, conforme discutimos anteriormente -

  • Coloque o número de chamada do sistema no registro EAX.
  • Armazene os argumentos para a chamada do sistema nos registros EBX, ECX, etc.
  • Ligue para a interrupção relevante (80h).
  • O resultado geralmente é retornado no registro EAX.

Criando e abrindo um arquivo

Para criar e abrir um arquivo, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_creat () número 8, no registro EAX.
  • Coloque o nome do arquivo no registro EBX.
  • Coloque as permissões do arquivo no registro ECX.

A chamada do sistema retorna o descritor do arquivo criado no registro EAX, em caso de erro, o código do erro está no registro EAX.

Abrindo um arquivo existente

Para abrir um arquivo existente, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_open () número 5, no registro EAX.
  • Coloque o nome do arquivo no registro EBX.
  • Coloque o modo de acesso ao arquivo no registro ECX.
  • Coloque as permissões do arquivo no registro EDX.

A chamada do sistema retorna o descritor do arquivo criado no registro EAX, em caso de erro, o código do erro está no registro EAX.

Entre os modos de acesso a arquivos, os mais comumente usados ​​são: somente leitura (0), somente gravação (1) e leitura / gravação (2).

Lendo de um arquivo

Para ler um arquivo, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_read () número 3, no registro EAX.

  • Coloque o descritor de arquivo no registro EBX.

  • Coloque o ponteiro para o buffer de entrada no registro ECX.

  • Coloque o tamanho do buffer, ou seja, o número de bytes a serem lidos, no registro EDX.

A chamada do sistema retorna a quantidade de bytes lidos no registro EAX, em caso de erro, o código do erro está no registro EAX.

Gravando em um Arquivo

Para gravar em um arquivo, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_write () número 4, no registro EAX.

  • Coloque o descritor de arquivo no registro EBX.

  • Coloque o ponteiro para o buffer de saída no registro ECX.

  • Coloque o tamanho do buffer, ou seja, o número de bytes a serem escritos, no registrador EDX.

A chamada do sistema retorna o número real de bytes escritos no registro EAX, em caso de erro, o código de erro está no registro EAX.

Fechando um Arquivo

Para fechar um arquivo, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_close () número 6, no registro EAX.
  • Coloque o descritor de arquivo no registro EBX.

A chamada do sistema retorna, em caso de erro, o código de erro no registro EAX.

Atualizando um arquivo

Para atualizar um arquivo, execute as seguintes tarefas -

  • Coloque a chamada de sistema sys_lseek () número 19, no registro EAX.
  • Coloque o descritor de arquivo no registro EBX.
  • Coloque o valor de deslocamento no registro ECX.
  • Coloque a posição de referência para o deslocamento no registro EDX.

A posição de referência pode ser:

  • Início do arquivo - valor 0
  • Posição atual - valor 1
  • Fim do arquivo - valor 2

A chamada do sistema retorna, em caso de erro, o código de erro no registro EAX.

Exemplo

O programa a seguir cria e abre um arquivo chamado myfile.txt e grava um texto 'Bem-vindo ao Ponto de Tutoriais' neste arquivo. Em seguida, o programa lê o arquivo e armazena os dados em um buffer denominado info . Por último, exibe o texto conforme armazenado nas informações .

section	.text
   global _start         ;must be declared for using gcc
	
_start:                  ;tell linker entry point
   ;create the file
   mov  eax, 8
   mov  ebx, file_name
   mov  ecx, 0777        ;read, write and execute by all
   int  0x80             ;call kernel
	
   mov [fd_out], eax
    
   ; write into the file
   mov	edx,len          ;number of bytes
   mov	ecx, msg         ;message to write
   mov	ebx, [fd_out]    ;file descriptor 
   mov	eax,4            ;system call number (sys_write)
   int	0x80             ;call kernel
	
   ; close the file
   mov eax, 6
   mov ebx, [fd_out]
    
   ; write the message indicating end of file write
   mov eax, 4
   mov ebx, 1
   mov ecx, msg_done
   mov edx, len_done
   int  0x80
    
   ;open the file for reading
   mov eax, 5
   mov ebx, file_name
   mov ecx, 0             ;for read only access
   mov edx, 0777          ;read, write and execute by all
   int  0x80
	
   mov  [fd_in], eax
    
   ;read from file
   mov eax, 3
   mov ebx, [fd_in]
   mov ecx, info
   mov edx, 26
   int 0x80
    
   ; close the file
   mov eax, 6
   mov ebx, [fd_in]
   int  0x80 
	
   ; print the info 
   mov eax, 4
   mov ebx, 1
   mov ecx, info
   mov edx, 26
   int 0x80
       
   mov	eax,1             ;system call number (sys_exit)
   int	0x80              ;call kernel

section	.data
file_name db 'myfile.txt'
msg db 'Welcome to Tutorials Point'
len equ  $-msg

msg_done db 'Written to file', 0xa
len_done equ $-msg_done

section .bss
fd_out resb 1
fd_in  resb 1
info resb  26

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Written to file
Welcome to Tutorials Point

o sys_brk()chamada de sistema é fornecida pelo kernel, para alocar memória sem a necessidade de movê-la posteriormente. Essa chamada aloca memória logo atrás da imagem do aplicativo na memória. Esta função do sistema permite definir o endereço mais alto disponível na seção de dados.

Esta chamada de sistema usa um parâmetro, que é o endereço de memória mais alto que precisa ser definido. Este valor é armazenado no registro EBX.

Em caso de qualquer erro, sys_brk () retorna -1 ou retorna o próprio código de erro negativo. O exemplo a seguir demonstra a alocação dinâmica de memória.

Exemplo

O programa a seguir aloca 16kb de memória usando a chamada de sistema sys_brk () -

section	.text
   global _start         ;must be declared for using gcc
	
_start:	                 ;tell linker entry point

   mov	eax, 45		 ;sys_brk
   xor	ebx, ebx
   int	80h

   add	eax, 16384	 ;number of bytes to be reserved
   mov	ebx, eax
   mov	eax, 45		 ;sys_brk
   int	80h
	
   cmp	eax, 0
   jl	exit	;exit, if error 
   mov	edi, eax	 ;EDI = highest available address
   sub	edi, 4		 ;pointing to the last DWORD  
   mov	ecx, 4096	 ;number of DWORDs allocated
   xor	eax, eax	 ;clear eax
   std			 ;backward
   rep	stosd            ;repete for entire allocated area
   cld			 ;put DF flag to normal state
	
   mov	eax, 4
   mov	ebx, 1
   mov	ecx, msg
   mov	edx, len
   int	80h		 ;print a message

exit:
   mov	eax, 1
   xor	ebx, ebx
   int	80h
	
section	.data
msg    	db	"Allocated 16 kb of memory!", 10
len     equ	$ - msg

Quando o código acima é compilado e executado, ele produz o seguinte resultado -

Allocated 16 kb of memory!