1.3.3
A Máquina Virtual Java
A máquina Virtual Java é um software do usuário capaz de interpretar
bytecodes Java. O termo interpretar é um pouco diferente de executar. A
execução de um código só é feita pela máquina real. Os códigos
interpretáveis são códigos binários de uma máquina que não existe,
idealizada em software e que são inteligíveis para um software simulador de
uma arquitetura.
Linguagem de Montagem
Já vimos os fundamentos da programação de computadores, sob o ponto
de vista da inteligibilidade dos comandos. Sabemos também que a
programação em baixo nível, principalmente em linguagem de máquina, é
difícil e de compreensão árdua para nós seres humanos, embora seja
inteligível para a máquina.
Neste capítulo vamos conhecer os comandos de linguagem de
montagem que nos permitem escrever programas para serem executados no
MIPS. Nossa abordagem busca aprender os principais comandos divididos por
categorias funcionais: aritmética; transferência de dados; suporte à tomada de
decisão; e a procedimentos.
Vamos iniciar pensando um pouco em forma de analogia: aprender um
idioma, de uma máquina ou não, requer conhecer sua palavras. Um dicionário
é um guia importante para quem está aprendendo as primeiras palavras. Assim
como um idioma, uma linguagem de montagem também sofre constantes
transformações, normalmente levando a um novo produto no mercado. Um
exemplo não muito distante foi a introdução do conjunto MMX nas máquinas
Pentium.
A expressividade de um idioma é medida pelo número de verbetes
conhecidos da língua. Não se pode afirmar aqui que um indivíduo falante do
português, com um vocabulário de 15.000 palavras, seja alguém que não tenha
muitas habilidades de comunicação. De fato um adulto usa entre 13.000 e
14.000 palavras no cotidiano e um dicionário pequeno contem 150.000
verbetes. Para termos uma idéia, o vocabulário possível de um computador,
que usa palavras de tamanho fixo de 32 bits, é de apenas 4.294.967.296
palavras. Nada mal, não é mesmo? Entretanto, no caso da máquina, existe uma
necessidade de tornar esta expressividade uma realidade, ou seja, é preciso
fazer com que um hardware possa executar estas instruções e que exista um
compilador/montador eficiente para gerar o código de máquina
correspondente. Sob este prisma, a realidade é bem diferente. Vamos aprender
ao longo deste texto que a simplicidade favorece o desempenho, então o
conjunto de instruções que utilizaremos é de cerca de uma centena de palavras
com suas flexões.
2.2 – A visão do software – Operandos e Operadores
O nome computador nos leva a pensar de imediato em uma máquina
que computa, ou seja, calcula. Certamente uma forte ênfase em cálculos é
desejada em um computador, o que implica que ele precisa saber realizar
operações aritméticas. Ora, uma operação aritmética é composta de
operadores e operandos. Os operadores básicos são: soma, subtração,
multiplicação e divisão. Nós utilizamos parcelas em pares para guardar os
operandos. As parcelas recebem nomes em particular, por exemplo, numa
subtração existe o minuendo, o subtraendo e o resto (ou diferença), que é o
resultado da operação. Na soma, as parcelas são denominadas ‘parcelas’.
Enfim, mesmo quando estamos realizando uma soma com três parcelas, nós
fazemos as contas primitivas com dois algarismos, invariavelmente. Esta
abordagem é seguida pela máquina em seus comandos aritméticos.
Um dado em binário não carrega consigo qualquer informação
semântica, ou seja, uma seqüência de zeros e uns não indica se tratar de um
número sinalizado, um número não sinalizado, uma cadeia de caracteres ou
um endereço. Quem dá significado ao operando é a operação. Uma operação
que usa operandos sinalizados vai interpretar seus operandos como sendo
números sinalizados. Uma outra operação, que usa operandos não
sinalizados vai interpretar os mesmos, como números não sinalizados. Por
exemplo, o número binário 111111111111111111111111111111102
pode ser interpretado como -2 ou 4.294.967.294. Isto só depende de qual
operação o utiliza.
Finalmente, existem classes de operações: lógica e aritmética;
transferência de dados; suporte à tomada de decisão; e suporte a
procedimentos. Estas classes têm um certo impacto em como cada instrução
será executada pelo processador.
add $8, $9, $10
Veja que os números dos registrados são precedidos de um caractere $ para
não restar dúvidas que se trata de um operando que está no banco de
registradores e não de um operando imediato.
Um dos problemas encontrados com esta forma de codificar é sua
rigidez. Aqui não há espaços para interpretações dúbias. Devemos escrever
exatamente de forma inteligível para o montador sob pena de nosso código
de máquina gerado não realizar a tarefa desejada. Agora vamos analisar
uma expressão aritmética, comum em HLLs, para sabermos como ela pode
ser escrita em linguagem de montagem. A expressão seria:
a = b + c + d + e
Considerando as variáveis a a e associadas aos registradores 8 a 12
respectivamente, podemos escrever:
add $8, $9, $10 # a = b + c
add $8, $8, $11 # a = a + d => a = (b+c) + d
add $8, $8, $12 # a = a + e => a = (b+c+d) + e
Veja que foi necessário fracionar a soma em diversas fases, tomando
proveito da propriedade da associatividade da soma. Surgiu também na
codificação uma espécie de ajuda após a instrução. O símbolo # indica para
um montador que o restante da linha deve ser menosprezado. Isto permite ao
programador explicitar sua idéia para um colega de trabalho e/ou para ele
mesmo, quando precisa lembrar exatamente como um problema foi resolvido.
Usamos estes campos de comentários para explicitar as variáveis, já que as
variáveis foram associadas a registradores anteriormente a confecção do
código.
Nosso próximo passo é conhecer as instruções de multiplicação e
divisão. A implementação de uma multiplicação em hardware não é tão
simples como um somador/subtrator. A multiplicação costuma ser muito
demorada para ser executada e devemos tentar evitá-la ao máximo. A divisão
é ainda mais lenta e complexa. Entretanto, não vamos simplesmente abdicar
delas, mas usá-las com restrições, onde elas não podem ser substituídas por
expressões mais simples.
Vamos começar pensando em uma multiplicação de dois números de 4
algarismos cada. O resultado desta multiplicação ficará, provavelmente, com
8 algarismos. Isto significa que ao multiplicarmos um número com n
algarismos por outro com m algarismos, o resultado será um número com
n+m algarismos. No caso do MIPS, a operação de multiplicação será
realizada sobre dois números de 32 bits. Isto implicará em um resultado de
64 bits. Ora, nenhum registrador tem 64 bits, então para tornar esta
operação exeqüível, dois registradores extras foram criados: HI e LO, ambos
de 32 bits. HI e LO, usados em conjunto, propiciam que resultados de 64 bits
sejam armazenados nele. Assim, a multiplicação tem um destino fixo e
obrigatório: o par HI, LO. Por isto ao multiplicarmos dois números
precisamos apenas especificar quais os registradores que os guardam. A
operação é mult. Exemplo:
mult $8, $9 # HI, LO = $8 x $9
O registrador HI guarda os 32 bits mais significativos (mais à esquerda) do
resultado, enquanto o registrador LO guarda os 32 bits menos significativos
(mais à direita). Em nenhuma ocasião é tratado overflow. O desenvolvedor
do software deve prover mecanismos suficientes para evitá-lo. mult
considera os dois operandos como sendo números sinalizados. Vamos supor
o exemplo acima, onde $8 contem o valor 16 e $9 contem 2.147.483.647. O
resultado da multiplicação, em binário, com 64 bits, seria:
0000 0000 0000 0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 00002
O registrador HI receberia então:
0000 0000 0000 0000 0000 0000 0000 01112
e o registrador LO receberia:
1111 1111 1111 1111 1111 1111 1111 00002.
Se o valor de $8 fosse –16, o resultado final seria:
1111 1111 1111 1111 1111 1111 1111 1000 0000 0000 0000 0000 0000 0000 0001 00002