Introdução à Arquitetura de Computadores/Suporte à Funções: diferenças entre revisões

Origem: Wikilivros, livros abertos por um mundo aberto.
[edição não verificada][edição não verificada]
Conteúdo apagado Conteúdo adicionado
Sem resumo de edição
Linha 63: Linha 63:


example: # Label
example: # Label

addi $sp, $sp, -8 # Alocamos espaço para dois valores de 4 bits.
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 $t1, 4($sp) # Preservamos o valor de $t1 à partir do 4o bit após o Stack Pointer

Revisão das 02h17min de 14 de agosto de 2008

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

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 ceeder 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 aramazenar 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

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é $r7.

Funções com Muitas Variáveis

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, noossa 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