Linguagem de aprendizagem de montagem
Linguagem de montagem é o equivalente humano legível ao nível mais baixo de programação de computador – código de máquina.
Embora o computador compreenda todos os programas como números, onde vários números diferentes instruem o computador a fazer operações diferentes, isto é demasiado tediuos para o consumo humano (quanto mais para a autoria). Portanto, os humanos programam usando linguagem assembly, que tem uma correspondência quase 1:1 com o código da máquina.
Aprenda a linguagem de programação C. Se você conhece C (ou C++), assembly será mais fácil.
C# e Java são boas linguagens, mas há poucas ou nenhumas ferramentas para mostrar como é o código C# e Java na linguagem assembly. (Na verdade Java e C# têm a sua própria linguagem byte code assembly que é bastante diferente, e distante, das arquiteturas de conjuntos de instruções de hardware reais.)
De código C (e C+++) podemos usar os compiladores para nos mostrar como é a linguagem assembly. C é uma linguagem de baixo nível, e se você souber como cada construção em C se traduz em linguagem assembly, você conhecerá a linguagem assembly! Exemplos das construções em C que queremos saber para a linguagem assembly são:
C Construct | Equivalente da linguagem assembly |
---|---|
variável local | Registro da CPU ou localização da memória da pilha |
variável global | acesso global ou Const memory location |
array reference | a pattern of addition and memory access |
goto | unconditional branch instruction |
if statement | a pattern in if-Vai para lá: teste condicional & instruções de ramificação |
enquanto que loop | um padrão em if-goto |
atribuição | modificação de registo ou memória |
aritmética | instruções aritméticas |
parametro passagem | modificação da memória de registo ou de pilha |
recepção de parâmetros | acesso ao registo ou à pilha de memória |
função chamada | instrução de chamada |
entrada da função | modelo do diário |
saída da função | modelo do diário |
valor de retorno | modificação do registo e epílogo |
Como a linguagem assembly Difere de outras linguagens de programação
Em linguagem assembly, você será exposto aos recursos nus do hardware da CPU subjacente. (Além disso, coisas que podemos escrever em C em uma linha levam várias linhas em assembly.)
-
Você estará ciente de coisas como registros de CPU, e a pilha de chamadas – estes recursos têm que ser gerenciados para que um programa funcione (e em outras línguas, estes recursos são gerenciados automaticamente para nós.) A consciência destes recursos é fundamental para funcionar chamando e retornando. (Registros de CPU são raros em comparação com a memória, porém, são super rápidos, e também consistentes em seu desempenho.)
-
Não há instruções estruturadas na linguagem assembly – assim, todos os fluxos de construções de controle têm que ser programados em um estilo “If-Goto” (em outras línguas, o compilador traduz instruções estruturadas em If-Goto). O estilo if-goto utiliza etiquetas como alvos. (Enquanto C suporta o uso de goto e etiquetas, os programadores preferem declarações estruturadas na maioria das vezes, assim não vemos frequentemente goto e etiquetas em C.)
Linguagem de montagem vs. código de máquina
Linguagem de montagem é destinada tanto para leitura como para escrita.
Etiquetas
Linguagem de montagem, usamos muitas etiquetas. Os usos das etiquetas traduzem-se em números em código de máquina e as etiquetas desaparecem no código da máquina. As etiquetas (vs. usar números directamente) tornam possível fazer pequenas alterações nos programas – sem elas mesmo uma pequena alteração seria na verdade uma enorme alteração.
O processador precisa de saber onde está um operando, talvez um operando de dados ou talvez uma localização do código. Para localizações de código, por exemplo, tipicamente o processador vai querer saber a que distância está o item da localização de código que está executando atualmente – isto é chamado de endereçamento pc-relativo. Então, para executar uma instrução if-then, podemos dizer ao processador: sob certas condições, salte adiante as instruções N, onde N representa o tamanho ou contagem das instruções a serem puladas (ou não), ou seja, N é o tamanho da parte de então – que estaria em código de máquina. Se adicionarmos uma instrução à então-parte, o número de instruções a saltar sobe (e se removermos uma instrução da então-parte, desce).
Usar etiquetas contabiliza automaticamente as alterações (por exemplo adicionando, removendo instruções) – uma instrução de ramo que usa uma etiqueta como alvo acomoda mudanças na contagem de quantas instruções devem ser condicionalmente puladas.
Pseudo Instruções
Mais intrucções na linguagem assembly traduzem 1:1 em instruções de código de máquina. Entretanto, ocasionalmente, temos que dizer ao assembler algo que não traduza diretamente em código de máquina. Tipicamente exemplos estão dizendo ao assembler que as seguintes instruções são código (geralmente via uma diretiva .text
) vs. dados (geralmente via uma diretiva .data
). Como instruções regulares, estas instruções são tipicamente escritas em sua própria linha, entretanto, o assembler é informado ao invés de gerar instruções de código de máquina diretamente.
Writing Assembly Language
Em C, nós escrevemos instruções; estas instruções são executadas consecutivamente uma após a outra, por padrão sequencialmente, até que alguma construção de controle de fluxo (loop, se) altere o fluxo. Na linguagem assembly o mesmo é verdadeiro.
Em C, comandos têm efeitos sobre variáveis – podemos considerar que comandos separados comunicam-se entre si através de variáveis. O mesmo é verdade em linguagem assembly: as instruções comunicam-se entre si através do efeito que têm sobre as variáveis, embora como há mais instruções (necessariamente assim, como as instruções são mais simples), também haverá necessariamente mais variáveis, muitas são temporárias de muito curta duração usadas para comunicar de uma instrução para outra.
Programação Básica de Langauge Assembly
Alternamos entre seleção de registros e seleção de instruções. Como as instruções em linguagem assembly são bastante simples, muitas vezes precisamos usar várias instruções, sim, interconectadas via valores como variáveis em registros da cpu. Quando isso acontecer, precisaremos selecionar um registro para manter resultados intermediários.
Seleção de Registros
Para selecionar registro, precisamos de um modelo mental do qual os registros estão ocupados e os registros são livres. Com esse conhecimento podemos escolher um registro livre para usar para segurar um resultado temporário (de curta duração). Esse registro permanecerá ocupado até o nosso último uso, então ele volta a ser livre – uma vez livre ele pode ser redirecionado para algo completamente diferente. (Em algum sentido, o processador não sabe realmente que estamos fazendo essa constante redirecionamento, pois não conhece a intenção do programa.)
Seleção de Instruções
Após termos estabelecido os recursos a serem usados, podemos escolher a instrução específica que precisamos. Muitas vezes não encontraremos uma instrução que faça exatamente o que queremos, então teremos que fazer um trabalho com 2 ou mais instruções, o que significa, sim, mais seleção de registros para temporários.
Por exemplo, MIPS e RISC V são arquiteturas similares que fornecem uma comparação & instrução de ramo, mas eles só podem comparar dois registros de cpu. Então, se queremos ver se um byte em uma string é um certo caractere (como uma nova linha), então temos que fornecer (carregar) esse valor de nova linha (uma constante numérica) em um registro cpu antes de podermos usar a comparação & instrução de ramo.
Chamada de Função & Retornando
Um programa consiste de talvez milhares de funções! Enquanto estas funções têm que compartilhar memória, a memória é vasta, e assim elas podem se dar bem. No entanto, com uma CPU há um número limitado e fixo de registos (como 32 ou 16), e, todas as funções têm de partilhar estes registos!!
Para que isso funcione, os designers de software definem convenções – que podem ser pensadas como regras – para partilhar estes limitados recursos fixos. Além disso, estas convenções são necessárias para que uma função (um chamador) chame outra função (uma chamada). As regras são referidas como a Convenção de Chamada, que identifica para quem chama/chamada, onde colocar/encontrar argumentos e retornar valores. Uma convenção de chamadas é parte da Interface Binária da Aplicação, que nos diz como uma função deve comunicar com o mundo exterior (outras funções) em código de máquina. A convenção de chamadas é definida para o uso do software (o hardware não se importa).
Estes documentos nos dizem o que fazer na passagem de parâmetros, uso de registros, alocação/dealocação de pilha e retorno de valores. Estes documentos são publicados pelo provedor do conjunto de instruções, ou provedor do compilador, e são bem conhecidos para que o código de vários lugares possa interoperar corretamente. As convenções de chamada publicadas devem ser tomadas como dadas, mesmo que o hardware não saiba sobre elas.
Sem essa documentação, não saberíamos como fazer essas coisas (passagem de parâmetros, etc.). Não temos que usar convenções de chamadas padrão, mas é impraticável inventar a nossa própria para nada além de um simples programa de brinquedo executado em um simulador. Rolar nossa própria convenção de chamadas não significa interoperabilidade com outro código, o que precisamos fazer em qualquer programa realista, como fazer chamadas de sistema e biblioteca.
O hardware não sabe ou não se importa com as convenções de uso de software. Ele apenas executa instrução após instrução (muito rápido). Por exemplo, para o hardware, a maioria dos registros são equivalentes, mesmo que os registros sejam particionados ou atribuídos usos particulares pelas convenções de software.
Registros Dedicados
Alguns registros de CPU são dedicados a propósitos específicos. Por exemplo, a maioria das CPU’s hoje em dia têm alguma forma de ponteiro de pilha. Em MIPS e RISC V o ponteiro da pilha é um registro comum indiferenciado dos outros, exceto por definição das convenções de software – a escolha de qual registro é arbitrário, mas é realmente escolhido, e esta escolha é publicada para que todos saibamos qual registro é. Em x86, por outro lado, o ponteiro da pilha é suportado por instruções dedicadas push
& pop
(assim como instruções de chamada e retorno), portanto para o registro do ponteiro da pilha há apenas uma escolha lógica.
Particionamento dos registros
Para compartilhar os outros registros entre milhares de funções, eles são divididos, por convenção de software, tipicamente em quatro conjuntos:
- Registros para passar parâmetros
- Registros para receber valores de retorno
- Registros que são “voláteis”
- Registros que são “não voláteis”
Parâmetro &Registros de retorno
Registros de parâmetros são usados para passar parâmetros! Ao aderir à convenção de chamada, os callee’s sabem onde encontrar seus argumentos, e os chamadores sabem onde colocar os valores dos argumentos. Os registros de parâmetros são usados para passar os parâmetros pelas regras estabelecidas na convenção de chamada e, quando mais parâmetros são passados do que a convenção para a CPU permite, o restante dos parâmetros são passados usando a memória da pilha.
Return value registra os valores de retorno da função hold, então quando uma função completa, seu trabalho é ter colocado seu valor de retorno no registro apropriado, antes de retornar o fluxo de controle para o chamador. Ao aderir à convenção de chamada, tanto o chamador quanto o chamado sabem onde encontrar o valor de retorno da função.
Volatile & Registros Não Voláteis
Registros Voláteis são livres para serem usados por qualquer função sem preocupação com o uso anterior ou valores naquele registro. Estes são bons para os temporais necessários para interligar uma sequência de instruções, e também podem ser usados para variáveis locais. Entretanto, como esses registros são livres para serem usados por qualquer função, não podemos contar com eles mantendo seus valores através de uma chamada de função – por definição (convenção de software)! Assim, após a função retornar sua chamada não podemos dizer quais valores estariam nesses registros – qualquer função é livre para redirecioná-los, embora com o conhecimento de que não podemos confiar que seus valores sejam preservados após o retorno de uma função chamada. Assim, esses registros são usados para variáveis e temporais que não precisam abranger uma chamada de função.
Para variáveis que precisam abranger uma chamada de função, existem registros não voláteis (e memória de pilha, também). Estes registros são preservados através de uma chamada de função (por software que adere à convenção de chamada). Assim, se uma função deseja ter uma variável em um registro rápido da CPU, mas essa variável está ao vivo através de uma chamada (definida antes de uma chamada, e usada após a chamada), podemos escolher um registro não-volátil, com o requisito (convenção de chamada definida) de que o valor anterior do registro seja restaurado ao retornar ao seu chamador. Isto requer salvar o valor atual do registro não-volátil (na memória da pilha) antes de redirecionar o registro, e restaurá-lo (usando o valor anterior da memória da pilha) no retorno.