Introdução à Arquitetura de Computadores/Suporte à Funções
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.