Utilizando Assembly inline do RL78 no GCC

Olá pessoal!

Há algum tempo decidi portar o meu pequeno sistema operacional (se é que ele pode ser chamado assim) ULWOS para a plataforma RL78 utilizando o compilador GCC. Como a base da troca de contexto em um agendador de tarefas exige programação de baixo nível, foi necessário aprender a utilizar o assembly inline no GCC. Neste artigo eu mostro algumas características e exemplos de uso do assembly dentro de um programa em C para o RL78.

Em primeiro lugar, é importante destacar que existem duas formas de inserir código assembly dentro do C no GCC: o basic assembly e o extended assembly. Basic assembly é utilizado para inserir uma ou mais instruções simples e normalmente é utilizado para habilitar interrupções, desabilitá-las e outras operações simples a nível de registrador. O extended assembly, por outro lado, é utilizado quando o código assembly interage diretamente com elementos do C como variáveis, funções, labels, etc.

Basic Assembly

O formato do basic assembly é bastante simples:

asm [ volatile ] ( AssemblerInstructions )

Onde “AssemblerInstructions” pode ser uma ou mais instruções assembly válidas para a plataforma alvo do GCC (no caso o RL78), caso exista mais de uma instrução, as mesmas deverão ser separadas por \n\t (linha nova e tab). O modificador “volatile” é opcional (por isso aparece entre []) e desnecessário pois o GCC trata todo basic assembly como código volátil. A seguir temos dois exemplos de basic assembly: a macro DI() é utilizada para desabilitar as interrupções e a macro EI() é utilizada para habilitar as interrupções no RL78.

#define DI() asm("di")
#define EI() asm("ei")

Note que o GCC não interfere de forma alguma no basic assembly, limitando-se apenas a transcrever as instruções para o código assembly final gerado pelo compilador.

Extended Assembly

O extended assembly é substancialmente mais complexo que o basic assembly, inclusive no seu formato:

asm [volatile] ( AssemblerTemplate
 : OutputOperands
 [ : InputOperands
 [ : Clobbers ] ])
asm [volatile] goto ( AssemblerTemplate
 :
 : InputOperands
 : Clobbers
 : GotoLabels)

Ele é composto por um AssemblerTemplate que irá conter as instruções assembly escritas dentro de aspas e terminadas por \n\t (nova linha e tab), além de operandos de saída (OutputOperands), de entrada (InputOperands) e eventualmente Clobbers e GogoLabels. Note que o extended assembly somente pode ser utilizado dentro de uma função C!

De maneira geral, operandos de saída especificam as variáveis C que serão alteradas pelo código assembly. Todo extended assembly deve conter operandos de saída e eles devem ser listados após os dois pontos que seguem o template de instruções. Caso o seu template assembly não altere nenhuma variável, então a lista de operandos de saída pode ser deixada vazia.

Os operandos de entrada são opcionais e, caso presentes, serão listados após o segundo dois pontos. Operandos de entrada devem listar apenas variáveis ou símbolos C que são lidos mas não alterados pelo código assembly!

Em seguida podemos ter opcionalmente Clobbers e GotoLabels. Os Clobbers são utilizados para indicar ao GCC quais os registradores foram diretamente alterados pelo código, de forma que ele considere isso ao gerar o restante do código C da função. GotoLabels são indicações de rótulos (labels) localizados no código C e que podem ser alvos de desvio oriundo do código assembly.

Mas não é só isso! Os operandos utilizam restritores (constraints) que especificam para o compilador como deverá ser feito o acesso ao símbolo C! Vejamos então alguns destes restritores:

  • “m” – indica que o operando é uma posição de memória;
  • “o” – indica que o operando é uma posição de memória deslocável, ou seja, que pode sofrer um deslocamento. Isto significa que o endereço somado a uma constante (deslocamento) deve ser um endereço válido;
  • “V” – indica que o operando é uma posição de memória que não é deslocável;
  • “r” – indica que o operando é um registrador;
  • “i” – para um operando imediato (uma constante ou símbolo C constante);
  • “n” – para operandos imediatos menores que 8 bits;
  • “p” – para um operando que é um endereço de memória;

Existem também modificadores de restrição (constraint modifiers):

  • “=” – indica que o operando é sobrescrito pelo código assembly (somente escrito, não lido);
  • “+” – indica que o operando é lido e escrito pelo código assembly;
  • “%” – indica que o operando é comutativo com o próximo operando (deve ser somente leitura e somente um par por template);

Por fim, existem também os restritores específicos para os RL78 os quais vamos listar apenas alguns (para a lista completa, veja o tópico 6.44.3.4 do manual do GCC):

  • “Int3” – constante inteira entre 1 e 7;
  • “Int8” – constante inteira entre 0 e 255;
  • “J” – constante inteira entre -255 e 0;
  • “Y” – um endereço de memória near;

Exemplos

Vejamos então alguns exemplos de uso do extended assembly. Suponha que você tenha três variáveis globais, ga, gb e gc e que em um dado momento você precise efetuar a soma de ga com gb e armazenar o resultado em gc. Esta operação poderia ser feita utilizando o seguinte trecho de extended assembly (dentro da função sum16):

void sum16(){
 asm volatile (
   "movw ax,%[a]\n\t"  // copia ga para AX
   "movw bc,%[b]\n\t"  // copia gb para BC
   "addw ax,bc\n\t"    // soma BC com AX (resultado em AX)
   "movw %[c],ax"      // gc recebe o conteúdo de AX
   : [c]"=m"(gc) // o símbolo c representa o operando de saída gc
   : [a]"m"(ga),[b]"m"(gb) // o símbolo a representa o operando ga e b representa gb
   : "ax","bc" // clobbers
 );
}

Note que são utilizados três operandos: ga e gb são entradas e gc é uma saída. A linha :[c]”=m”(gc) descreve o operando de saída gc. Esta linha informa que a referência ao símbolo C “gc” dentro do assembly irá utilizar o símbolo “c” e o restritor =m implica que gc é uma posição de memória que será sobrescrita.

A próxima linha :[a]”m”(ga),[b]”m”(gb) descreve os dois operandos de entrada. O operando ga é referenciado no assembly através do símbolo “a” e o operando gb é referenciado através do símbolo “b”, ambos são operandos do tipo “memória”.

A última linha são os clobbers, informando ao GCC que o nosso código assembly alterou o estado dos registradores AX e BC.

O GCC também admite uma outra forma de representação dos operandos, que é a referência pela posição dos mesmos na lista de operandos (sem utilizar nenhum símbolo). Este tipo de representação torna o código menos legível. O próximo exemplo demonstra como o exemplo anterior poderia ser reescrito utilizando esta outra representação:

void sum16(){
 asm volatile (
   "movw ax,%1\n\t"
   "movw bc,%2\n\t"
   "addw ax,bc\n\t"
   "movw %0,ax"
   : "=m"(gc) // gc é o operando de saída (%0)
   : "m"(ga),"m"(gb) // ga e gb são operandos de entrada (%1 e %2)
   : "ax","bc"
 );
}

Também é possível utilizar rótulos (labels) dentro do programa assembly, de forma que se implemente desvios condicionais ou incondicionais. O próximo exemplo demonstra um contador utilizando os registradores A e B:

void __attribute__((__noinline__)) count(unsigned char val){
 asm volatile(
   "mov a,%[cnt]\n\t"  // copia val para A
   "clrb b\n\t"        // apaga B
   "repeat:"
   "inc b\n\t"         // B=B+1
   "dec a\n\t"         // A=A-1
   "bnz $repeat"       // desvia para repeat se A diferente de zero
   : // nenhum operando de saída
   :[cnt]"m"(val) // cnt representa o operando val
   :"a","b" // clobbers
 );
}

Uma observação muito importante acerca do uso de labels no assembly inline: certifique-se sempre de que o compilador não irá clonar ou fazer o inline da sua função! Caso isto ocorra, o programa não será compilado e gerará um erro de símbolo já definido. No caso acima o erro seria: Error: symbol `repeat’ is already defined. A razão disso é simples: o código assembly é transcrito pelo GCC diretamente para o código assembly final resultante do processo de compilação. Quando uma função é clonada ou sobre inline, o conteúdo da mesma é escrito no local da chamada à mesma. Se você possui um rótulo no seu assembly, este rótulo também será repetido, ocasionando o erro! Para contornar isso utilize o atributo noinline conforme mostrado acima!

O trecho de código a seguir demonstra como efetuar a leitura de valores de 16 bits (unsigned int) e o retorno de um resultado de uma função, tudo em assembly. A função average16 irá somar o valor dos dois parâmetros (va e vb) e dividir o resultado por 2 (através de uma rotação de um bit à direita, instrução SHRW) e retornar o resultado para o chamador. Observe que foi criada uma variável local “result” que recebe o resultado do deslocamento à direita feito em AX com a instrução SHRW AX,1. Este código demonstra a facilidade do acesso aos parâmetros da função que são passados através da pilha. Não foi necessário identificar em qual posição da pilha estava cada parâmetro, isso foi feito pelo GCC!

Os operandos de entrada va e vb foram declarados com o constraint “o”, pois eles são parâmetros da função e necessariamente são passados através da pilha, por isso, o acesso aos mesmos deve ser feito utilizando um endereço deslocável!

Observe que o código inclui um clobber “cc”. Este código informa ao GCC que o conteúdo do registrador de status (PSW) foi alterado.

unsigned int average16(unsigned int va, unsigned int vb){
 volatile unsigned int result;
 asm volatile (
   "movw ax,%[va]\n\t"  // copia va para AX
   "movw bc,ax\n\t"     // copia AX para BC
   "movw ax,%[vb]\n\t"  // copia VB para AX
   "addw ax,bc\n\t"     // soma BC com AX (resultado em AX)
   "shrw ax,1\n\t"      // desloca AX um bit à direita (divide por 2)
   "movw %[res],ax"     // copia AX para result
   : [res] "=m" (result)
   : [va] "o" (va),[vb] "o" (vb)
   : "ax","bc","cc"
 );
 return result;
}

Por hora é isso, num próximo artigo vamos aplicar estes conhecimentos na implementação de um agendador de tarefas que será a base de um pequeno sistema operacional para o RL78, o ULWOS! Até lá!

Leave a Reply