Assembly
Last updated
Was this helpful?
Last updated
Was this helpful?
Agora que você já sabe como um binário PE é construído, está na hora de entender como o código contido em suas seções de código de fato é executado. Acontece que um processador é programado em sua fábrica para entender determinadas sequências de bytes como código e executar alguma operação. Para entender isso, vamos fazer uma analogia com um componente muito mais simples que um processador, um circuito integrado (genericamente chamado de chip).
Um circuito integrado (CI) bastante conhecido no mundo da eletrônica é o 7400. Seu funcionamento interno é detalhado no seguinte diagrama a seguir.
Se você já estudou portas lógicas, vai perceber que este CI tem 4 portas NAND (AND com saída negada). Cada porta possui duas entradas e uma saída, cada uma delas conectada a seu respectivo pino/perna do CI.
Admitindo duas entradas (A e B) e uma saída S, a tabela verdade de cada uma das portas deste CI é a seguinte:
0
0
0
1
1
0
0
1
0
1
0
1
1
1
1
0
Podemos dizer então que este CI faz uma única operação sempre, com as entradas de dados que recebe.
Se seu projeto precisasse também de portas OR, XOR, AND, etc você precisaria comprar outros circuitos integrados, certo? Alternativamente, uma solução seria utilizar um chip que fosse programável. Dessa forma, você o configuraria, via software, para atuar em certos pinos como porta NAND, em outros como porta OR e por aí vai, de acordo com sua necessidade. Para casos assim, existem os microcontroladores. Eles podem podem ser programados em linguagens de alto nível que contam com recursos como repetições, condicionais, e tudo que uma linguagem de programação completa oferece. No entanto, estes chips requerem uma reprogramação a cada mudança no projeto, assim como se faz com o Arduino hoje em dia.
Neste sentido um microprocessador, ou simplesmente processador é muito mais poderoso. Ao invés nós gravarmos um programa nele como fazemos com os microcontroladores, o próprio fabricante do processador é que grava um programa lá, de modo que este microprograma entenda diferentes instruções para realizar diferentes operações de muito mais complexidade se comparadas às simples operações booleanas. Sua entrada de dados também é muito mais larga, de modo que mais bits podem ser enviados por vez.
Isso significa que se um processador receber em seu barramento um conjunto de bits específico, sabe que vai precisar executar uma operação específica. Considere agora bytes como conjuntos de 8 bits, como já aprendemos. À estes bytes possíveis damos o nome de opcodes. Ao conjunto dos opcodes + operandos damos o nome de instrução.
Para entender melhor, suponha que queiramos então fazer uma operação OR entre os valores 0x20 e 0x18 utilizando um processador de arquitetura x86-64. Na documentação deste processador, suponha que constem as seguintes informações:
Ao receber o opcode 0xb8, os próximos quatro bytes serão um número salvos numa memória interna (similar àquela memória M+ das calculadoras).
Para acessar essa memória interna específica, utiliza-se o byte identificador 0xc8.
Ao receber o opcode 0x83, o próximo byte identifica a memória interna a ser acessada e o byte seguinte é um operando que efetua uma operação OR dele com o número de quatro bytes armazenado na memória interna.
Precisaríamos então enviar para este processador as duas instruções, da seguinte forma:
Na primeira, que tem um total de 5 bytes, o opcode 0xb8 é utilizado para colocar o número de 32-bits (próximos 4 bytes) na memória interna. Como nosso número desejado possui somente 1 byte, preenchemos os outros três com zero, respeitando o endianness.
A segunda instrução tem 3 bytes sendo que o primeiro é o opcode dela (OR), o segundo é o identificador da memória interna e o terceiro é o nosso operando 0x18.
Temos que concordar que criar um programa assim não seria nada fácil. Para resolver este problema foi criada uma linguagem de programação, completamente atrelada à arquitetura do processador, aos seus opcodes e às suas instruções, chamada Assembly. Ela dá nomes, por exemplo, para a tal memória interna, que são chamadas de registradores. Além disso, ao invés de usar os opcodes numéricos, você pode usar mnemônicos (palavras) em inglês para sinalizar a instrução desejada. Veja se não é mais fácil de entender assim:
De posse de um compilador de Assembly, muitas vezes chamado de assembler (ou montador em Português), o resultado da compilação do código-fonte acima é justamente um arquivo objeto que contém os opcodes e argumentos corretos para o processador alvo onde o programa vai rodar.
Agora você sabe o motivo pelo qual um programa compilado não é compatível entre diferentes processadores de diferentes arquiteturas. Como estes possuem instruções diferentes e opcodes diferentes, não há mesmo essa compatibilidade.
Perceba que Assembly é uma linguagem legível para seres humanos, diferente da linguagem de máquina que não passa de uma "tripa de bytes" onde você tem que sar seu jeito para fazer sentido.
Como afirmados antes, as palavras em linguagem Assembly que você usa para sinalizar instruções são chamadas de mnemônicos. No exemplo de código acima, utilizamos dois: o MOV e o OR. Estudaremos mais mnemônicos em breve.