Para entender como os computadores são organizados, como eles parecem funcionar em um nível muito baixo, é necessário entender como funciona um programa de linguagem de montagem. No nível mais simplista, os computadores têm três partes principais:
- memória principal ou RAM que contém dados e instruções,
- um processador, que processa os dados através da execução das instruções, e
- entrada e saída (às vezes abreviada para E/S), que permitem ao computador se comunicar com o mundo exterior e armazenar dados fora da memória principal para que possa recuperar os dados mais tarde.
Memória principal
Na maioria dos computadores, a memória é dividida em bytes. Cada byte contém 8 bits. Cada byte na memória também tem um endereço que é um número que diz onde o byte está na memória. O primeiro byte na memória tem um endereço 0, o próximo tem um endereço 1, e assim por diante. A divisão da memória em bytes a torna endereçável porque cada byte recebe um endereço único. Os endereços das memórias de bytes não podem ser usados para se referir a um único bit de um byte. Um byte é o menor pedaço de memória que pode ser endereçado.
Embora um endereço se refira a um byte específico na memória, os processadores permitem o uso de vários bytes de memória em uma linha. O uso mais comum deste recurso é usar 2 ou 4 bytes em fila para representar um número, geralmente um número inteiro. Bytes individuais também são às vezes usados para representar números inteiros, mas como eles têm apenas 8 bits de comprimento, eles só podem conter 28 ou 256 valores possíveis diferentes. O uso de 2 ou 4 bytes em uma linha eleva o número de diferentes valores possíveis para 216, 65536 ou 232, 4294967296, respectivamente.
Quando um programa usa um byte ou um número de bytes em uma linha para representar algo como uma letra, um número ou qualquer outra coisa, esses bytes são chamados de objeto porque todos fazem parte da mesma coisa. Mesmo que todos os objetos sejam armazenados em bytes idênticos de memória, eles são tratados como se tivessem um "tipo", que diz como os bytes devem ser entendidos: ou como um inteiro ou um personagem ou algum outro tipo (como um valor não-inteiro). O código da máquina também pode ser pensado como um tipo que é interpretado como instruções. A noção de um tipo é muito, muito importante porque define o que pode ou não ser feito ao objeto e como interpretar os bytes do objeto. Por exemplo, não é válido armazenar um número negativo em um objeto de número positivo e não é válido armazenar uma fração em um número inteiro.
Um endereço que aponta para (é o endereço de) um objeto multi-byte é o endereço do primeiro byte desse objeto - o byte que tem o endereço mais baixo. Como um aparte, uma coisa importante a notar é que você não pode dizer qual é o tipo de objeto - ou mesmo seu tamanho - por seu endereço. Na verdade, você não pode nem mesmo dizer que tipo é um objeto, olhando para ele. Um programa de linguagem de montagem precisa acompanhar quais endereços de memória contêm quais objetos, e qual o tamanho desses objetos. Um programa que o faz é do tipo seguro porque só faz coisas a objetos que são seguros para fazer em seu tipo. Um programa que não funciona provavelmente não funcionará corretamente. Note que a maioria dos programas não armazena explicitamente qual é o tipo de objeto, eles apenas acessam objetos de forma consistente - o mesmo objeto é sempre tratado como o mesmo tipo.
O Processador
O processador executa (executa) as instruções, que são armazenadas como código de máquina na memória principal. Além de poder acessar a memória para armazenamento, a maioria dos processadores tem alguns espaços pequenos, rápidos e de tamanho fixo para guardar objetos que estão sendo trabalhados atualmente. Estes espaços são chamados de registros. Os processadores normalmente executam três tipos de instruções, embora algumas instruções possam ser uma combinação destes tipos. Abaixo estão alguns exemplos de cada tipo em linguagem de montagem x86.
Instruções que lêem ou escrevem memória
A seguinte instrução de linguagem de montagem x86 lê (carrega) um objeto de 2 bytes do byte no endereço 4096 (0x1000 em hexadecimal) em um registro de 16 bits chamado 'ax':
mov ax, [1000h]
Nesta linguagem de montagem, colchetes em torno de um número (ou um nome de registro) significam que o número deve ser usado como um endereço para os dados que devem ser usados. O uso de um endereço para apontar para os dados é chamado de indireção. Neste próximo exemplo, sem os colchetes, outro registro, bx, na verdade recebe o valor 20 carregado nele.
mov bx, 20
Como não foi utilizada nenhuma indireção, o valor real em si foi colocado no registro.
Se os operandos (as coisas que vêm depois da mnemônica), aparecem na ordem inversa, uma instrução que carrega algo da memória, em vez disso, grava-o na memória:
mov [1000h], eixo
Aqui, a memória no endereço 1000h recebe o valor do eixo. Se este exemplo for executado logo após o anterior, os 2 bytes em 1000h e 1001h serão um inteiro de 2 bytes com o valor de 20.
Instruções que realizam operações matemáticas ou lógicas
Algumas instruções fazem coisas como subtração ou operações lógicas como não:
O exemplo de código de máquina anteriormente apresentado neste artigo seria este na linguagem de montagem:
adicionar eixo, 42
Aqui, 42 e eixo são adicionados juntos e o resultado é armazenado de volta no eixo. Na montagem x86 também é possível combinar um acesso à memória e uma operação matemática como esta:
adicionar eixo, [1000h]
Esta instrução acrescenta o valor do inteiro de 2 bytes armazenados a 1000h ao eixo e armazena a resposta no eixo.
ou machado, bx
Esta instrução calcula o ou do conteúdo do eixo e bx dos registros e armazena o resultado de volta ao eixo.
Instruções que decidem qual vai ser a próxima instrução
Normalmente, as instruções são executadas na ordem em que aparecem na memória, que é a ordem em que são digitadas no código de montagem. O processador apenas as executa uma após a outra. Entretanto, para que os processadores possam fazer coisas complicadas, eles precisam executar instruções diferentes com base nos dados que lhes foram fornecidos. A capacidade dos processadores de executar instruções diferentes dependendo do resultado de algo é chamada de ramificação. As instruções que decidem qual deve ser a próxima instrução são chamadas de branch instructions.
Neste exemplo, suponha que alguém queira calcular a quantidade de tinta que precisará para pintar um quadrado com um determinado comprimento lateral. Entretanto, devido à economia de escala, a loja de tintas não os venderá menos do que a quantidade de tinta necessária para pintar um quadrado de 100 x 100.
Para descobrir a quantidade de tinta que eles precisarão obter com base no comprimento do quadrado que desejam pintar, eles inventam este conjunto de etapas:
- subtrair 100 do comprimento lateral
- se a resposta for menor que zero, ajuste o comprimento do lado para 100
- multiplicar o comprimento lateral por si só
Esse algoritmo pode ser expresso no seguinte código, onde o eixo é o comprimento lateral.
mov bx, eixo sub bx, 100 jge continuar mov ax, 100 continua: eixo mul
Este exemplo introduz várias coisas novas, mas as duas primeiras instruções são familiares. Elas copiam o valor do eixo em bx e depois subtraem 100 de bx.
Uma das novidades deste exemplo é chamado de rótulo, um conceito encontrado nas linguagens de montagem em geral. Os rótulos podem ser qualquer coisa que o programador queira (a menos que seja o nome de uma instrução, o que confundiria o montador). Neste exemplo, o rótulo é "continuar". É interpretado pelo assembler como o endereço de uma instrução. Neste caso, é o endereço de mult ax.
Outro novo conceito é o das bandeiras. Nos processadores x86, muitas instruções colocam 'bandeiras' no processador que podem ser usadas pela próxima instrução para decidir o que fazer. Neste caso, se bx era inferior a 100, o sub colocará uma bandeira que diz que o resultado foi inferior a zero.
A instrução seguinte é jge que é abreviatura de "Jump if Greater than or Equal to" (Saltar se for maior ou igual a). É uma instrução de ramo. Se as bandeiras no processador especificarem que o resultado foi maior ou igual a zero, em vez de apenas ir para a próxima instrução o processador saltará para a instrução na etiqueta continue, que é mul-eixo.
Este exemplo funciona bem, mas não é o que a maioria dos programadores escreveria. A instrução de subtração define a bandeira corretamente, mas também muda o valor em que ela opera, o que exigia que o eixo fosse copiado para bx. A maioria dos idiomas de montagem permite uma instrução de comparação que não altera nenhum dos argumentos que são passados, mas ainda assim define as bandeiras corretamente e a montagem x86 não é exceção.
eixo cmp, 100 jge continuar mov ax, 100 continua: eixo mul
Agora, em vez de subtrair 100 do eixo, vendo se esse número é inferior a zero, e atribuí-lo de volta ao eixo, o eixo é deixado inalterado. As bandeiras ainda são colocadas do mesmo modo, e o salto ainda é dado nas mesmas situações.
Entrada e Saída
Embora a entrada e a saída sejam uma parte fundamental da computação, não há uma forma de serem feitas em linguagem de montagem. Isto porque a forma de E/S funciona depende da configuração do computador e do sistema operacional em funcionamento, não apenas do tipo de processador que ele possui. Na seção de exemplo, o exemplo do Hello World usa chamadas de sistema operacional MS-DOS e o exemplo depois usa chamadas de BIOS.
É possível fazer E/S em linguagem de montagem. De fato, a linguagem de montagem pode geralmente expressar qualquer coisa que um computador é capaz de fazer. Entretanto, mesmo que haja instruções para adicionar e ramificar na linguagem de montagem que sempre farão a mesma coisa, não há instruções na linguagem de montagem que sempre façam E/S.
O importante é observar que a forma como as E/S funcionam não faz parte de nenhuma linguagem de montagem, pois não faz parte de como o processador funciona.