Atividade de Laboratório 6

Objetivos

O objetivo desta atividade de laboratório é exercitar os conceitos de programação em linguagem de montagem do ARM, incluindo representação de dados na memória, conversão de endiannes, laços, operações lógicas e aritméticas.

Descrição

Suponha que você tenha acesso a dois computadores diferentes, e precise exportar um arquivo de texto com uma listagem de números inteiros (sem-sinal) de um para o outro; contudo, os computadores possuem endianness diferentes! O computador de origem, que gera o arquivo de texto, possui representação de dados na memória do tipo big-endian, ao passo que o computador de destino é do tipo little-endian.

Ao se tentar ler o arquivo gerado no computador big-endian no computador little-endian, os valores apresentados estarão incorretos do ponto de vista do computador de origem. Para resolver esse problema, é preciso converter o arquivo para que haja coerência entre os valores gerados no computador big-endian com os valores lidos na plataforma little-endian.

Nesse laboratório, você deverá escrever uma função em linguagem de montagem que converta todos os elementos de um vetor para o formato little-endian e os imprima na tela. Como deve-se imprimir dados na tela, esse laboratório deve ser desenvolvido nas placas ARM. Detalhes e requisitos de entrega estão apresentados nas seções a seguir.

Informações e Detalhes

Primeiramente, vale recordar o conceito de endianness: computadores representam dados na memória em geral numa granularidade de bytes, ou seja, cada palavra de memória tem 1 byte. Portanto, para representar um inteiro de 32 bits (4 bytes) precisamos de 4 palavras de memória. Mas como devemos colocar esse valor na memória: posições menos significativas ficam nos endereços mais altos ou mais baixos? É justamente essa possibilidade de escolha que define o endianness de uma arquitetura: se posições menos significativas ficam nos endereços mais baixos, então temos uma arquitetura little-endian; caso contrário, temos uma arquitetura big-endian. A tabela a seguir, que demonstra como ambas representações organizam o valor 66055 (00000000 00000001 00000010 00000111 em binário) na memória, ajuda a fixar o conceito de endianness:

Endereços de memóriaValores (representação big-endian)End. de memóriaValores (representação little-endian)
0000000000000000000111
0010000000100100000010
0100000001001000000001
0110000011101100000000

Assim, note que a conversão entre os endianness é uma questão de ordenação/movimentação de bytes.

Nesse laboratório, você deve criar uma função em linguagem de montagem que seja equivalente a uma função com o seguinte protótipo em C:
void troca_endianness_imprime(unsigned int *vetor).
Ou seja, sua função não deve retornar nada e deve receber como parâmetro um endereço de memória que aponta para um vetor de inteiros sem-sinal. Tal vetor apresenta as seguintes características:

Sua função deve varrer o vetor imprimindo na tela todos os elementos convertidos para representação little-endian. Um bom teste para saber se tudo está funcionando é checar o primeiro elemento, que deve ser o tamanho do vetor! Não é preciso gravar os valores convertidos no vetor.

Note que nesse laboratório você apenas deverá criar parte de um programa maior; o restante já está desenvolvido e o código-fonte (em C) está disponível; basta baixar o arquivo main.c. Esse módulo já implementado do programa lê um arquivo de texto com uma listagem de números e gera um vetor de inteiros sem-sinal a partir do arquivo de texto; então, o módulo invoca sua função para que ela converta os valores do vetor para representação little-endian e os imprima na tela. Desse modo, para testar sua implementação, você pode compilar o executável final usando os seguintes passos:

Agora basta invocar o executável final passando como argumento um arquivo de texto que contenha uma listagem de números inteiros sem-sinal, um por linha, e o resultado deverá ser uma impressão na tela da sequência de números convertidos para little-endian. Como exemplo, o arquivo teste.txt deve produzir a seguinte saída:


  $ ./raXXXXXX teste.txt
  10
  4686
  4679
  3054
  1203
  1567
  4708
  4742
  4514
  26
  3881

Observe que, uma vez que o módulo principal invoca sua função, as regras da convenção de chamada do ARM devem ser seguidas: sua função deve salvar qualquer registrador que seja modificado, com exceção de r0, r1, r2 e r3 - 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, como printf, ela deve previamente salvar os valores dos registradores r0, r1, r2 e r3 pois a função chamada não terá responsabilidade de assegurar os valores desses registradores.

Aqui vale lembrar um conceito importante: a passagem de parâmetros para funções se dá por registradores (para os primeiros 4 parâmetros; depois disso é por pilha e esse caso não ocorrerá no laboratório). Assim, sua função irá receber o endereço de memória do vetor no registrador r0.

Finalmente, uma dica: procure criar uma outra função dentro do seu código em linguagem de montagem apenas para conversão de endianness de um único número. Assim, na função
troca_endianness_imprime você pode iterar sobre os elementos do vetor e invocar sua outra rotina de conversão a cada elemento do vetor - isso organiza o código e facilita o raciocínio, além de exercitar a chamada de função em linguagem de montagem.

Exemplo de código

O código de exemplo abaixo é de uma função denominada imprime que itera sobre um vetor de elementos de tamanho 10, imprimindo um elemento por linha:


.globl imprime              @torna a visibilidade da função imprime global - útil para que outros módulos possam chamar tal função
.extern printf


.data
  print_mask: .asciz "%u\n" @máscara do printf


.text
imprime:
  push {r4, lr}
  mov r4, r0                @endereço do vetor é passado via r0; coloca o endereço em r4  
  mov r3, #0                @zera r3, que será usado como variável de indução do laço

laco:
  add r2, r4, r3, lsl #2    @cômputo do *endereço* do i-ésimo elemento do vetor
@note que na instrução acima, lsl #2 provoca um deslocamento de 2 bits em r3, multiplicando seu valor por 4 - 4 bytes é justamente o tamanho de um inteiro.
  
  ldr r1, [r2]              @carrega o *valor* do i-ésimo elemento do vetor em r1
  ldr r0, =print_mask       @carrega a máscara de printf em r0
  
  push {r3}                 @salva o valor de r3 para que printf não estrague
  bl printf
  pop {r3}
  
  add r3, r3, #1            @incremento de r3
  
  cmp r3, #10
  blo laco                  @se (r3 < 10) salta para o rótulo laco
  
  pop {r4, pc}              @ volta para instrucao após chamada de imprime

Requisitos de entrega

Endereço da atividade no sistema SuSy: https://susy.ic.unicamp.br:9999/mc404ab/Lab06.