INF1600/INF1612 - Software Básico - 99.2

Aulas de Laboratório

Aula 1 - Linux

Como saber o que fazer:

Para nos acostumarmos com algumas ferramentas do ambiente Linux, vamos fazer alguns pequenos programas C para conversão entre bases.

Agora, escreva (um ou mais) pequenos programas C para resolver as seguintes questões:

Para completar o laboratório, escreva uma função em C que receba uma string com um número binário e retorne seu valor (cola). Escreva outra função que receba um inteiro e imprima seu valor em binário.

Aula 2 - SPIM: O simulador da CPU MIPS

Ao longo do curso, vamos usar o programa spim, que simula uma CPU MIPS (usada, por exemplo, nas workstations Silicon Graphics). Você pode usar esse programa em casa também; veja aqui. Existem versões para Linux e para Windows.

É muito importante que você tenha uma cópia da documentação do spim. Você pode pegar essa documentação aqui, em postscript. Se o netscape não entender direto esse formato, salve a página como um arquivo local e use o programa ghostview para abrí-lo. Você também pode pegar uma cópia dessa documentação na pasta 518 do Document Center.

No nosso laboratório, o programa xspim (uma interface de janelas para o spim) está instalado em /home/super-homem/i1600bp/spim/xspim. Você pode executá-lo escrevendo tudo isso na linha de comando, mas é mais fácil criar um "alias" para o programa. Para isso, edite seu arquivo .tcshrc, acrescente a linha

alias xspim	/home/super-homem/i1600bp/spim/xspim
salve o arquivo, e execute esse novo alias com o comando source .tcshrc. Após isso, você pode executar o simulador chamando direto xspim. (Você também pode executar o simulador com xspim &. Assim, ele executará em background e sua janela de comandos não ficará bloqueada.)

Nosso primeiro programa MIPS está aqui. (O Netscape as vezes se confunde para mostrar esses arquivos. Salve-o em um arquivo local e use um editor para ver seu conteúdo; ou use o comando more nome-do-arquivo para listá-lo.) Carregue esse programa no simulador e execute-o. Recarregue o programa (para "ressetar" o simulador), e execute-o novamente, passo a passo. As primeiras instruções executadas são fixas do simulador, e servem para preparar a chamada ao main, que é o nosso programa. Após a instrução jal main (um "jump") o controle passa para o nosso programa, e você pode acompanhar sua execução passo a passo. O que o programa faz é uma chamada a uma função do sistema operacional para imprimir um inteiro na console. O inteiro a ser impresso deve estar no registrador $a0 (ou $R4). No registrador $v0 colocamos 1 (código interno para a função print_int), e então chamamos o sistema operacional com a instrução syscall.

Agora, modifique o programa para as tarefas abaixo; use os registradores t0-t9 para armazenar valores temporários.

Para fazer contas (por exemplo, para conferir as respostas), você pode usar o programa xcalc.

Lab 3 - jumps e loads

Vamos começar com esse pequeno programa. Copie-o para seu diretório e execute-o no xspim.

Esse programa tem duas rotinas (funções). A primeira, mostraint, mostra um inteiro na console (da mesma forma que o programa da aula passada) e em seguida pula uma linha. Para isso, ele chama o serviço print_string do sistema, passando como parâmetro (em $a0, como sempre) o endereço da string a ser impressa. Essa string está declarada na seção de dados (.data) do programa, com o label s1. (A diretiva .asciiz define uma string na memória, com um 0 no final.)

A segunda rotina, main, imprime todos os elementos de um array de 10 inteiros armazenados na memória. Esse array está definido com o label arr, também na seção de dados. (A diretiva .word define uma sequência de palavras na memória.) Grosso modo, o que main faz é equivalente a

  int *p = arr;
  int cont = 0;
  while (cont < 10) {
    mostraint(*p);
    p++;
    cont++;
  }

O contador cont é armazenado no registrador $s2, e o ponteiro p em $s1. A cada elemento impresso, o contador é incrementado e o ponteiro passa para a próxima posição do array (daí o incremento de 4, que é o tamanho de cada palavra). Quando o contador chega em 10, o loop termina.

Após entender como o programa funciona, modifique-o para executar as tarefas abaixo:

Para desligar a máquina: na tela de login do X digitar CTRL-ALT-F1. Antes de desligar a máquina, digitar CTRL-ALT-DEL. Esperar surgir a tela de escolha do sistema operacional. Após surgir essa tela a máquina pode ser desligada.

Lab 4 - jumps e loads (continuação)

Vamos começar com o mesmo programa da última aula. Modifique-o para executar as tarefas abaixo. (Dica: se necessário, primeiro esboce sua solução em alguma linguagem de alto nível, como C, e depois traduza para assembler.)

Examine agora esse novo programa, que manipula strings (arrays de bytes). Modifique o programa para executar as tarefas abaixo:

Lab 5 - sub-rotinas e funções

O objetivo deste laboratório é escrever rotinas e/ou funções para as tarefas abaixo. Todas essas rotinas devem receber seus argumentos no registrador $a0, e retornar eventuais resultados no registrador $v0. Use a pilha sempre que for necessário preservar registradores durante uma rotina; em particular, se sua rotina chamar outra, salve o endereço de retorno na pilha.

Tarefas:

Para cada uma das tarefas, escreva uma pequena função main que permita testar sua rotina.

Lab 6 - Operadores "lógicos"

Os chamados operadores lógicos são operadores descritos diretamente no nível de bits de um valor binário. São também chamados operadores bitwise (ou bit-a-bit). Os mais importantes são:

Observe que a operação de shift para a esquerda é equivalente a multiplicar o número por uma potência de 2: x << n == x*2n (Compare a situação com o que ocorre quando deslocamos um número decimal para a esquerda, preenchendo com zeros a sua direita.)

Em muitas CPUs, a operação de multiplicação de inteiros é bem mais lenta que as operações de soma ou as operações lógicas. Nessas CPUs, uma técnica de otimização é transformar uma multiplicação por uma constante em uma sequência equivalente de somas, subtrações e shifts. Por exemplo, para multiplicarmos o conteúdo de $a0 por 3, podemos fazer

        sll     $t0, $a0, 1     # $t0 = 2*$a0
        add     $a0, $a0, $t0   # $a0 = $a0+$t0 = $a0+2*$a0=3*$a0

Outro uso bastante frequente de operadores lógicos é para manipularmos pedaços de palavras. Muitos sistemas guardam um conjunto de valores booleanos (verdadeiro ou falso) em uma única palavra, usando um bit por valor (por exemplo, o 1o bit pode indicar se uma janela tem borda ou não, o 2o bit indica se ela tem título ou não, etc.). Para "setarmos" (colocar em 1) o n-ésimo bit de uma palavra w, podemos executar w = w | (1 << n); (em C, podemos escrever w |= 1 << n;) para "resetarmos" o n-ésimo bit, executamos w = w & ~(1 << n); para trocarmos o n-ésimo bit (se estiver em 0 vai para 1, se estiver em 1 vai para 0), executamos w = w ^ (1 << n); (observe que ou exclusivo de qualquer bit com 0 dá o próprio bit, enquanto o ou exclusivo de qualquer bit com 1 dá a negação do bit). Finalmente, para testarmos o n-ésimo bit de uma palavra w, observamos que o valor (w & (1 << n)) é zero se e somente se o bit em questão for zero.

Lab 7 - Ponto flutuante

A CPU MIPS tem um co-processador para executar operações de ponto flutuante. Esse co-processador possui um conjunto próprio de 32 registradores de 32 bits cada um, denominados $f0, ..., $f31, e um conjunto próprio de instruções sobre esses registradores, diferente do conjunto de instruções disponível para os registradores "normais".

Como primeira tarefa, para se acostumar com esse novo conjunto de instruções, escreva uma função que receba em $a0 um ponteiro para um array de floats na memória, e em $a1 o número de elementos deste array, e retorne (em $v0) a soma (representada em ponto flutuante) dos elementos positivos do array. Para testar sua função, você pode usar a declaração .float para declarar um array,

      .data
A1:   .float 3.5, -10.4, 0.01, 34.0
e a syscall 2 para imprimir seu resultado.

Agora, vamos imaginar que nossa CPU não possui um co-processador. Então, queremos implementar algumas operações sobre números com ponto flutuante em software. A representação de um número em ponto flutuante contém três partes: o bit mais significativo representa o sinal do número (0 para positivo, 1 para negativo); os próximos 8 bits representam o expoente na base 2 do número, representado como excesso 127 (isso é, o expoente real é o número representado pelos 8 bits menos 127); e os 23 bits restantes representam a mantissa: a mantissa real é o inteiro representado por esses 23 bits, dividido por 2^23, somado de 1. Assim, um float s1e8m31 representa o número real ((m/2^23)+1) * 2 ^ (e-127), negativo se s for 1.

Como primeiro passo, vamos usar a função monta. Essa função recebe, em $a0, a "mantissa" do número a ser representado, multiplicada por 2^23 (para ser sempre um número inteiro). Em $a1, recebemos o expoente excesso 127; e em $a2, recebemos o sinal. A função retorna, em $v0, a representação em ponto flutuante dos dados recebidos.

Escreva agora uma função para somar dois números representados em ponto flutuante. Para simplificar, vamos assumir que os dois números são positivos, e ignorar casos especiais (Inf, -Inf, etc.). Sua função pode seguir os seguintes passos:

Lab 8 - Construções da linguagem C

Converta ("compile") as seguintes construções em C para assembler:

Lab 9 - Variáveis Locais

Quantas instruções MIPS são necessárias para executar o comando i++, para os casos da variável int i ser uma global, uma local na pilha ou uma local em um registrador? (Ignore possíveis otimizações; lembre-se que algumas instruções que usamos na verdade são pseudo-instruções, que correspondem a duas instruções reais.)

Converta ("compile") as seguintes construções em C para assembler:

Lab 10 - Strings

Converta ("compile") o seguinte programa em C para assembler:

      char a[10][10];

      void strcpy (char *s1, char *s2) {
        while ((*s1++ = *s2++) != '\0') ;
      }

      int strlen (char *s) {
        int i = 0;
        while (*s++) i++;
        return i;
      }

      int main (void) {
        char *b[10];
        b[0] = "alo alo";
        strcpy(a[0], "oi oi");
        b[1] = a[1];
        strcpy(b[1], &b[0][4]);
        b[2] = a[1]+strlen(a[1]);
        strcpy(b[2], &a[0][2]);
        print_string(b[1]);
        return 0;
      }

Lab 11 - Recursão

Lab 12 - Trabalho

Escreva uma função main para testar o segundo ítem do trabalho entregue por seu grupo. Essa função deve declarar dois arrays de floats, com os casos a serem testados. Para cada par de números (um de cada array), a main deverá chamar a função de vocês, multiplicar os dois números usando a operação de multiplicação do co-processador, e em seguida comparar os resultados. Para cada par testado, ela deve imprimir uma linha com o resultado do teste, por exemplo
  0.2 x 1.0 = 0.2    OK
  1.0 x Inf = Inf    OK
  -1 x 1 = 1  (erro: correto e' -1)

Preencha o array de testes com casos significativos, que testem todos os casos particulares da função de multiplicação.

Lab 13 - Símbolos em arquivos objeto

No Unix, podemos usar o programa nm para vermos os símbolos de um dado arquivo objeto. Os símbolos são classificados nas seguintes categorias: Esses códigos podem vir em maiúscula, representando um símbolo global, ou minúscula, representando um símbolo local.

Para os códigos D e T, o nm indica o endereço relativo do símbolo no arquivo objeto analisado. Para o C (ou B), ele indica apenas o tamanho a ser alocado pelo símbolo (por que?). Para o U, não indica nada (pois o símbolo está indefinido).

Para exemplificar, considere o programa C abaixo:

  extern int a,b,c,c1;
  int a = 10;
  int a1 = 20;
  int d,e;

  static char s1,s2[10];
  static char s3[] = "uma string qualquer, com varios bytes";
  int k=300;

  void f1 (void) { c = 1; }

  static void f2 (void) { d=e; }

  void f3 (void) { fext(12); }
Compile esse programa (gcc -c, o -c indica que a compilação deve parar no código objeto, sem gerar um executável), e em seguida rode o nm sobre o objeto gerado. Explique detalhadamente os resultados.