Introdução à Arquitetura de Computadores/Suporte à Funções

Origem: Wikilivros, livros abertos por um mundo aberto.

Funções são ferramentas muito importantes, pois tornam código escrito muito mais usável e torna a tarefa de programar mais produtiva. Elas permitem que um programador se concentre em apeenas uma tarefa de cada vez. Portanto, é essencial que um processador possua suporte á elas. Veremos como isso ocorre no MIPS.

As Etapas Necessárias para Invocar Funções[editar | editar código-fonte]

Quando queremos chamar uma função, as seguintes etapas devem ser cumpridas:

  • O programa principal deve colocar os parâmetros da função em um local que ela possa acessar.
  • O programa principal deve ceder o controle para a função.
  • A função deve coletar todos oos parâmetros deixados pelo programa principal.
  • A função deve executar a tarefa desejada.
  • A função deve armazenar seus resultados em um lugar em que o programa principal possa acessar.
  • A função deve retornar o fluxo do código para o ponto imediatamente após ser chamada.

Para que estes requisitos sejam cumpridos, as seguintes convenções fora criadas:

  • Os registradores $r4, $r5, $r6 e $r7 seriam usados para armazenar parâmetros de funções. Por causa desta funcionalidade, tais registradores podem ser chamados pelos seus "apelidos": $a0, $a1, $a2 e $a3.
  • Os registradores $r2 e $r3 seriam usados para as funções armazenarem seus valores de retorno para o programa principal. Por isso, eles costumam ser chamados de $v0 e $v1.

Tais regras são apenas convenções. Nada impede que um programador as desrespeite e use outros registradores para estabelecer comunicação entre funções e programas principais. Entretanto, para que um programa funcione bem em conjunto com todos os demais, é importante quee tais convenções sejam seguidas.

Além dos registradores convencionados acima, o registrador $r31 também tem um papel importante. Ele sempre armazena o endereço de retorno para o qual a última função chamada deve retornar. Por ter uma função tão importante, este registrador é mais conhecido pelo apelido $ra.

Instruções de Uso de Funções: jal e jr[editar | editar código-fonte]

A instrução que usamos para invocar uma função chama-se jal, ou Jump and Link. Ela é usada da seguinte forma:

jal ENDEREÇO_DA_FUNÇÃO

Entretanto, só devemos chamar esta função depois de já termos salvo os argumentos nos registradores apropriados.

Depois da função executar todas as operações e salvar os resultados apropriados em $v0 e $v1, podemos usar a instrução jr, ou Jump Register. Normalmente usamos a instrução passando para ela o valor do registrador $ra, que contém o endereço certo para voltarmos:

jr $ra

Como exemplo de como isso pode ser feito, vamos converter para Assembly o seguinte código em C:

int example(int a, int b, int c, int d){
   int f;
   
   f = (a + b) - (c + d)
   return f;
}

O código no Assembly do MIPS ficaria assim:

example:                    # Label
          add $t0, $a0, $a1 # Soma a + b
          add $t1, $a2, $a3 # Soma c + d
          sub $v0, $t0, $t1 # Subtrai os dois valores e coloca o resultado no registrador de retorno
          jr $ra            # Retorna o controle para a função principal

Entretanto, observe que para realizarmos os cálculos necessários, precisamos usar registradores. Ao fazer isso, existe o grande risco de apagarmos dados importantes do programa principal. Para evitar isso, existe uma convenção que diz que os registradores $r8 até $r15, $r24 e $r25 são registradores temporários. Por isso, eles costumam ser chamados de $t0 até $t9. Após chamar uma função, o programa principal não deve esperar que os valores destes registradores sejam os mesmos. Somente dados que estejam nos registradores $r16 até $r23 são sempre preservados entre funções. Por esta razão, tais registradores permanentes (salvos) são chamados de $s0 até $s7.

Funções com Muitas Variáveis[editar | editar código-fonte]

Existem funções que podem precisar receber mais do que 4 parâmetros. Entretanto, só temos registradores suficientes para armazenar 4 parâmetros. Nós também podemos querer retornar mais do que os 2 valores permitidos pelos registradores. Ou então, nossa função pode precisar armazenar mais do que 10 valores temporários. O que fazer para resolver isso?

Como o espaço dentro de registradores é bastante limitado, só nos resta apelar para a pilha da memória. Por convenção, o registrador $r29, ou $sp (Stack Pointer) armazena o endereço na pilha de onde novos valores podem ser colocados. Acessando seu endereço e alterando-o podemos guardar valores na memória. O acesso à pilha da memória é muito mais lento que o acesso à registradores, mas este é o único modo de podermos armazenar uma quantidade muito maior de informação.

Por motivos históricos, a pilha "cresce" sempre dos valores de endereços altos para valores menores. Ou seja, para alocar espaço para mais valores, precisamos sempre decrementar o seu valor. Incrementando-o, estamos na verdade removendo os últimos dados da pilha.

Vamos reescrever agora o código Assembly visto acima, desta vez preeservando os valores dos registradores $t0 e $t1 para vermos como ficaria o resultado:

example:                    # Label
          addi $sp, $sp, -8 # Alocamos espaço para dois valores de 4 bits.
          sw $t1, 4($sp)    # Preservamos o valor de $t1 à partir do 4o bit após o Stack Pointer
          sw $t0, 0($sp)    # Preservamos o valor de $t0 sobre o Stack Pointer
          add $t0, $a0, $a1 # Soma a + b
          add $t1, $a2, $a3 # Soma c + d
          sub $v0, $t0, $t1 # Subtrai os dois valores e coloca o resultado no registrador de retorno
          lw $t0, 0($sp)    # Retiramos o valor antigo salvo de $t0
          lw $t1, 4($sp)    # Retiramos da pilha o valor antigo de $t1
          addi $sp, $sp, 8  # Voltamos o Stack Pointer para a posição original apagando de vez as variáveis locais
          jr $ra            # Retorna o controle para a função principal

Funções Recursivas e Sub-Funções[editar | editar código-fonte]

Da mesma forma que o programa principal invocou uma função, uma função também pode invocar outras funções. Se la for recursiva, ela pode até mesmo invocar clones de si mesma para realizar uma tarefa. Como implementar isso sendo que temos apenas um registrador $ra? Se chamarmos outra função, o $ra original é sobrescrito e podemos perder a capacidade de voltar ao programa principal. Além disso, valores temporários que representam variáveis locais podem ser perdidos.

A única forma de evitar isso é enviar para a pilha tudo aquilo que precisa ser salvo - da mesma forma que fizemos com alguns valores no exemplo acima. Para mostrar isso na prática vamos implementar em Assembly a seguinte função em C:

/* Calcula Fatorial */

int fat(int n){
     if(n < 1)
          return 1;
     else
          return (n * fat(n - 1));
}

O resultado final é algo como:

fat:                     # Início da função
     addi $sp, $sp, -8   # Aloca espaço na pilha para 2 valores
     sw $ra, 4($sp)      # Guarda valor de retorno na pilha
     sw $a0, 0($sp)      # Guarda primeiro argumento na pilha
     slti $t0, $a0, 1    # $t0 = ( $a0 < 1)
     beq $t0, $zero, L1  # Se $a0 >= 1, vá para L1
     addi $v0, $zero, 1  # Senão, coloque 1 como valor de retorno
     addi $sp, $sp, 8    # Apague as duas variáveis locais salvas na pilha
     jr $ra              # Encerra a função
L1:  addi $a0, $a0, -1   # Se $a >= 1, decremente $a0
     jal fat             # Chame um clone recursivo de fat que retorna fat($a0-1)
     lw $a0, 0($sp)      # Recupere os valores iniciais do parâmetro da função
     lw $ra, 4($sp)      # Recupere o endereço de retorno antigo
     addi $sp, $sp, 8    # Apague as duas variáveis locais salvas na pilha
     mul $v0, $a0, $v0   # Coloque $a0 * fat($a0 - 1) como valor de retorno
     jr $ra              # Encerra a função

Por fim, uma última coisa útil que é interessante comentar é que o Stack Pointer ($sp) pode ser alterado várias vezes ao longo de uma função. Para manter memorizado o valor do endereço da pilha no início da função, costuma-se usar o registrador $r30 como um Frame Pointer ($fp). Nos exemplos acima, isso não foi preciso, pois só mudamos o valor do Stack Pointer no começo e fim de cada função. Mas em alguns programas utilizar este registrador pode ser útil.