Syscalls write e exit.

Esse laboratório é dividido em duas partes. Na primeira você vai aprender a combinar software escrito em C com código escrito em linguagem de montagem. Na segunda você irá implementar as syscalls write e exit, além do tratador de chamadas de sistema.

Combinando código em C, com código ARM

Trecho em código C

Fazer com que código C chame código feito em linguagem de montagem e vice-versa é uma tarefa bastante útil. Isso acontece porque um projeto de software raramente é feito completamente em linguagem de montagem. Normalmente, algumas partes críticas são feitas em linguagem de montagem e o restante é feito em linguagem C. Um exemplo acontece se você quiser realizar chamadas de sistemas diretamente em seu programa de usuário, sem depender da biblioteca padrão C, ou se você está escrevendo uma implementação desta parte da biblioteca. Isso acontece porque a linguagem C não define nenhum método de realizar chamadas de sistema, apenas métodos para chamar outras funções. Para quaisquer outros casos em que a linguagem C não faz, você deverá também recorrer a programas em linguagem de montagem, e isso inclui muito do código de sistema operacional ou drivers. Estes códigos precisam frequentemente desabilitar ou habilitar interrupções, funcionalidades que só são feitas diretamente em linguagem de montagem.

Neste laboratório você deverá se conectar na placa ARM física e compilar o programa user.c fornecido na página de submissões SuSy. Para isso utilizaremos comandos próprios do gcc, as e ld para ligar nosso programa sem usar a biblioteca C. Como você não pode usar a biblioteca C, você terá que se comunicar com o sistema operacional para leitura e escrita usando apenas chamadas de sistema. Para realizar a chamada de sistema propriamente dita, você irá implementar pequenos trechos em linguagem de montagem para realizar a chamada de sistema e retornar para o programa C.

Na seção "arquivos auxiliares" da página de submissões SuSy, você encontrará o arquivo user.c. Esse arquivo contém a função main e chamadas para as funções write e read. Observe que essas não são as chamadas de sistema, apenas funções, também conhecidas como pontes ou wrappers. No entanto, essas funções possuem os mesmos parâmetros das syscalls.

Você deve compilar o seu código de forma a gerar um arquivo em linguagem de montagem. Para obter esse efeito utilize o seguinte comando em uma das placas ARM:

	gcc user.c -march=armv5 -marm -fno-stack-protector -fno-builtin-exit -S

Abra o arquivo gerado (user.s) e observe que há duas instruções do tipo branch-and-link para os rótulos write e exit conforme esperado. Porém esses rótulos não existem. O próximo passo portanto é escrever essas rotinas.

Trecho em linguagem de montagem

Para implementar em linguagem de montagem uma função que é chamada por um programa em C (no nosso caso, as funções write e exit), você deve:

Isso significa que no rótulo write, você deverá implementar uma função que espera, no registrador R0, o primeiro argumento da função, no registrador R1 o segundo argumento e no registrador R2 o terceiro argumento da função em C. O retorno é feito no registrador R0. Você não deverá sujar registradores salvos, a não ser que salve eles antes e recupere antes de retornar para o código C. Por outro lado, no rótulo exit você não precisa implementar uma função completa que salva registradores e retorna. Essa função recebe no primeiro parâmetro (em R0) o código que deve ser passado para syscall exit. A syscall exit nunca retorna.

O corpo dessas funções em linguagem na montagem é bem pequeno. Ele apenas organiza os parâmetros e realiza a chamada de sistema com a instrução SVC.

Construindo o programa final

Supondo que a implementação das funções write e exit tenha sido feita no arquivo syscalls.s, você pode usar os seguintes comandos para gerar um arquivo executável:

	gcc user.c -march=armv5 -marm -fno-stack-protector -fno-builtin-exit -S
	as user.s -o user.o
	as syscalls.s -o syscalls.o
	ld user.o syscalls.o -o user

Observe a ordem com que os arquivos foram ligados (último comando). O arquivo binário user pode ser executado na placa ARM com o comando:

	./user
Para executar o seu código de usuário no simulador você precisa ligar o código de usuário junto o código de sistema desenvolvido na segunda parte do laboratório (ou com o arquivo sys.o fornecido nos laboratórios anteriores).

Atendendo syscalls

No laboratório anterior você implementou um pequeno programa em linguagem de montagem ARM para atender às interrupções de hardware do tipo IRQ. Nesse laboratório, você deve expandir a implementação anterior para atender interrupções de software do tipo SVC (syscalls). No laboratório anterior você também configurou os dispositivos GPT e TZIC. Nesse, você deverá expandir seu código para configurar o dispositivo de saída serial, UART. Essas alterações vão permitir que você execute o código de usuário criado na primeira parte do laboratório.

Inicialização das pilhas

Para permitir a execução do código de usuário, você deve inicializar as pilhas. Essa tarefa pode ser realizada com o código abaixo. A inicialização deve ser feita na rotina de tratamento de reset implementada no laboratório anterior.

	@ Configurable STACK values for each ARM core operation mode
	.set SVC_STACK, 0x7800
	.set UND_STACK, 0x7900
	.set ABT_STACK, 0x7A00
	.set IRQ_STACK, 0x7B00
	.set FIQ_STACK, 0x7C00
	.set USR_STACK, 0x7D00

	@ First configure stacks for all modes
	mov sp, #SVC_STACK 
	msr CPSR_c, #0xDF	@ Enter system mode, FIQ/IRQ disabled
	mov sp, #USR_STACK
	msr CPSR_c, #0xD1	@ Enter FIQ mode, FIQ/IRQ disabled
	mov sp, #FIQ_STACK
	msr CPSR_c, #0xD2	@ Enter IRQ mode, FIQ/IRQ disabled
	mov sp, #IRQ_STACK
	msr CPSR_c, #0xD7	@ Enter abort mode, FIQ/IRQ disabled
	mov sp, #ABT_STACK
	msr CPSR_c, #0xDB	@ Enter undefined mode, FIQ/IRQ disabled
	mov sp, #UND_STACK
	msr CPSR_c, #0x1F	@ Enter system mode, IRQ/FIQ enabled

Tratamento de interrupções de software

Na convenção mais recente ARM EABI, para realizar uma chamada ao sistema, você deve colocar o número da chamada de sistema no registrador R7, e os parâmetros seguem a mesma convenção de uma chamada de função comum (devem estar nos registradores R0 a R3). O valor de retorno é passado no registrador R0. Para realizar a chamada de sistema, o código de usuário utiliza a instrução svc 0. Esta instrução irá gerar uma exceção e fazer com que o registrador PC (apontador para a instrução a ser executada) aponte para a posição 0x00000008 e troque o modo do processador para SUPERVISOR. A implementação de uma chamada de sistema, portanto, é similar à implementação de interrupções do laboratório anterior.

No entanto, você recebe em R7 um número correspondendo ao número da syscall. O seu tratador de chamadas de sistema deve, portanto, analisar o valor contido nesse registrador e selecionar a rotina de tratamento adequada (write ou exit no caso deste laboratório). Lembre-se que, para retornar do tratador de chamadas de sistema para o código do usuário que chamou a syscall, você deve utilizar uma instrução especial como movs pc, lr para recuperar o registrador CPSR original, modificando o modo do processador. Mais detalhes sobre a troca de modos e o serviço de interrupções e syscalls no processador ARM estão disponíveis no enunciado do laboratório anterior.

syscall write

Não é necessário que seu sistema administre arquivos e descritores de arquivos. Portanto, ignore o primeiro parâmetro da chamada de sistema write, que diz respeito ao número do descritor de arquivo. Ou seja, independente do valor do descritor de arquivo, a chamada de sistema write deve escrever os bytes no dispositivo UART. Esta é uma pequena simplificação que deve ser adotada no seu sistema em relação ao um sistema operacional POSIX real.

Escreva R2 bytes do buffer cujo ponteiro está em R1. Retorne o número de bytes escritos com sucesso (0 indica que nenhum byte foi escrito) ou -1 caso tenha ocorrido algum erro. A seção Transmissão de dados pela UART a seguir, explica como enviar os bytes para o dispositivo UART.

syscall exit

Em um sistema operacional POSIX real, a chamada de sistema exit finaliza o programa do usuário. Nesse laboratório, como estamos implementando um sistema simples, o tratamento dessa syscall deve simplesmente levar o processador para a execução de um laço infinito.

A tabela a seguir resume as chamadas de sistema que devem ser implementadas:

syscall Parâmetros Descrição
write R0 - descritor do arquivo
R1 - Ponteiro para o buffer de escrita
R2 - número de bytes a serem escritos
A chamada de sistema write (consulte man 2 write) escreve R2 bytes que estão na área de memória apontada por R1 no arquivo cujo descritor está em R0. O descritor de número 1 indica a saída padrão, conhecida como stdout em C e aberta automaticamente pelo shell. O número da chamada de sistema write é 4.
exit R0 - código de retorno Como estamos implementando um sistema simples, o tratamento dessa syscall deve simplesmente entrar em um laço infinito.

UART

Configuração

A UART possui um datasheet intimidador de mais de 70 páginas. Não se preocupe, você só irá utilizar o datasheet para consultar os endereços na memória de cada registrador da UART. Quando for fazer isso, lembre-se que a plataforma iMX (da placa que está conectada na rede do IC e que o simulador também reproduz fielmente) possui 4 UARTs. Pegue o endereço da primeira UART, indicada no datasheet como UART1. Sabendo traduzir os endereços dos registradores, para ligar a UART, veja um exemplo de código de inicialização seguindo os passos da seção 75.5.1 do datasheet. Contudo, não realize o oitavo passo, pois não utilizaremos interrupções para comunicação com a UART.

Transmissão de dados pela UART

Nessa seção será explicado como transferir dados por polling, isto é, sem utilizar interrupções.

Verifique se o bit TRDY (transmitter ready), isto é, o bit 13 do registrador UART STATUS REGISTER 1 (UART1_USR1), está definido como 1. Caso positivo, isto indica que a ocupação da fila de transmissão (TX_FIFO) está abaixo de um nível de segurança pré-configurado (lembre-se que você configurou a UART seguindo os passos do datasheet), ou seja, você pode escrever um byte para transmissão sem correr o risco de perder o dado por fila cheia. Essa fila possui 32 posições e armazena os caracteres que estão pendentes para serem enviados. Quando um byte é transmitido, um caractere é removido da fila.

Para escrever um caractere na fila TX_FIFO e escaloná-lo para ser transmitido, basta realizar uma escrita no registrador UART1_UTXD. Ao escrever neste registrador, o dado vai para a fila TX_FIFO. Caso a ocupação da fila fique acima de um determinado nível, correndo o risco de ficar cheia, o bit TRDY é automaticamente desligado (vai para 0).

Portanto, uma forma de realizar escrita pela UART é implementar um laço que escreve byte por byte, realizando sucessivas escritas em UART1_UTXD sempre que a flag TRDY estiver avisando que o dispositivo pode receber mais um byte. A condição de parada do laço será se o número de bytes escritos já alcançou o número solicitado pelo usuário da chamada de sistema syswrite.

Saltando para o código do usuário

No laboratório anterior, ao final da rotina de tratamento de reset, você colocou o processador em um laço infinito aguardando interrupções. Nesse laboratório, no entanto, o seu código de sistema deve transferir o controle da execução para o código do usuário. Essa tarefa é realizada através de um salto direto (instrução b). O ponto de entrada do código do usuário fica em uma posição predefinida na memória (0x00008000). Lembre-se de que você precisa mudar o modo de operação do ARM para User. Você deve fazer isso com a instrução msr. O código a seguir pode ser inserido ao final do tratador de reset para saltar para o código de usuário:

	@ Enable interruptions, set ARM mode to USR, and jump to 0x8000
	msr	CPSR_c, #0x10
	b	0x8000

Simulando o código de sistema

O único modo de testar o código supervisor (de sistema) é utilizando o simulador. Você deve especificar o código de sistema a carregar com o parâmetro --load-sys= e especificar o código de usuário (o mesmo que também roda na placa como um aplicativo independente) com o parâmetro --load=.

O código de sistema deve ser ligado com a opção --Ttext=0 para que o código seja montado a partir do endereço 0x00000000. Se você não especificar esta opção, o ligador irá colocar seu código no endereço 0x00008000 e, dessa forma, seu sistema não irá executar, pois o processador, assim que ligado, começa sempre a executar a instrução que está no endereço 0x00000000. Já o código de usuário não precisa ter seu endereço alterado: ele deve iniciar no endereço padrão escolhido pelo ligador (0x00008000)1. Os comandos a seguir podem ser utilizados para construção do software.

	# System
	arm-elf-as -g myos.s -o myos.o
	arm-elf-ld -g -Ttext=0x0 myos.o -o myos
	# User
	arm-elf-as -g user.s -o user.o
	arm-elf-as -g syscalls.s -o syscalls.o
	arm-elf-ld -g user.o syscalls.o -o user

Para executar no simulador execute o seguinte comando:

	arm-sim --load-sys=myos --load=user

1 Você pode conferir o endereço inicial do código gerado com o comando a seguir:

	arm-elf-objdump -D user

Submissão

Você deve submeter os arquivos myos.s, user.s e syscalls.s na página de submissões em: http://susy2.ic.unicamp.br:9999/mc404ab/09

Arquivos auxiliares