Vamos Fazer um Programa

No final do artigo anterior deixámos dito que se arranjou uma forma de substituir os Opcodes por mnemónicas que os representam e que são mais facilmente entendíveis por nós humanos, criando assim uma linguagem de programação de muito baixo nível (muito próxima do código máquina) a que se chama Assembly. O programa em Assembly  é essencialmente uma sequência de mnemónicas que correspondem a uma sequência de Opcodes, enriquecido com mais algumas opções simples tendentes a facilitar a vida aos programadores. Vamos ver como

Assembly

A linguagem Assembly não é universal. Cada fabricante pode ter os seus mnemónicos próprios de acordo com a arquitetura da sua UCP. Na realidade encontraram forma de uniformizar grande parte das UCP com um Assembly específico (o X86 para UCP de 32 bits e o X86-64 para UCP de 64 bits). Mas isto é a realidade.

A nossa UCP é de 8 bits e feita à medida para o nosso objetivo. Vamos utilizar um Assembly do tempo dos 8 bits a que vamos chamar Z80-LC. O LC é de Lógica do Computador e só pretende justificar alguma pequena adaptação.

Então vamos escolher um conjunto de 16 instruções com base nas que poderemos vir a necessitar para a elaboração de um pequeno programa que vamos desenvolver de seguida. Para esse programa vamos precisar de ler da MD, escrever na MD, subtrair, comparar, executar saltos condicionais (Zero, menor que Zero e menor do que) e executar saltos incondicionais. Embora não vamos utilizar, vamos incluir no conjunto a soma, a multiplicação e a ausência de operação.

Assim teremos o Conjunto de Instruções representado na tabela da Figura 14, associado ao valor que o opcode,  os sinais de comando e o código binário final encontrado pelo descodificador devem assumir para cada uma das instruções. Sempre que aparece a indicação X ou XX é porque esses sinais de comando e portanto os respetivos bits não são relevantes para a instrução. Porque o irrelevante não tem representação atribuímos-lhes o valor 0 no resultado final. A tradução dos mnemónicos é como segue:

  • LD (LoaD) significa Carrega.
  • ADD significa Soma.
  • SUB significa  Subtrai.
  • MUL significa  Multiplica
  • CP (ComPare) significa Compara.
  • JP (JumP) significa Salta e corresponde a um salto incondicional.
  • JP seguido de Z, N ou M, significa Salta e corresponde a um salto condicional.
  • NOP (NoOPeration) significa Sem Operação, isto é, não faz nada

Na Tabela da Figura 14 representamos as  Mnemónicas Assembly e os correspondentes Opcode, Sinais de Comando  e Resultado Binário. Os valores de valor e endereço referidos na Tabela são fornecidos pela constante da instrução. Quando é referido o endereço da MD pretende-se referir o valor contido nessa posição da MD.

Tabela de correspondência do Opcode com os mnemónicos Assembly. Opcode e correspondente valor descodificado. Valor e endereço são representados pela constante na intrução. Valor é um valor fixo com que se vai operar e é introduzido pela constante. Endereçp é o endereço do local da MD onde se encontra o valor com que se pretende operar e é introduzido pela constante. A é a notação abreviada para o RegA.
Figura 14

Assembler

O Assembler é um interpretador de Assembly que cria o código máquina convertendo as mnemónicas do Assembly em opcodes. Mas faz mais do que isso. O Assembler dispõe de diretivas que uma vez implementadas no Assembly o levam a executar determinados procedimentos.

No nosso pequeno programa Assembly   vamos utilizar a diretiva EQU (de Equal ou Igual). Esta diretiva não gera instruções em código máquina, limitando-se a indicar ao Assembler que a constante simbólica que aparece antes da instrução tem o valor indicado. A sua utilização evita que o programador tenha sempre que escrever os mesmos valores em vários locais. Para além de que o nome que lhes possa ser atribuído, tendo algum significado para o programador, permitir-lhe-á  saber com mais clareza os locais onde deve inseri-las no programa. Para além ainda de que, se por qualquer motivo o valor da constante tiver que ser alterado pelo programador ele só o fará uma vez, alterando a diretiva. Quando da interpretação do programa, o Assembler substitui o nome da constante pelo seu valor.

Para a definição dos endereços de salto o Assembler introduziu o conceito das etiquetas. Estas devem ser colocadas antes das instruções do programa Assembly para as quais eventualmente possam ser efetuados saltos. Quando da interpretação do programa, o Assembler atribui a cada etiqueta o endereço correto da MI em que essa instrução se encontra, substituindo-as depois nos locais onde são referidas no programa, pelo seu valor. Assim, sempre que forem feitas alterações ao programa, não é necessário modificar todos os endereços nas instruções de salto. Quando o Assembler interpretar o programa atribui os novos valores às etiquetas.

Um programa

Vamos desenvolver um programa que calcula o fatorial de um número, partindo da determinação do algoritmo da solução para a sua conversão num programa em Assembly e escrevê-lo posteriormente em C.

Para a sua execução vamos seguir o caminho contrário decompondo as instruções C em Assembly, estas em opcodes e finalmente em quadros gráficos da UCP ilustrando a operação máquina e evidenciando os circuitos nela intervenientes.

O fatorial de um número é o valor resultante da multiplicação desse número sucessivamente por todos os que se seguem até 1. Exemplificando com o fatorial de 4

4! = 4 x 3 x 2 x 1 = 24

Temos que multiplicar 4 por 3 (4-1) e depois multiplicar o resultado por 2 (3-1). A multiplicação por 1 dispensa-se uma vez que não produz efeitos. Assim, acabamos a operação quando multiplicamos por 2.

O Algoritmo do Programa

Para a determinação do algoritmo simulámos esta operação numa calculadora que só faz uma operação de cada vez,  provida com 2 memórias, M1 e M2. Utilizámos 2 variáveis, fatorial e temporário e realizámos a operação por iterações.

A variável fatorial representa o resultado do cálculo no final de cada iteração. A variável temporário representa o valor por que multiplica fatorial em cada iteração.

Com estes pressupostos teremos então a seguinte descrição para o algoritmo:

  1. Atribuímos a uma constante N o valor do número de que se pretende calcular o fatorial.
  2. Definimos M1 como a memória em que guardamos o valor de fatorial em.
  3. Definimos M2 como a memória em que guardamos o valor de temporário.
  4. Escrevemos o valor de N, aquele de que queremos calcular o fatorial, no ecrã.
  5. Guardamos o valor do ecrã (4)  na M1, que assim passa a conter o valor de fatorial.
  6. Decrementamos (subtraímos 1) o valor no ecrã.
  7. Guardamos o valor do ecrã na M2, que assim passa a conter o valor de temporário.
  8. Comparamos o resultado no ecrã com 2.
  9. Se o valor no ecrã for menor do que 2 salta para o passo 14.
  10. Multiplicamos o valor no ecrã pelo valor na M1.
  11. Guardamos o resultado que está no ecrã na M1. Este é o valor do cálculo do fatorial no final desta operação.
  12. Colocamos no ecrã o valor temporário que está na M2.
  13. Voltamos a repetir a operação  a partir do passo 6. Salta para 6.
  14. Está concluída a operação. O valor fatorial guardado na M1 é o valor final do cálculo do fatorial de N.

O Programa em Assembly

Figura-7-43-ProgrAssembly
Figura 16a – O Programa em Assembly

Fctr e Temp são os endereços da MD a que correspondem as suas posições [Fctr] e [Temp], onde estão guardados os valores de fatorial e temporário e que vão assumir o lugar de M1 e M2 da máquina de calcular. O RegA, que no Assembly é designado só por A, vai assumir o papel do ecrã da maquina de calcular. Vamos usar a diretiva EQU para atribuir valores a N e aos endereços Fctr e Temp.

Podemos vre na Figura 16a o programa em linguagem Assembly:

 Nas primeiras 3 linhas do programa Assembly podemos verificar a utilização da diretiva EQU para atribuir valores a constantes que depois são utilizadas ao longo do programa. Também podemos verificar a utilização de etiquetas antes de algumas instruções, como é o caso de Repete e Fim, para que vão ser efetuados saltos durante o programa.

O Programa em Código Máquina

Figura-7-44-DiretivasAssembler
Figura 16b

O Assembler, ao interpretar o Assembly, começa por estabelecer valores para as diretivas e para as etiquetas, como se pode ver na Figura 16b.

O Assembler lê todo o programa em Assembly, verifica a existência de instruções de salto com etiquetas e por isso atribui a essa etiquetas o valor das posições da MI onde elas se encontram.

Figura-7-45-Assemb-CodMaq
Figura 16c

As restantes atribuições vêm no cumprimento de diretivas EQU.

Depois o Assembler cria as instruções máquina com a correspondência que se pode ver na Figura 16c.

Agora é só olharem para a coluna da esquerda e imaginarem-se a escrever um programa em código máquina sem a ajuda de mnemónicas.

O Programa em Operações na Máquina

Figura-7-46-OperMaq
Figura 16d – As operações na máquina

Vamos ver a quantas operações na máquina corresponde este programa:

Se este programa fosse por exemplo para o cálculo do fatorial de 10, a máquina executaria mais 48 operações, pois teria de repetir mais 6 vezes um ciclo iterativo de 8 instruções, que no nosso caso é repetido 4 vezes. Assim podemos concluir que as soluções de saltos e repetições dos programas são executadas pela máquina em operações individuais por cada passagem.

Vamos voltar à analogia com o  nosso comboio miniatura. Suponhamos que queríamos que ele desse 50 voltas seguidas a 8 percursos diferentes dentro do circuito. Nós definíamos assim o seu trabalho:

  1. Percorre o percurso 1
  2. Percorre o percurso 2
  3. Percorre o percurso 3
  4. Percorre o percurso 4
  5. Percorre o percurso 5
  6. Percorre o percurso 6
  7. Percorre o percurso 7
  8. Percorre o percurso 8 (neste percurso o comboio aciona uma alavanca que incrementa um valor num visor).
  9. Se o valor no visor for 50 podes parar.
  10. Volta ao princípio.

Nós conseguimos resumir em 10 instruções a tarefa que queremos que ele execute, mas o coitado do comboio não consegue resumir as 400 voltas que vai ter que dar ao circuito pelos 8 diferentes percursos para cumprir a tarefa.

Na tabela da Figura 16 representamos todas as operações da UCP estabelecendo o relacionamento de cada uma com

  • o seu passo do algoritmo,
  • a sua instrução Assembly,
  • a sua instrução máquina (opcode/constante),
  • o valor em [Fctr],
  • o valor em [Temp],
  • o valor no RegA e
  • o valor da constante na instrução em código máquina
Figura 1-16 Tabela com a correspondência entre o algoritmo, o programa em Assembly, o programa em código máquina e as operações máquina.
Figura 16
 O Programa em C

Vamos escolher C para verificarmos como ficaria este programa  escrito numa linguagem de alto nível. O nível de uma linguagem de programação define-se na proporção inversa à proximidade das suas instruções com o código máquina. Quanto maior a correspondência entre as instruções e o código máquina menor o nível da linguagem, e quanto menor a correspondência maior o nível da linguagem.

Um programa C é composto por funções. As instruções de cada função estão sempre compreendidas dentro das chavetas {} que definem o conteúdo da função. As funções são pedaços de programa que executam tarefas específicas e que são chamadas a executarem essa tarefa durante a execução do programa. Na forma definidora de uma função em C – tipo nome (argumentos)os diversos elementos dizem-nos:

  • O tipo  do valor retornado pela função, i.e. se é um int, (inteiro) uma char (caratere), um bool (1 ou 0), etc.
  • O nome da função, que pode ser qualquer um exceto main (principal). A função main é sempre procurada e executada por um programa C quando arranca.
  • Os argumentos da função, i.e. os valores de determinadas variáveis que a função vai incluir na sua tarefa.

Um programa C quando é executado começa por procurar a função main e executa-a. As restantes funções são chamadas de dentro desta e em sequência de dentro de cada uma delas, retornando sempre àquela que chamou, até chegar de novo à principal.

Figura-7-15
Figura 15

O gráfico da Figura 15 tenta ilustrar a forma de chamada de uma função e aquilo que lhe é pedido. A função
int mult (x,y)
que faz a multiplicação dos argumentos x por y é chamada de dentro de qualquer outra referindo
mult (4,3)
isto é, perguntando-lhe: Quanto é  4 vezes 3? Como retorno recebe a execução da tarefa da função pretendida com os dois valores que enviou em argumento, ou seja
int a = z = 4*3
ou seja, a resposta à pergunta: São 12.

 Após esta muito breve e ligeira introdução, podemos desenvolver o nosso programa em C que poderá ficar como na Figura 15a.

Figura-7-47_prg_C
Figura 15a

A função main tem definido o tipo int (inteiro) como sendo o tipo do valor que retorna. Na realidade main não retorna nada para ninguém, por isso convencionou-se que quando main retornasse 0, indicava que o programa tinha sido corretamente executado. Se o retorno for diferente de 0 é porque houve um erro. Daí a última instrução do programa (dentro da função main) ser:
return(0);
No início, o programa faz a atribuição do valor 4 a uma constante simbólica N
#define N 4
e declara e inicializa duas variáveis do tipo int.
int fatorial = N;
int temporário = N-1;

Para a UCP não existe o valor de uma variável, somente um endereço de memória com um determinado nome onde um dado valor está guardado. Quando em C dizemos int fatorial = N, o compilador reserva um espaço em memória para um tipo int, regista o endereço desse espaço e atribuí-o a um apontador [Fctr] para uma variável com o nome fatorial. Depois coloca nesse espaço o valor da constante simbólica N, i.e. inicializa  o valor da variável fatorial preenchendo o seu espaço em memória com um valor.

O mesmo acontece  com a instrução int temporário = N-1 e com a variável temporário.

Se estas duas variáveis não tivessem sido declaradas qualquer referência que lhes fosse feita reportaria um erro, pois quando o compilador procurasse o endereço para tais nomes verificaria que não constavam da lista de endereços atribuídos em memória.

Portanto, em C, quando referenciamos uma variável, referenciamos um apontador para o local da memória onde a mesma se encontra guardada.

Só a título de informação,  C permite ao programador a alocação direta desses espaços. Não é questão para aqui. Quando tal não é assim feito, o compilador encarrega-se de criar os espaços e referenciá-los.

Já referimos várias vezes o compilador. O Compilador é um programa que faz a conversão de código fonte, isto é, de um programa escrito numa linguagem de alto nível, em código máquina.

Passemos à instrução seguinte:
while (temporário ≥ 2){fatorial=fatorial*temporário; temporário –;}
que é uma instrução do tipo
while (this is true) {do that}
que se pode traduzir por
enquanto (isto é verdade) {faz aquilo}
em que isto é o predicado a ser avaliado e  aquilo as declarações  a serem executadas. While representa um ciclo iterativo que consiste em executar repetitivamente as declarações que estão dentro do corpo da instrução. O corpo da instrução é composto pelas declarações que estão contidas entre as chavetas {} que se seguem a esta instrução. De cada vez que o corpo da instrução é executado e antes de ser repetida a sua execução, while verifica se a avaliação do predicado devolve verdadeiro. Se sim, lá vai ele dar mais uma voltinha. Se não, salta fora do corpo e continua o programa na instrução seguinte.

O ciclo começa com o valor inicial de temporário
int temporário = N-1;
e as declarações a executar dentro do ciclo são
fatorial=fatorial*temporário;
que atribui à variável fatorial um valor igual ao produto dela própria pelo valor de temporário e
temporário –;
que decrementa o valor de temporário, i.e. diminui-lhe uma unidade. Quando o valor de temporário for menor do que 2, o programa salta para fora do corpo desta instrução e continua a sua execução, neste caso para a instrução final
return(0);

A introdução destes conceitos de C tem a ver com a associação de um programa numa linguagem de alto nível ao desenvolvimento que vamos fazer de seguida. Não pretende ser uma iniciação a C nem esse é o nosso objetivo, pelo menos para já. Mas pretende que entendam o que estamos a fazer.