Trabalho de Programação 1998/1999
Notas
-
A resolver em grupo.
-
Leia atentamente todo o enunciado.
-
A resolução deste problema deve ser entregue
até sexta-feira, 22 de Janeiro de 1999.
-
A resolução deve ser entregue pessoalmente
a um dos docentes da cadeira e consta de uma disquete, devidamente identificada,
contendo apenas os ficheiros onde se encontra o código C++
(extensão .cpp) e o código assembly desenvolvido
(extensão .asm).
-
A entrega do problema fora do prazo implica a penalização
de um valor por cada dia útil de atraso (i.e., 23, 24 ou 25 de Janeiro,
-1 valor; 26 de Janeiro -2 valores; 27 de Janeiro -3 valores; 28 de Janeiro
-4 valores) não se aceitando trabalhos após quinta-feira,
28 de Janeiro de 1999.
1 Objectivo
O objectivo deste trabalho é desenvolver em C++ um simulador de
um computador com um pequeno conjunto de instruções e, usando
um simples assemblador (assembler) que será fornecido brevemente,
desenvolver um programa em assembly para esse mesmo computador.
O computador que será simulado não corresponde a nenhuma
implementação real, muito embora fosse simples de concretizar
em hardware. A esse computador chamar-se-á XIM.
2 Descrição do computador XIM
O computador XIM é constituído por um processador, contendo
um pequeno número de registos (16 registos, de nomes
A
a P, e ainda um registo especial PC), por uma memória
(com 216 posições), e por dispositivos de entrada
e de saída. Os registos podem ser encarados como posições
de memória especiais sobre as quais é possível efectuar
operações aritméticas, não sendo possível
efectuar quaisquer operações aritméticas directamente
sobre as posições de memória. As únicas
operações que é possível efectuar sobre posições
de memória são as de transferência de conteúdos
entre um registo e uma posição de memória e vice-versa.
A memória pode ser encarada como um vector de palavras, sendo cada
palavra constituida por 32 bits, representados aqui por b31
a b0. Os registos têm exactamente a mesma
constituição (com excepção do registo PC,
que tem apenas 16 bits). Esses padrões de bits
tanto podem representar valores inteiros (em complemento para 2, se interpretado
como um inteiro com sinal, sendo b31 o bit mais
significativo) como instruções a serem executadas pelo processador.
Assim, a memória contém simultaneamente as instruções
que constituem os programas a executar bem como, possivelmente, dados necessários
à execução do programa (i.e., o XIM tem uma arquitectura
de Von Neumann). É muito importante perceber que, dada uma
posição de memória arbitrária, o seu conteúdo
é simplesmente um padrão de bits, que pode ser interpretado
de formas diferentes, nomeadamente como um número inteiro ou como
uma instrução para o processador.
2.1 A memória
A memória é constituída
por 216 palavras de 32 bits. Cada posição
(palavra) tem um endereço. Os endereços válidos
são todos os inteiros entre 0 e 216 - 1. Isto é:
Memória
Endereço
|
0
|
1
|
2
|
...
|
216 - 2
|
216 - 1
|
Palavras
|
p0
|
p1
|
p2
|
...
|
p216-2
|
p216-1
|
Como é óbvio, são necessários exactamente
16 bits para endereçar todas as posições válidas
de memória. Um endereço pode assim consistir num padrão
de 16 bits interpretados como a representação binária
(na base 2) de um número inteiro sem sinal (o endereço).
Cada posição de memória
corresponde a uma palavra de 32 bits. Isto é:
Em que cada bit pode valer 0 ou 1 (bit = binary digit).
Estes bits formam padrões que podem ser interpretados de
formas diferentes. Se forem tomados como instruções
para o processador, são interpretados como se indica em 2.4, se
forem tomados como inteiros sem sinal, são interpretados como uma
representação em numeração binária,
etc.
2.2 Os registos
Existem 16 registos básicos que consistem em palavras de 32
bits
sobre as quais se podem efectuar várias operações
descritas mais à frente. Estes registos chamam-se A,
B,
C,
D, E, F, G,
H,
I,
J, K, L, M,
N,
O
e P. Também se podem referir os registos usando números
inteiros, sendo o registo A correspondente ao número 0,
o B ao número 1, e assim sucessivamente até ao registo
P, com o número 15. Os registos
I (8) e
O (14) são especiais no sentido em que estão associados
a operações de leitura e escrita nos dispositivos externos.
O registo A (0) é também especial por ser apenas
nele que se efectuam as operações de soma e subtração.
Finalmente, existe o registo PC (de program counter),
com 16 bits apenas, que indica qual o endereço da posição
de memória que contém a próxima instrução
a ser executada.
2.3 Entradas e saídas
Assume-se que existe uma fonte inesgotável de padrões de
32 bits (a entrada) e um escoadouro, também com capacidade
de escoamento infinita, dos mesmos padrões (a saída).
Pode-se imaginar quer a entrada quer a saída como duas fitas infinitas
de papel: a da entrada coberta de padrões de bits e a da
saída vazia, mas onde se podem escrever tantos quantos se desejar.
No simulador estas entradas e saídas serão associadas ao
teclado e ao ecrã respectivamente, como se verá mais à
frente.
2.4 O processador
O processador tem um papel muito simples: executar cegamente as instruções
que encontrar em memória. Que instruções existem
e como são representadas em memória é o assunto desta
secção.
2.4.1 As instruções
As instruções no XIM são compostas por uma operação
e, possivelmente, pelos respectivos operandos. As operações
descrevem-se em seguida (em termos do seu efeito no estado do computador
e das entradas e saídas). No caso das operações
com argumentos, estes apresentam-se em itálico, significando r
um registo básico (de A a P) e e um endereço
de memória (de 0 a 216 - 1).
-
noop - Esta operação não tem qualquer efeito.
-
load r1 e - Copia para o registo r1
o conteúdo da posição de memória endereçada
por e. Por exemplo, load C 14 coloca no registo C
o conteúdo da posição de memória 14.
-
store r1 e - Copia para a posição
de memória endereçada por e o conteúdo do registo
r1.
Por exemplo, store B 10 coloca na posição de memória
10 o conteúdo actual do registo B.
-
loadi r1 r2 - Copia para o
registo r1 o conteúdo da posição
de memória cujo endereço está no registo r2
(o
'i' em loadi significa "indirecto" ou "indexado"). Por exemplo,
loadi C B, se B contiver 21, coloca no registo
C
o conteúdo da posição de memória 21.
-
storei r1 r2 - Copia para
a posição de memória cujo endereço está
no registo r2 o conteúdo do registo r1
(o
'i' em storei significa "indirecto" ou "indexado"). Por
exemplo, storei B F, se F contiver 1021, coloca na posição
de memória 1021 o conteúdo actual do registo B.
-
add r1 - Soma o conteúdo dos registos
A
e r1 e guarda o resultado no registo A.
-
sub r1 - Subtrai o conteúdo do registo
r1
do conteúdo do registo A e guarda o resultado no registo
A.
-
zero r1 - Coloca todos os bits do registo
r1
a zero (ou seja, se o seu conteúdo for interpretado como um inteiro,
coloca zero no registo).
-
incr r1 - Incrementa o conteúdo do registo
r1
interpretado como um inteiro.
-
decr r1 - Decrementa conteúdo do registo
r1
interpretado como um inteiro.
-
neg r1 - Nega todos os bits do registo
r1
(i.e., todos os 1 passam a 0 e vice-versa).
-
jz r1 e - (jump if zero) Se o
conteúdo de r1 for 0 (zero), interpretado como
um inteiro, carrega o registo PC com o endereço e,
i.e., a próxima instrução a executar será a
contida nesse endereço. Caso contrário simplesmente
incrementa o conteúdo de PC.
-
jn r1 e - (jump if negative) Se
o conteúdo de r1 for menor que 0 (zero), interpretado
como um inteiro com sinal, carrega o PC com o endereço e,
i.e., a próxima instrução a executar será a
contida nesse endereço. Caso contrário simplesmente
incrementa o conteúdo de PC.
-
jump e - (jump if negative) Carrega o PC
com o endereço e, i.e., a próxima instrução
a executar será a contida nesse endereço.
-
read - Lê da entrada um padrão de bits que
guarda no registo I (input). A "fita" correspondente
às entradas avança de modo que a próxima instrução
read
a executar leia o padrão seguinte.
-
write - Escreve na saída o padrão de bits
contido no registo O (output). A "fita" correspondente
às saídas avança de modo que a próxima instrução
write
a executar escreva na próxima posição vazia da "fita".
É de notar que todas as operações, com excepção
das que envolvem saltos (jump, jz e jn), têm
o efeito adicional de incrementarem o PC, de modo a que a próxima
instrução a executar seja a subsequente na memória.
Os padrões de bits, no caso das operações
aritméticas, são interpretados como inteiros com ou sem sinal
em representação binária, é irrelevante (é
uma característica da representação em complemento
para dois que o padrão de bits resultante é exactamente
o mesmo!).
São de notar aqui as operações que manipulam o
PC,
pois permitem fazer "saltos", i.e., podem ser usadas para implementar ciclos,
e as instruções de acesso indexado à memória,
pois permitem tratá-la (ou parte dela) como se de uma matriz (array)
se tratasse.
Finalmente, note-se que, no caso dos acessos indexados à memória
(loadi e storei) se usa uma palavra de 32 bits
para endereçar a memória, quando apenas são necessários
16. O que acontece nesse caso é que apenas são usados
os 16 bits menos significativos do registo que contém endereço
(o índice).
2.4.2 O código máquina
As instruções apresentadas
são representadas como padrões de 32
bits. Cada
palavra de 32 bits é dividida (na sua interpretação
como uma instrução), nas seguintes partes:
operação
|
r1
|
r2
|
(não usada)
|
e
|
b31
|
b30
|
b29
|
b28
|
b27
|
b26
|
b25
|
b24
|
b23
|
b22
|
b21
|
b20
|
b19
|
b18
|
b17
|
b16
|
b15
|
b14
|
b13
|
b12
|
b11
|
b10
|
b9
|
b8
|
b7
|
b6
|
b5
|
b4
|
b3
|
b2
|
b1
|
b0
|
Em que r1, r2 e e têm
o mesmo significado que na descrição das operações
feita anteriormente. É de notar que algumas operações
não fazem uso de algumas partes da palavra. Por exemplo, a
operação noop não faz uso de nenhum dos possíveis
operandos, e portanto só mesmo os 4 bits mais significativos
da palavra são relevantes (isto significa que existem 228
versões da instrução que não faz nada!).
A operação é, assim,
representada pelos bits b31, b30,
b29
e b28. Se esses quatro
bits forem interpretados
como um valor inteiro temos a seguinte correspondência:
padrão de bits
(b31, b30, b29,
b28)
|
inteiro correspondente
(b31b30b29b28)2
|
operação representada
|
0000
|
0
|
noop
|
0001
|
1
|
load
|
0010
|
2
|
store
|
0011
|
3
|
loadi
|
0100
|
4
|
storei
|
0101
|
5
|
add
|
0110
|
6
|
sub
|
0111
|
7
|
zero
|
1000
|
8
|
incr
|
1001
|
9
|
decr
|
1010
|
10
|
neg
|
1011
|
11
|
jz
|
1100
|
12
|
jn
|
1101
|
13
|
jump
|
1110
|
14
|
read
|
1111
|
15
|
write
|
Quanto aos registos, exactamente o mesmo tipo de representação
é usada, correspondendo o padrão 0000 (valor 0) ao registo
A,
o padrão 0001 (valor 1) ao registo B, ..., e o padrão
1111 (valor 15) ao registo P.
Os 16 bits do endereço e são interpretados
como um valor inteiro, sem sinal, correspondente a uma posição
de memória.
Exemplo
Qual a instrução correspondente ao número inteiro
268436567? Convertendo para representação binária
obtém-se (0001 0000 0000 0000 0000 0100 0101 0111)2,
ou seja, a operação é a correspondente ao padrão
0001, que, consultando a tabela acima, corresponde à operação
load.
Esta operação utiliza apenas o operando
r1,
que neste caso é o registo A (padrão 0000), e o operando
e, com valor (0000 0100 0101 0111)2 = 1111. Assim,
a instrução representada é
load A 1111.
A que número inteiro corresponde a instrução
read?
A operação read corresponde ao padrão 1110.
Mas esta operação não utiliza quaisquer operandos,
pelo que os restantes 28 bits são arbitrários.
Colocando-os a zero, obtém-se uma das 228 possíveis
representações da instrução read:
1110 0000 0000 0000 0000 0000 0000 0000, que corresponde, na notação
complemento para dois (e com 32 bits), ao inteiro -536870912.
2.5 A execução
Arranque
Quando o computador arranca assume-se que o registo PC contém
o endereço 0, i.e., a primeira instrução a executar
encontra-se na primeira posição de memória, e que
a memória foi "automagicamente" carregada com um programa a executar
(a parte da memória que não contenha o programa assume-se
inicializada com 0). Numa implementação prática
a memória deverá ser carregada antes do arranque da simulação
do computador virtual.
Execução
A execução processa-se instrução a instrução.
Em cada passo o padrão de bits guardado na posição
de memória endereçada pelo conteúdo do registo PC
(interpretado como um inteiro sem sinal) é executada, de acordo
com o que se indicou acima. Duma instrução normalmente
resulta normalmente uma alteração no estado do computador
(alteração da memória ou de registos ou das entradas
ou saídas). Essa alteração corresponde, salvo
em casos patológicos (quais?) a uma alteração do conteúdo
do registo PC, pelo que a instrução a ser executada
no próximo passo é, normalmente, diferente (salvo no caso
de saltos é a instrução subsequente na memória).
Paragem
Arbitra-se que o computador pára a sua execução logo
que PC contiver o endereço 216 - 1 (ou seja,
o endereço da última posição de memória).
Este método da paragem não tem paralelo em máquinas
reais, mas para uma máquina virtual, tem a vantagem de ser simples
e eficiente.
2.6 Um exemplo
Suponha que o seguinte programa (cada instrução é
representada por um inteiro de 32 bits com sinal) foi carregado
no ínicio da memória do XIM:
-536870912
671088657
268435473
-536870912
671088657
285212689
1912602624
-1073741813
1627389952
-2113929216
-805306361
1358954496
-1845493760
570425361
503316497
-268435456
-805240833
Este programa, traduzido para um formato mais compreensível, é:
read
store I 17
load A 17
read
store I 17
load B 17
zero C
jn A 11
sub B
incr C
jump 7
add B
decr C
store C 17
load O 17
write
jump 65535
Que acontece durante a execução do programa? Nas primeiras
linhas (até ao load B 17 inclusivé) lêem-se
simplesmente dois valores que se guardam nos registos A e B.
Em seguida, usando o registo C, calcula-se a divisão do
primeiro valor lido pelo segundo. É capaz de perceber como?
Se parece confuso, veja mais abaixo o mesmo programa em assembly e comentado.
3 A linguagem assembly
Para utilização neste trabalho, foi desenvolvida uma linguagem
assembly
muito simples, mas que permite escrever programas para o computador XIM
sem preocupações quanto à representação
em padrão de bits de cada instrução.
A breve trecho será distribuida um assemblador que, dado um programa
escrito na linguagem assembly definida abaixo, gera o conjunto de instruções
na sua representação inteira em complemento para dois (com
32 bits).
3.1 Mnemónicas
As instruções podem ser escritas na forma
operação lista_de_operandos
tal como se sugeriu já atrás. Por exemplo,
incr
G significa "incremente-se o valor contido no registo G". É
de notar que o assemblador distingue maiúsculas de minúsculas:
as operações devem ser sempre escritas em minúsculas
e os registos em maiúsculas. Na linguagem assembly
desenvolvida cada linha contém ou uma instrução (com
o formato anterior), ou um valor inteiro com sinal (representação
complemento para 2 em 32 bits), ou outras construções
a ver abaixo. O assemblador encarrega-se de converter instruções
e valores inteiros para a sua representação em padrão
de 32 bits e, posteriormente, de converter cada posição
do programa no inteiro com sinal correspondente.
3.2 Etiquetas
Para que não se tenham de definir explicitamente os endereços,
é possível usar etiquetas. Assim, linhas começadas
por :indentificador são interpretadas como significando
"crie-se a etiqueta chamada identificador com o valor correspondente
ao endereço da instrução subsequente". Estas
etiquetas podem depois ser usadas como qualquer endereço.
Por exemplo, poder-se-ia escrever
read
store I aux
load A aux
read
store I aux
load B aux
zero C
:inicio_do_ciclo
jn A fim_do_ciclo
sub B
incr C
jump inicio_do_ciclo
:fim_do_ciclo
add B
decr C
store C aux
load O aux
write
jump end
:aux
em vez da versão original, mais dificil de interpretar, e onde o
programador teve de se preocupar com escolher uma posição
de memória fora da zona do programa para fazer a transferência
dos valores (e teria de alterar manualmente esse valor sempre que o comprimento
do programa se alterasse!). Note-se que existe uma etiqueta especial
end,
pré-definida, e que tem como valor 216 - 1 = 65535, podendo
por isso ser usada para parar a execução dum programa.
3.2.1 Reservando memória
É possível "reservar memória" usando uma construção
da linguagem assembly que permite repetir instruções ou valores.
Por exemplo, as instruções assembly
:espaco
*100 0
...
atribuiriam à etiqueta espaco o endereço duma zona
de memória com 100 palavras inicializadas com zero
3.2.2 Constantes
É possível ainda atribuir valores explícitos às
etiquetas. Por exemplo, as instruções
assembly
seguintes
=fim end
=bits 32
criam duas etiquetas fim e bits, a primeira com o
mesmo valor que fim e a segunda com o valor 32.
3.3 Comentários
Consideram-se comentários todos os caracteres desde ; até
ao final da linha respectiva.
3.4 Exemplo
O exemplo apresentado anteriormente pode ser expresso duma forma mais clara
usando os mecanismos proprorcionados pela linguagem
assembly apresentada:
read ; cin >> I;
store I aux ; mem[aux] = I;
load A aux ; A = mem[aux];
read ; cin >> I;
store I aux ; mem[aux] = I;
load B aux ; B = mem[aux];
; // PC: A >= 0 && B > 0 && A = a
zero C ; C = 0;
:inicio_do_ciclo ; // CI: a = C * B + A && A >= -B
jn A fim_do_ciclo ; while(A >= 0) {
sub B ; A -= B;
incr C ; C++;
jump inicio_do_ciclo ; }
:fim_do_ciclo ; // CI && ~G: a = C * B + A && -B <= A < 0
; // ou seja
add B ; A += B;
decr C ; C--;
; // CO: a = C * B + A && 0 <= A < B
; // ou seja, A contém o resto da divisão
; // de a (valor inicial de A) por B, estando
; // em C o quociente.
store C aux ; mem[aux] = C;
load O aux ; O = mem[aux];
write ; cout << O;
jump end ; // o fim enfim!
:aux
Este programa em assembly, depois de traduzido para linguagem máquina
e apresentado na forma de insteiros com sinal resulta em:
-536870912
671088657
268435473
-536870912
671088657
285212689
1912602624
-1073741813
1627389952
-2113929216
-805306361
1358954496
-1845493760
570425361
503316497
-268435456
-805240833
Note que pode (e deve) usar o programa acima para testar o simulador do
XIM que desenvolver.
4 Implementação do simulador
Uma vez que será distribuído um assemblador, i.e., um tradutor
de assembly para linguagem máquina representada por inteiros
com sinal, o simulador ser capaz de carregar programas nesse formato e
de os executar. O simulador, ao correr, deverá permitir as
seguintes opções:
-
Carregar novo programa cujo nome seja dado pelo utilizador (ao arrancar
o simulador recomenda-se que inicialize a memória do XIM com zero,
o que corresponde a colocar na memória 216 noop).
Deve colocar PC a zero.
-
Executar XIM (colocar o PC a zero e executar instruções
até que o registo PC contenha 216 - 1).
-
Continuar execução do XIM (sem colocar o PC
a zero, executar instruções até que o registo PC
contenha 216 - 1).
-
Executar apenas a próxima instrução (avançar
um passo).
-
Colocar PC a zero (para recomeçar).
-
Ver os valores nos registos A a P e no PC (como
inteiros com sinal).
-
Ver os valores de uma parte da memória (como inteiros com sinal
e como instruções do XIM).
-
Terminar execução do simulador.
As entradas e saídas do XIM devem ser associadas ao teclado e ecrã
do computador (i.e., deve-se usar cin e cout).
4.1 Exemplo de interacção
Suponha que o programa da divisão de dois inteiros apresentado anteriormente
se encontra num ficheiro chamado divide.xim (a versão código
máquina, pois a versão assembly estaria no ficheiro divide.asm).
Apresenta-se um possível exemplo de interação com
o simulador do XIM (a negrito os valores introduzidos pelo utilizador
do simulador, a negrito e itálico os valores introduzidos pelo utilizador
como
resposta a leituras do programa executado pelo simulador, e a itálico
simplesmente os valores escritos pelo programa executado pelo simulador):
xim: ?
p nome - carregar programa do ficheiro 'nome'.
e - executar programa.
c - continuar execução.
a - avançar
um passo a execução.
z - coloca PC a zero.
r - mostrar registos.
m i n - mostrar n posições de
memória (começando em i).
s - sair.
xim: p divide.xim
xim: m 0 17
memória:
0: valor = -536870912 operação = read
1: valor = 671088657 operação = store
I 17
2: valor = 268435473 operação = load
A 17
3: valor = -536870912 operação = read
4: valor = 671088657 operação = store
I 17
5: valor = 285212689 operação = load
B 17
6: valor = 1912602624 operação = zero
C
7: valor = -1073741813 operação = jn
A 11
8: valor = 1627389952 operação = sub
B
9: valor = -2113929216 operação = incr
C
10: valor = -805306361 operação = jump
7
11: valor = 1358954496 operação = add
B
12: valor = -1845493760 operação =
decr C
13: valor = 570425361 operação = store
C 17
14: valor = 503316497 operação = load
O 17
15: valor = -268435456 operação = write
16: valor = -805240833 operação = jump
65535
xim: e
1000
3
333
xim: z
xim:
a
1000
xim: a
xim:
a
xim: a
7
xim:
r
registos:
A: valor = 1000 operação = noop
B: valor = 3 operação = noop
C: valor = 333 operação = noop
D: valor = 0 operação = noop
E: valor = 0 operação = noop
F: valor = 0 operação = noop
G: valor = 0 operação = noop
H: valor = 0 operação = noop
I: valor = 7 operação = noop
J: valor = 0 operação = noop
K: valor = 0 operação = noop
L: valor = 0 operação = noop
M: valor = 0 operação = noop
N: valor = 0 operação = noop
O: valor = 333 operação = noop
P: valor = 0 operação = noop
PC: 4
xim: c
142
xim: r
registos:
A: valor = 6 operação = noop
B: valor = 7 operação = noop
C: valor = 142 operação = noop
D: valor = 0 operação = noop
E: valor = 0 operação = noop
F: valor = 0 operação = noop
G: valor = 0 operação = noop
H: valor = 0 operação = noop
I: valor = 7 operação = noop
J: valor = 0 operação = noop
K: valor = 0 operação = noop
L: valor = 0 operação = noop
M: valor = 0 operação = noop
N: valor = 0 operação = noop
O: valor = 142 operação = noop
P: valor = 0 operação = noop
PC: 65535
xim:
s
4.2 Notas sobre operações bit-a-bit
Pode assumir que na sua máquina (provavelmente um PC) e no seu ambiente
de desenvolvimento (provavelmente o Visual-C++) os int e os unsigned
[int] têm 32 bits, os
short [int]
e os unsigned short [int] têm 16 bits, e
que os inteiros com sinal são representados em complemento para
dois (isto é verdade na maior parte da máquinas). Como,
dado um int com um valor qualquer (por exemplo lido do ficheiro
do programa), se pode saber qual a correspondente instrução
XIM, de acordo com a interpretação apresentada na Secção
2.4.2? Felizmente, o C++, embora sendo uma linguagem de nível
razoavelmente alto, proporciona acesso a informação de baixo
nível, como os bits individuais dos inteiros! Para
isso usam-se os chamados operadores bit-a-bit. Estes
operadores, por questões que têm a ver com a interpretação
dos valores negativos, só devem ser usados com tipos sem sinal.
Assim, se a instrução estiver guardada num int chamado
instrução_original,
deve-se começar por converter essa instrução para
unsigned:
int instrução_original;
// Aqui, presume-se, ler-se-ia instrução de qualquer lado
// por exemplo da memória na posição dada por PC!
unsigned instrução = unsigned(instrução_original);
Depois desta conversão é já possível utilizar
os operadores bit-a-bit. Por exemplo:
instrução >> 28 // deslocamento para a direita.
desloca o padrão de bits do valor contido na variável
28 posições para a direita, deitando fora os bits
que "caem" do lado direito (os unsigned só têm 32
bits!)
e inserindo zeros à esquerda. Assim, se instrução
contiver o valor decimal (268436567)10 = (0001 0000 0000 0000
0000 0100 0101 0111)2 esse deslocamento resulta num inteiro
com o valor (0000 0000 0000 0000 0000 0000 0000 0001)2 = (1)10,
que é o código da operação load.
Esta operação usa dois operandos: r1 (registo
onde carregar) e e (endereço de onde tirar o valor).
Que registo é especificado na instrução? E que
endereço? Se deslocar o padrão de bits para
a direita de apenas 24 bits, o padrão de bits correspondente
ao registo fica concentrado nos quatro bits menos significativos,
ou seja:
instrução >> 24
resulta no padrão 0000 0000 0000 0000 0000 0000 0001 0000.
Se se pudesse anular todos os bits com excepção dos
últimos quatro o padrão obtido seria a representação
binária do número do registo. Para isso utiliza-se
um mascaramento:
(instrução >> 24) & 0x0000000F // ou simplesmente 0xF (zeros à esquerda...)
O que significa 0x0000000F? É um valor literal que,
ao contrário de por exemplo 11, está especificado em hexadecimal,
i.e., na base 16 (0x não passa de um prefixo usado pelo
C++ para distinguir valores literais em hexadecimal de valores literais
em decimal). Por esta altura já aprendeu em Arquitectura de
Computadores quais as vantagens desta base para este tipo de operações.
O valor (F)16 = (15)10 = (0000 0000 0000 000 0000
0000 0000 1111)2. Como o operador & (e não
&&,
que é o e lógico!) faz o e
bit-a-bit
dos dois operandos, o resultado da expressão anterior é o
padrão 0000 0000 0000 000 0000 0000 0000 0000, que, interpretado
como um inteiro, tem o valor 0. Conclui-se portanto que o registo
da instrução é o registo
A. Relativamente
ao endereço, não é necessário fazer qualquer
deslocamento (porquê?) bastando mascarar a instrução
com 0xFFFF, ou seja
instrução & 0xFFFF
que, neste caso, resulta no padrão de bits 0000 0000 0000
000 0000 0100 0101 0111, cujo valor em decimal é 1111. Assim,
a instrução correspondente ao valor inteiro 268436567 é
load
A 1111.
Finalmente, pode-lhe ser útil saber que existe um operador no
C++ que nega todos os bits dum inteiro. É a operação
bit-a-bit
~ (não confundir com o símbolo ~ usado na notação
matemática das folhas da cadeira para representar a negação
lógica). Assim, ~ 0xF0F3 resulta em 0x0F0C.
4.3 Mais notas sobre a implementação
-
Recomenda-se a utilização de tipos enumerados para representar
operações e registos (são possíveis conversões
entre inteiros e tipos enumerados).
-
Utilize matrizes sempre que desejável (fará sentido ter 16
variáveis para os registos básicos?).
-
Tenha o cuidado de verificar erros (acessos fora da memória permitida,
etc.).
-
Antes de começar a escrever em C++, pense.
-
Construa primeiro as estruturas de dados de que necessita (utilize classes
sempre que justificável).
-
Estruture o programa com cuidado em módulos (métodos, funções
e procedimentos) que implementam as operações sobre as estruturas
de dados.
5 Programação em assembly
Serão fornecidos à posteriori enunciados de pequenos problemas
a serem resolvidos na linguagem assembly apresentada para o XIM.
Cada grupo deverá resolver um desse problemas (será publicada
uma lista dos grupos com a indicação do problema a resolver
por cada um), construindo o respectivo programa em assembly.
Esse programa deverá poder ser assemblado pelo assemblador fornecido
e executado pelo simulador do XIM desenvolvido pelo grupo.
6 Avaliação
A avaliação do trabalho terá em conta, não
só a correcta execução do programa, mas também,
e principalmente, a correcta estruturação dos dados e métodos,
funções e procedimentos que compõem o programa e ainda
a sua legibilidade.
A nota deste trabalho será dada apenas após uma discussão,
individual, com cada um dos elementos do grupo.
Nesta discussão
qualquer elemento do grupo terá de demonstrar um total conhecimento
do programa e ser capaz de operar as alterações que forem
pedidas. Nessa oral poderão também ser feitas perguntas
sobre a matéria em geral. A nota final dependerá
não só da qualidade do trabalho, mas também, e principalmente,
do conhecimento do mesmo programa e da matéria em geral e da capacidade
de resolver problemas em C++ demonstrados nessa discussão.
Quaisquer funcionalidades extra que não tenham sido pedidas no
enunciado, tais como menus sofisticados, interfaces com janelas, etc.,
não serão avaliadas.
7 Curiosidades
Pense nisto. Quando o simulador estiver a funcionar: há um
processador (x86) a executar um programa, escrito em C++ e traduzido por
um compilador (Visual C++), que simula um computador virtual (XIM) a executar
outro programa, que por sua vez foi escrito numa linguagem assembly
e traduzido para a linguagem máquina do computador virtual (XIM)
por um assemblador escrito em C++ e traduzido...... Confuso?
Pense bem no assunto... Lembre-se que o compilador (Visual C++) foi
escrito em C++ e muito provavelmente compilado por si próprio (ou
melhor, pela sua própria versão compilada por um outro compilador)!