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.
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.
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ória | Valores (representação big-endian) | End. de memória | Valores (representação little-endian) |
| 000 | 00000000 | 000 | 00000111 |
| 001 | 00000001 | 001 | 00000010 |
| 010 | 00000010 | 010 | 00000001 |
| 011 | 00000111 | 011 | 00000000 |
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.
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
Endereço da atividade no sistema SuSy: https://susy.ic.unicamp.br:9999/mc404ab/Lab06.