Introdução a Linguagem de Baixo Nível

Última atualização: 30 March, 2024
Última atualização: 30 March, 2024
Escrito por Lambda

1. Introdução

Até agora observamos como as linguagens de baixo nível oferecem uma interface mais próxima do hardware, permitindo aos programadores escrever instruções mais legíveis, mas ao mesmo tempo lidar com aspectos do hardware. Caso você ainda não tenha lido nosso conteúdo sobre tipos de linguagens de programação, sugerimos que leia nosso artigo sobre Linguagens de Programação.

A linguagem de montagem, também conhecida como Assembly, é uma linguagem de baixo nível, diretamente executada pelo processador. Em Assembly, as instruções são representadas por mnemônicos que correspondem diretamente às operações do processador. Nesse artigo iremos abordar de forma introdutória a linguagem assembly, com o objetivo de conhecer sua estrutura e aspectos básicos. Não aprofundaremos  no conjunto de instruções, apenas iremos apresentar os aspectos mais importantes da linguagem de montagem.

2. Estrutura de um programa Assembly

A estrutura dos programas em Assembly consiste em uma sequência de instruções e diretivas. Cada instrução representa uma operação específica, como a movimentação de dados entre registradores ou a execução de operações aritméticas. As diretivas são utilizadas para definir dados, símbolos e outras informações necessárias para o montador. A sintaxe do Assembly varia de acordo com a arquitetura do processador. Geralmente, as instruções são escritas com mnemônicos seguidos de operandos e podem incluir rótulos para referenciar partes do programa. Vamos examinar um exemplo:

Figura 1 - Estrutura de uma instrução Assembly

Na Figura 1, observamos que uma instrução é formada por um mnemônico que representa um opcode (Código de Operação) dentro do processador. O opcode é definido por um número binário que será decodificado pelo processador para realizar uma operação específica. Por exemplo, a instrução mov realiza uma operação de movimentação de dados de um registrador (abordaremos em breve o conceito de registradores) para outro. Essa operação pode ser representada pelo opcode (0x00fc ou 0000000011111100). Este código é o código da operação mov dentro do processador. Os operandos de origem (source) e destino (target) definem a origem e o destino da operação, no caso, os registradores EBX e EAX, respectivamente.

A grande maioria dos programas em Assembly possui a seguinte estrutura:

section .data
    msg db 'Hello World!', 10, 0  # A mensagem a ser exibida, com um caractere de nova linha (\n) no final

section .bss

section .text
    global main

main:
                        # Escrever a mensagem na saída padrão (stdout)
    mov rax, 1          # syscall número 1 é a sys_write (escrever)
    mov rdi, 1          # O primeiro argumento para sys_write é o descritor de arquivo (1 para stdout)
    mov rsi, msg        # O segundo argumento é um ponteiro para a mensagem
    mov rdx, 13         # O terceiro argumento é o comprimento da mensagem
    syscall             # Fazer a chamada do sistema para escrever a mensagem

                        # Sair do programa
    mov rax, 60         # syscall número 60 é a sys_exit (sair)
    xor rdi, rdi        # O primeiro argumento para sys_exit é o código de saída (0 para indicar sucesso)
    syscall             # Fazer a chamada do sistema para sair do programa
  • O bloco .data representa a região onde os dados usados pelo programa são definidos.
  • O bloco .bss representa a região onde os dados não inicializáveis são definidos.
  • O bloco .text representa o código do programa.
  • O bloco main representa o ponto de entrada do programa. Após esse bloco, geralmente são declaradas as rotinas e sub-rotinas do programa.

A linguagem Assembly é altamente dependente da arquitetura do processador, porém possui algumas características e recursos que são comuns a todos os processadores. Vamos conhecer cada um desses recursos:

Curiosidade: A Microsoft disponibilizou o código fonte em Assembly do MS-DOS, onde você conseguirá baixar e visualizar as instruções do sistema operacional.

3. Registradores

Registradores são locais de armazenamento de dados de alta velocidade localizados dentro do processador. Em arquiteturas de 16 bits, eles podem conter informações de até 16 bits de largura, enquanto em arquiteturas mais avançadas, como 32 ou 64 bits, são capazes de armazenar quantidades maiores de dados. Nas arquiteturas 32 ou 64 bits os registradores são identificados como a letra E e R respectivamente. Exemplo:

  • 16 bits: AX, BX
  • 32 bits: EAX, EBX
  • 64 bits: RAX, RBX

 

Além disso, os registradores podem ser divididos em duas partes: High e Low. Na arquitetura de 16 bits, a parte High refere-se aos 8 bits mais significativos, enquanto o Low refere-se aos 8 bits menos significativos. A tabela abaixo lista os nomes de registradores do processador 8086(16 bits).

NomeDescrição
ax, bx, cx, dxPropósito Geral
cs, ds, ss, esSegmentos
si, di, bp, spPonteiros e índices
ipPonteiro de instrução
flagsEstados do processador
Figura 2 - Divisão de um registrador

4. Modos de Endereçamento

Os modos de endereçamento em Assembly são métodos de especificação de operandos em instruções, influenciando diretamente a forma como os dados são acessados e manipulados. Existem três formas de endereçamento: O modo direto, indireto e imediato. No modo direto, o operando é especificado explicitamente na instrução, podendo ser um valor constante ou um endereço de memória. Já no modo indireto, o endereço do operando não é especificado diretamente, mas é referenciado por meio de um registrador ou local de memória.

Por fim, no modo imediato, o operando é um valor constante incorporado diretamente na instrução. Esses modos fornecem flexibilidade na execução de operações e cálculos em Assembly, permitindo uma ampla gama de manipulações de dados e endereços de memória.

Figura 3 - Tipos de Endereçamentos

5. Fluxo de Controle

Em Assembly, as estruturas de controle são implementadas utilizando instruções como je, jne e jmp. Por exemplo, a instrução jmp (desvio incondicional) transfere o controle de execução para uma posição de memória específica determinada pelo label, sem levar em consideração qualquer condição. Por outro lado, instruções condicionais como je (Jump if equal) e jne (Jump if not equal) avaliam os valores dos registradores para determinar se o desvio ocorrerá, caso os valores sejam iguais ou diferentes, respectivamente.

Por exemplo, um código pode conter uma instrução je para desviar o fluxo do programa para uma posição de memória específica se uma comparação resultar em igualdade, enquanto um jne desviaria se uma comparação resultar em diferença. Essas instruções são fundamentais para a implementação de estruturas de controle, como loops, condicionais e tratamento de erros em programas Assembly.


    cmp ax, bx                  # compara os valores de 'ax' com 'bx'
    je  igual                   # desvia para o bloco igual
    ...
    jne diferente		# desvia para o bloco diferente se a comparação resultar em valores diferentes 
    nop
    ...
    jump fim                    # desvia de forma incondicional para o bloco fim
    nop
    
                                #inicio do bloco igual
igual:
                                # faz alguma lógica
    nop
                                #fim do bloco igual
                                #inicio do bloco diferente
diferente:
                                # faz alguma lógica
    nop
                                #fim do bloco diferente
                                
                                #inicio do bloco fim
fim:
    ret                         # finaliza o programa
                                #fim do bloco fim

Nesse programa, comparamos o valor de ax e bx usando a instrução cmp. Se os valores forem iguais, a instrução je irá desviar a execução para o rótulo "igual", caso contrário, a instrução jne irá desviar para "diferente".

6. Rotinas e Sub-rotinas

Em Assembly, rotinas e sub-rotinas são blocos de código que executam uma determinada tarefa e podem ser chamados de diferentes partes do programa principal. Elas permitem a reutilização de código, promovendo a modularidade e a organização do programa. A diferença entre rotinas e sub-rotinas é que as rotinas são usadas para realizar operações comuns que podem ser necessárias em várias partes do programa, enquanto as sub-rotinas são blocos de código que executam uma tarefa específica e são chamadas por outras partes do código principal.

# Define a rotina que imprime uma mensagem na tela
print_message:
    mov ax, 1           # syscall número 1 é a sys_write (escrever)
    mov di, 1           # O primeiro argumento para sys_write é o descritor de arquivo (1 para stdout)
    mov si, msg         # O segundo argumento é um ponteiro para a mensagem
    mov dx, 13          # O terceiro argumento é o comprimento da mensagem
    syscall             # Fazer a chamada do sistema para escrever a mensagem
section .text
    global main
main:
                        # Chama a sub-rotina add_numbers
    call add_numbers
    ret                 # finaliza o programa
                        # Sub-rotina que adiciona dois números e imprime o resultado
add_numbers:
    mov ax, 5           # Primeiro número
    add ax, 7           # Segundo número
    mov bx, ax          # Move o resultado para ebx
    call print_message  # chama rotina que imprime na tela
    ret                 # Retorna para o chamador

Nesse programa inicia-se a execução do programa no rótulo main, e fazemos uma chamada à sub-rotina add_numbers usando a instrução call. Dentro da sub-rotina, a soma dos números 5 e 7 é feita e o valor resultante é enviado para a rotina global print_message para exibir o resultado na tela. Em seguida, o programa é finalizado.

7. Manipulação de Memória

Em Assembly, a manipulação da memória é essencial para o funcionamento dos programas. Isso inclui acesso, leitura, escrita e alocação de dados. No contexto dos programas Assembly, várias seções, como .data e .text, são usadas para definir dados e textos do programa. Todos os dados declarados nessas seções serão armazenados na memória do programa e podem ser acessados, lidos e manipulados usando as instruções apropriadas. Isso permite que os programas Assembly gerenciem informações importantes, como strings, números e outros tipos de dados, durante a execução do programa.

O acesso e a manipulação eficientes da memória são fundamentais para o desenvolvimento de programas funcionais e otimizados em Assembly. A seguir, apresentamos um exemplo de programa que imprime na tela o texto 'Hello, World' armazenado na seção .data.


section .data
    msg db 'Hello World!', 10, 0  ; A mensagem a ser exibida, com um caractere de nova linha (\n) no final

section .bss

section .text
    global _start

main:
    ; Escrever a mensagem na saída padrão (stdout)
    mov rax, 1          ; syscall número 1 é a sys_write (escrever)
    mov rdi, 1          ; O primeiro argumento para sys_write é o descritor de arquivo (1 para stdout)
    mov rsi, msg        ; O segundo argumento é um ponteiro para a mensagem
    mov rdx, 13         ; O terceiro argumento é o comprimento da mensagem
    syscall             ; Fazer a chamada do sistema para escrever a mensagem

    ; Sair do programa
    mov rax, 60         ; syscall número 60 é a sys_exit (sair)
    xor rdi, rdi        ; O primeiro argumento para sys_exit é o código de saída (0 para indicar sucesso)
    syscall             ; Fazer a chamada do sistema para sair do programa

8. Conclusão

Neste artigo, exploramos os principais aspectos das linguagens de baixo nível ao introduzir a linguagem Assembly. Descobrimos o poder dessa linguagem, que oferece ao programador controle total sobre os recursos do hardware. No entanto, é evidente a complexidade e o trabalho envolvido em escrever programas nesse nível de linguagem.