Nesta seção serão apresentados os avisos importantes referentes ao trabalho 2.
Um simulador computacional de uma arquitetura é um programa capaz de executar softwares escritos para uma arquitetura "hóspede" (ou simulada) numa arquitetura "hospedeira", sendo idealmente capaz de ler, carregar e executar tais softwares de modo transparente no hospedeiro; formalmente, um simulador desse tipo é denominado uma máquina virtual. Um simulador busca se comportar de modo bem próximo a uma máquina real (física), e em geral possui módulos simulando a memória, dispositivos de entrada/saída e a CPU da arquitetura hóspede. Da mesma forma que um computador real, a máquina virtual executa instrução por instrução do programa sendo simulado e para tanto geralmente implementa todos os passos para executar uma instrução, isto é, busca, decodificação, execução, etc.
Além disso é comum que um simulador disponha de uma interface para permitir a inspeção, em tempo de execução, do programa sendo simulado e do estado atual da máquina virtual. Note que o simulador deve executar as instruções, contudo não é obrigatório que internamente ele se comporte exatamente como o hardware original da arquitetura hóspede: basta que a arquitetura seja simulada fielmente, a microarquitetura não precisa ser simulada de modo acurado (embora, como dito acima, geralmente os simuladores também são fiéis à microarquitetura).
Nesse trabalho você irá implementar um simulador da arquitetura IAS (arquitetura hóspede) escrito completamente em linguagem de montagem do ARM (arquitetura hospedeira). Tal simulador deverá ser capaz de executar as instruções da arquitetura do IAS e além disso dispor de uma interface que permita ao usuário controlar a execução do programa simulado e inspecionar o estado da máquina virtual. Esse trabalho está dividido em 3 partes, que devem ser desenvolvidas e entregues separadamente. Na seção abaixo, uma visão geral do simulador é apresentada, e nas seções subsequentes, cada parte é explicada em detalhes. Por fim, a última seção apresenta um bônus relacionado com esse trabalho. O trabalho é individual - caso haja qualquer tentativa de fraude, como plágio, todos os envolvidos serão punidos com atribuição de nota 0 na média da disciplina.
O trabalho deve ser implementado em linguagem de montagem do ARM, e deverá ser compatível com o simulador do ARM apresentado no laboratório 4, ou seja, não será permitido o uso de nenhuma função da biblioteca-padrão de C, como printf e scanf, e seu trabalho deve ser executado sem problemas no simulador ARM. As placas ARM podem ser usadas para teste, contudo a correção será feita usando o simulador do ARM, e não as placas. O trabalho está dividido em 3 partes: a parte 1 consiste na implementação de funções auxiliares que serão usadas nas outras partes; na parte 2 uma "interface" de controle da máquina virtual será implementada e finalmente na parte 3 o motor do seu simulador deve ser desenvolvido. As partes serão corrigidas individualmente.
A máquina virtual completa e funcional será composta por 5 arquivos-fonte: os 3 arquivos a serem submetidos por você em cada uma das partes, e mais 2 arquivos a serem disponibilizados: main.s, que corresponde à função main do simulador e ias_arch.s que contém alguns dados globais da arquitetura do IAS. Devido à restrição de não se utilizar funções padrões de C, não haverá leitura de arquivo. O mapa de memória do IAS a ser simulado está declarado no arquivo ias_arch.s como um vetor de 1024*5 bytes (que equivale a 1024 palavras de 40 bits). Tal arquivo pode ser modificado para se carregar um mapa de memória específico.
O seu simulador deverá prover um pequeno shell que aceita os seguintes comandos:
| Código de retorno | Comando | Descrição |
| 0 | quit | Finaliza execução do programa sendo simulado e do simulador. |
| #1 | step [#1] | Executa #1 instruções e pára. Se nenhum #1 for passado, executa apenas a instrução atual. Retorna o número de instruções executadas com êxito. |
| -1 | continue | Continua a executar o programa até encontrar uma instrução não identificada. |
| -2 | write #1 #2 | Armazena o valor #2 no endereço de memória #1. |
| -3 | print #1 | Imprime na tela o valor atualmente armazenado no endereço de memória #1. |
| -4 | registers | Imprime o conteúdo atual dos registradores AC, MQ e PC. |
O código de retorno será melhor explicado no detalhamento da parte 2. Os argumentos #1 e #2 podem ser valores na representação decimal ou hexadecimal - você deve diferenciá-los observando o prefixo. Se houver prefixo "0x", então é um valor hexadecimal; ausência de prefixo indica que o número é decimal. Lembre-se que #2 pode ser um valor de 5 bytes. Todos os comandos podem ser escritos com o nome completo, como registers ou apenas a inicial, nesse caso r. Comandos inexistentes devem exibir uma mensagem na tela e gerar código de retorno -5, contudo o simulador não precisa ser encerrado, podendo-se voltar ao seu shell normalmente.
Por fim, o comando registers deve imprimir o conteúdo atual dos registradores utilizando a seguinte ordem e formato:
AC: 0x0123456789
MQ: 0x9876543210
PC: 0x001 - {E|D}
em que {E|D} indica ou D ou E, dependendo se for instrução à direita
ou à esquerda.
Importante: não se esqueça de respeitar a convenção de chamada do ARM. Sua funções devem salvar qualquer registrador que seja modificado, com exceção de r0, r1, r2 e r3, pois esses registradores podem ser "estragados" pela sua função sem que haja necessidade de se resguardar os valores originais deles. A recíproca vale: caso sua função invoque outra função, ela deve previamente salvar os valores dos registradores r0, r1, r2 e r3 caso haja interesse em preservar seus valores, pois a função chamada não terá responsabilidade de assegurar os valores desses registradores. Por fim, as funções devem retornar em r0.
A Parte 1 consiste na implementação de algumas funções que serão úteis nas outras partes. A tabela abaixo indica as funções a serem implementadas e seus protótipos em linguagem C. Note que todas devem ter visibilidade global pois serão usadas em outros módulos.
| Protótipo | Descrição |
| unsigned int arm_strtou(const char *str); | Converte uma cadeia de caracteres com dígitos decimais ou hexadecimais terminada em NULL (\0) para um inteiro sem sinal de 32 bits. |
| void arm_utostr(unsigned int value, char *buf); | Converte um inteiro sem sinal para uma cadeia de caracteres com dígitos decimais representando o inteiro. A cadeia deve ser preenchida na memória a partir do endereço fornecido em buf e deve ser terminada com NULL. |
| int arm_strcmp(const char *s1, const char *s2); | Compara as duas cadeias de caracteres apontadas por s1 e s2. Retorna 0 se ambas forem iguais, -1 se a primeira for lexicograficamente menor que a segunda e 1 se a segunda for lexicograficamente menor que a primeira. |
| unsigned int arm_strlen(const char *buf); | Retorna o tamanho em bytes da cadeia de caracteres apontada por buf. Note que a cadeia deve ser terminada com NULL e o caractere NULL não é contabilizado no tamanho calculado pela função. |
| void arm_memcpy(char *dest, const char *src, unsigned int num); | Copia num bytes a partir do endereço de memória apontado por src para a região da memória com início em dest. As regiões de memória apontadas por dest e src não devem se sobrepor. |
Na segunda parte do trabalho, você deverá implementar duas funções que juntas correspondem ao módulo de interface com o usuário, decodificando e executando os comandos de entrada. Os protótipos em C das funções são:
int get_command(char *addr1, char *addr2); int run_command(const int op, const char *addr1, const char *addr2);
A função get_command lê da entrada padrão (via
syscall read) uma cadeia de caracteres e
identifica qual comando o usuário deseja executar.
Ela deve retornar o código de retorno conforme apresentando
na Tabela 1 acima. Além disso, os operandos #1 e #2 (caso hajam)
devem ser escritos como cadeias de caracteres nas posições de memória
indicadas por addr1 e addr2, respectivamente.
Por exemplo, se o usuário entrou com o comando
write 0x100 0x12345, então a função get_command
deve retornar o valor -2 e o valor 0x100 deve ser gravado na
posição de memória iniciada em addr1, ao passo que
0x12345 deve ser gravado no trecho de memória iniciado em
addr2.
Caso a função get_command receba um comando inexistente,
deve retornar o valor -5, conforme já dito anteriormente.
A função run_command por sua vez deve receber como argumentos o retorno de get_command, addr1 e addr2, e deve então executar a ação desejada. Caso sejam comandos step [#1] ou continue, deve-se utilizar a função execute apresentada na parte 3 para simular uma instrução do IAS, de modo que tal função seja chamada iterativamente para executar o correto número de instruções do IAS a serem simuladas, dependendo do comando fornecido pelo usuário. Qualquer outro caso fica livre para que você implemente do seu modo, desde que o comando seja executado de maneira consistente com a especificação. A função run_command deve retornar 0 em caso de êxito, -1 caso haja um salto para endereço inválido por parte do código simulado, -2 caso a simulação atinja o fim do mapa de memória e -3 em qualquer outro caso. Para implementar corretamente o retorno de run_command, avalie os retornos da função execute, apresentada na parte 3.
Note que ao término da parte 2 do trabalho você terá um simulador capaz de ler e executar comandos fornecidos pelo usuário; o único módulo que não será funcional é a simulação em si. Contudo, nesse ponto todos os comandos (exceto step [#1] e continue) devem funcionar. Aqui vale relembrar que o arquivo ias_arch.s inclui definições globais essenciais para o funcionamento do simulador, como o vetor global que representa o mapa de memória, denominado IAS_MEM_MAP, bem como os registradores PC, AC e MQ.
Uma vez que esse trabalho deve ser desenvolvido para ser compatível com o simulador ARM (já apresentado no laboratório 4), há o requisito de não se utilizar funções da biblioteca-padrão de C. Assim, para realizar leituras e escritas nas entradas/saídas padrão, é preciso usar chamadas de sistema (syscalls), que já foram brevemente introduzidas e utilizadas no laboratório 4.
Chamadas de sistema nada mais são do que funções nativas do sistema operacional, de modo que ao invocar uma syscall, você está solicitando um serviço ao sistema operacional. Cada syscall é identificada por um número, e da mesma maneira que uma função, elas também recebem parâmetros nos registradores r0, r1, etc. Para escrever na tela ou ler do teclado, é necessário utilizar as chamadas de sistema read e write. A seguinte tabela apresenta uma descrição resumida dos parâmetros enviados para essas duas syscalls:
| Read | |
| R0 -> Descritor de arquivo. | |
| R1 -> Ponteiro para onde escrever os dados lidos. | |
| R2 -> Quantidade de bytes a serem lidos. | |
| R7 -> 0x3. | |
| Write | |
| R0 -> Descritor de arquivo. | |
| R1 -> Ponteiro para a posição de memória que contém os dados a serem escritos. | |
| R2 -> Quantidade de bytes a serem escritos. | |
| R7 -> 0x4. |
Você pode obter maiores informações sobre essas syscalls no manual do Linux, seção 2 (como exemplo, basta entrar com o comando man 2 read num terminal). Para fazer a chamada a uma syscall, você deve colocar os parâmetros nos registradores r0, r1, etc., o número da syscall no registrador r7 e então executar a instrução svc 0. Por exemplo, o código
mov r0, #0x0 ldr r1, =msg1 mov r2, #0x15 mov r7, #0x3 svc 0
executa a syscall read (0x3) para ler no máximo 0x15 bytes da entrada padrão (0x0) e armazená-los a partir do endereço msg1. Como exemplo mais sofisticado, segue abaixo um programa que escreve na tela uma mensagem solicitando ao usuário que digite um texto, e após o usuário informar o texto, o programa escreve na tela o texto informado.
.globl main
.data
msg1: .asciz "Digite uma mensagem: "
msg2: .asciz "\nA mensagem digitada eh: "
mensagem: .space 100
.text
main:
push {r7, lr}
@ $> Digite uma mensagem:
mov r0, #0x1
ldr r1, =msg1
mov r2, #0x15
mov r7, #0x4
svc 0
@ Le a mensagem do usuário
mov r0, #0x0
ldr r1, =mensagem
mov r2, #0x64
mov r7, #0x3
svc 0
@ $> A mensagem digitada eh:
mov r0, #0x1
ldr r1, =msg2
mov r2, #0x19
mov r7, #0x4
svc 0
@ Escreve a mensagem de volta na tela
mov r0, #0x1
ldr r1, =mensagem
mov r2, #0x64
mov r7, #0x4
svc 0
mov r0, #0x0 @retorno da main
pop {r7,pc}
Nessa etapa você deve implementar o motor do seu simulador. Para tanto você deve implementar uma função denominada execute com o seguinte protótipo em C:
int execute();
Tal função deve executar a próxima instrução do IAS e modificar PC, AC e MQ de acordo com o resultado da instrução executada. Ao iniciar o simulador, o PC têm valor 0. Note que a função execute não recebe parâmetros; isso não é necessário pois tanto os registradores do IAS quanto o vetor que representa o mapa de memória são globais e podem ser acessados de dentro da função. Os valores de retorno da função execute são: 0 no caso de êxito ao executar uma instrução, 1 no caso da instrução saltar para um endereço inválido, 2 caso o fim do mapa de memória seja atingido e 3 em qualquer outra circunstância (como opcode inválido). Abaixo, algumas regras sobre as instruções do IAS a serem simuladas:
Esse bônus envolve tanto esse trabalho 2 (as 3 partes) quanto o futuro trabalho 3. A regra é simples: sabendo que no cômputo da nota final da disciplina entra um fator de participação F_Part, calculado como
F_Part = número de atividades de laboratório realizadas e entregues dividido por N (N é o número total de atividades)
o aluno que entregar todas as partes deste trabalho 2 e o trabalho 3 (e todas suas futuras partes, caso ele também seja divido) no primeiro prazo (aquele com fator de multiplicação 1.0x) e obtiver nota maior que 6 em todas as partes/trabalhos, terá seu F_Part calculado como
F_Part = número de atividades de laboratório realizadas e entregues dividido por N-1 (N é o número total de atividades).
Ou seja: o aluno que for "pontual" na entrega e se esforçar no trabalho será beneficiado com a possibilidade de não-entregar um laboratório sem ter suas notas prejudicadas, ou ainda, caso entregue todos, pode ter um fator de participação maior que 1. Note que esse bônus não tem nenhuma relação com o trabalho 1, já encerrado.
Este bônus será concedido para aqueles que implementarem as instruções de multiplicação e divisão do IAS. Como os algoritmos de divisão e multiplicação fogem do escopo do trabalho eles são dados em C em Dica do bônus. Observe ainda que é possível utilizar soma sucessiva para resolver a multiplicação. O bônus só será contabilizado se os algoritmos estiverem funcionando. Este bônus vale dois pontos a mais na nota do trabalho (1 ponto para a multiplicação e 1 ponto para a divisão).