Linguagem de programação de baixo nível - Low-level programming language

Uma linguagem de programação de baixo nível é uma linguagem de programação que fornece pouca ou nenhuma abstração da arquitetura do conjunto de instruções de um computador - comandos ou funções no mapa da linguagem que são estruturalmente semelhantes às instruções do processador. Geralmente, isso se refere a código de máquina ou linguagem assembly . Por causa da baixa abstração (daí a palavra) entre a linguagem e a linguagem de máquina, as linguagens de baixo nível às vezes são descritas como sendo "próximas do hardware". Programas escritos em linguagens de baixo nível tendem a ser relativamente não portáteis , devido a serem otimizados para um certo tipo de arquitetura de sistema.

As linguagens de baixo nível podem ser convertidas em código de máquina sem um compilador ou interpretador - as linguagens de programação de segunda geração usam um processador mais simples chamado montador - e o código resultante é executado diretamente no processador. Um programa escrito em uma linguagem de baixo nível pode ser executado muito rapidamente, com uma pequena pegada de memória . Um programa equivalente em uma linguagem de alto nível pode ser menos eficiente e usar mais memória. As linguagens de baixo nível são simples, mas consideradas difíceis de usar, devido aos inúmeros detalhes técnicos que o programador deve se lembrar. Por comparação, uma linguagem de programação de alto nível isola a semântica de execução de uma arquitetura de computador da especificação do programa, o que simplifica o desenvolvimento.

Código da máquina

Painel frontal de um minicomputador PDP-8 / E. A linha de interruptores na parte inferior pode ser usada para alternar em um programa de linguagem de máquina.

O código de máquina é a única linguagem que um computador pode processar diretamente sem uma transformação prévia. Atualmente, os programadores quase nunca escrevem programas diretamente em código de máquina, porque isso requer atenção a vários detalhes que uma linguagem de alto nível trata automaticamente. Além disso, requer memorização ou procura de códigos numéricos para cada instrução e é extremamente difícil de modificar.

O código de máquina verdadeiro é um fluxo de dados brutos, geralmente binários . Um programador que codifica em "código de máquina" normalmente codifica instruções e dados em uma forma mais legível, como decimal , octal ou hexadecimal, que é traduzido para o formato interno por um programa chamado carregador ou alternado para a memória do computador a partir de um painel frontal .

Embora poucos programas sejam escritos em linguagem de máquina, os programadores geralmente se tornam adeptos da leitura trabalhando com core dumps ou depurando no painel frontal.

Exemplo: Uma função na representação hexadecimal de 32 bits x 86 código de máquina para calcular o n th número de Fibonacci :

8B542408 83FA0077 06B80000 0000C383
FA027706 B8010000 00C353BB 01000000
B9010000 008D0419 83FA0376 078BD989
C14AEBF1 5BC3

Linguagem de montagem

As linguagens de segunda geração fornecem um nível de abstração acima do código de máquina. Nos primeiros dias da codificação em computadores como TX-0 e PDP-1 , a primeira coisa que os hackers do MIT fizeram foi escrever montadores. A linguagem assembly tem pouca semântica ou especificação formal, sendo apenas um mapeamento de símbolos legíveis por humanos, incluindo endereços simbólicos, para opcodes , endereços , constantes numéricas, strings e assim por diante. Normalmente, uma instrução de máquina é representada como uma linha de código de montagem. Os montadores produzem arquivos de objeto que podem ser vinculados a outros arquivos de objeto ou carregados por conta própria.

A maioria dos montadores fornece macros para gerar sequências comuns de instruções.

Exemplo: a mesma calculadora de número de Fibonacci acima, mas em linguagem assembly x86-64 usando a sintaxe AT&T :

_fib:
        movl $1, %eax
        xorl %ebx, %ebx
.fib_loop:
        cmpl $1, %edi
        jbe .fib_done
        movl %eax, %ecx
        addl %ebx, %eax
        movl %ecx, %ebx
        subl $1, %edi
        jmp .fib_loop
.fib_done:
        ret

Neste exemplo de código, os recursos de hardware do processador x86-64 (seus registros ) são nomeados e manipulados diretamente. A função carrega sua entrada de % edi de acordo com o System V ABI e realiza seu cálculo manipulando valores nos registros EAX , EBX e ECX até que termine e retorne. Observe que, nesta linguagem assembly, não existe o conceito de retornar um valor. O resultado tendo sido armazenado no registrador EAX , o comando RET simplesmente move o processamento do código para o local do código armazenado na pilha (geralmente a instrução imediatamente após aquela que chamou esta função) e cabe ao autor do código de chamada saiba que esta função armazena seu resultado em EAX e recuperá-lo de lá. A linguagem assembly x86-64 não impõe nenhum padrão para retornar valores de uma função (e, portanto, de fato, não tem conceito de função); cabe ao código de chamada examinar o estado após o retorno do procedimento, caso seja necessário extrair um valor.

Compare isso com a mesma função em C:

unsigned int fib(unsigned int n) {
   if (!n)
       return 0;
   else if (n <= 2)
       return 1;
   else {
       unsigned int a, c;
       for (a = c = 1; ; --n) {
           c += a;
           if (n <= 2) return c;
           a = c - a;
       }
   }
}

Este código é muito semelhante em estrutura ao exemplo da linguagem assembly, mas existem diferenças significativas em termos de abstração:

  • A entrada (parâmetro n ) é uma abstração que não especifica nenhum local de armazenamento no hardware. Na prática, o compilador C segue uma das muitas convenções de chamada possíveis para determinar um local de armazenamento para a entrada.
  • A versão em linguagem assembly carrega o parâmetro de entrada da pilha em um registro e em cada iteração do loop diminui o valor no registro, nunca alterando o valor na localização da memória na pilha. O compilador C pode carregar o parâmetro em um registrador e fazer o mesmo ou pode atualizar o valor onde quer que esteja armazenado. O que ele escolhe é uma decisão de implementação completamente oculta do autor do código (e sem efeitos colaterais , graças aos padrões da linguagem C).
  • As variáveis ​​locais a, bec são abstrações que não especificam nenhum local de armazenamento específico no hardware. O compilador C decide como realmente armazená-los para a arquitetura de destino.
  • A função de retorno especifica o valor a ser retornado, mas não determina como ele deve ser retornado. O compilador C para qualquer arquitetura específica implementa um mecanismo padrão para retornar o valor. Compiladores para a arquitetura x86 normalmente (mas nem sempre) usam o registro EAX para retornar um valor, como no exemplo de linguagem assembly (o autor do exemplo de linguagem assembly optou por copiar a convenção C, mas a linguagem assembly não exige isso).

Essas abstrações tornam o código C compilável sem modificação em qualquer arquitetura para a qual um compilador C tenha sido escrito. O código da linguagem assembly x86 é específico para a arquitetura x86.

Programação de baixo nível em linguagens de alto nível

No final dos anos 1960, as linguagens de alto nível , como PL / S , BLISS , BCPL , ALGOL estendido (para grandes sistemas Burroughs ) e C incluíam algum grau de acesso às funções de programação de baixo nível. Um método para isso é o assembly embutido, no qual o código do assembly é incorporado em uma linguagem de alto nível que oferece suporte a esse recurso. Algumas dessas linguagens também permitem que as diretivas de otimização do compilador dependente da arquitetura ajustem a maneira como um compilador usa a arquitetura do processador de destino.

Referências