Capítulo 4 Procedimentos e funções Neste capítulo vamos estudar as facilidades que os processadores oferecem para a implementação do conceito de procedimentos e funções por linguagens de programação. Considere o trecho de programa abaixo que inclui um procedimento denominado troca. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void troca(int *a, int *b) { int tmp; } tmp = *a; *a = *b; *b = tmp; int main(void) { int x,y,z; ... troca(&x,&y); z=x+1; troca(&z,&x); x=y-1; ... } A tabela abaixo indica a sequência de comandos executados durante as duas invocações do procedimento troca mostradas no trecho de programa: 9 10 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES Linhas 15 5, 6, 7 16 17 5, 6, 7 18 Comentário primeira invocação de troca execução dos comandos do procedimento retorna do procedimento e executa este comando segunda invocação de troca execução dos comandos do procedimento retorna do procedimento e executa este comando Ou seja, como esperado, a cada invocação do procedimento o fluxo de execução desvia para o início do procedimento, e ao final da execução do procedimento deve ser feito um desvio para o comando seguinte à chamada de procedimento: após a execução do comando na linha 7 deve ser executado o comando na linha 16 na primeira invocação; na segunda invocação, após a execução do comando na linha 7 deve ser executado o comando na linha 18. E aí aparece a dificuldade de se implementar procedimentos com as instruções de desvio que vimos até agora: a cada chamada de procedimento, o desvio ao término do procedimento deve ser feito para endereços diferentes. Uma primeira abordagem para a implementação de procedimentos é a seguinte. A chamada de procedimento é implementada por uma instrução especial que armazena o valor de ip corrente na primeira palavra do procedimento (note que neste momento ip já foi incrementado e contém o valor do endereço da instrução seguinte à chamada de procedimento, ou seja, ip contém o endereço de retorno). Mais especificamente, suponha que exista uma instrução especial JSR (desvia para subrotina – em inglês, jump to subroutine), que tem um operando, o endereço do procedimento, definida como: JSR Desvia para subrotina Sintaxe Operação Flags Codificação 31 jsr expr32 mem[imd32 ] ← ip + 8 ip ← imd32 + 4 – 0 xx 31 0 imd32 Ao ser executada, essa instrução armazena o valor do endereço de retorno na primeira palavra do procedimento (assumindo o valor de ip no início da execução da instrução, o endereço de retorno é textttip+8). O montador deve deixar essa primeira palavra livre na montagem). Ao final da execução do procedimento, para retornar da chamada, deve-se desviar para o endereço armazenado na primeira palavra. Como exemplo, considere o trecho abaixo em linguagem de montagem: 11 1 2 3 4 5 6 7 8 9 10 ... jsr proc_P ret_aqui: ... ; uma chamada ao procedimento de nome proc_P ; este é o endereço de retorno desta ; esta é a declaração do procedimento proc_P: ds 4 ; a primeira palavra do procedimento é reservada set r0,1 ; a primeira instrução está no endereço ... ; resto do procedimento Nesse caso, a invocação do procedimento na linha x armazenaria no endereço proc_P o valor do endereço ret_aqui. Com esse esquema, quando uma chamada de procedimento é executada, o endereço de retorno para aquela chamada fica armazenado em uma posição de memória conhecida. O retorno do procedimento pode ser executado pela seguinte sequência de instruções: 1 2 3 4 ; ... ld r1,proc_P jmp (r1) ; carrega endereço de retorno previamente ; e salta para esse endereço ; ... Esta solução foi adotada em alguns processadores antigos, como o IBM-1130 da década de 60. Ela no entanto tem várias limitações. Por exemplo, ela não funciona se o código do programa está sendo executado a partir de uma memória de leitura apenas, como ROMs ou EPROMs, muito comuns hoje em qualquer equipamento. Um outro problema, bem mais grave, é que esta solução não permite recursão: uma nova chamada ao procedimento de dentro do próprio procedimento faria com que o endereço de retorno da primeira chamada fosse destruído, já que o endereço dessa segunda chamada seria armazenado no mesmo lugar que o da primeira chamada. Na época, uma das linguagens mais utilizadas era FORTRAN, que não permitia recursão, e essa limitação não era tão incômoda. Obviamente essa primeira abordagem não é interessante hoje em dia, pois recursão é uma funcionalidade indispensável em linguagens de programação. Como permitir recursão? Sabemos que recursão envolve o uso de pilhas a solução portanto é o uso de uma pilha pelo processador. 4.0.1 Instruções que manipulam pilhas Uma pilha pode ser implementada no Faíska com um registrador de propósito geral, como r0 . Inicialmente, ele deve ser inicializado para apontar para uma região de memória disponível. Um dado é empilhado na pilha apontada por r0 decrementandose r0 de 4 e escrevendo-se o dado na nova posição apontada por r0 (neste esquema, 12 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES a pilha “cresce” de endereços altos para endereços baixos). Para retirar o elemento do topo da pilha procede-se à operação inversa: lê-se o valor da palavra apontada por r0 e incrementa-se r0 de 4, de forma a que ele aponte para o novo topo da pilha. Manipulações de pilhas são tão freqüentes que os processadores modernos incluem instruções específicas e um registrador especial, normalmente chamado sp (do inglês stack pointer), que funciona como um apontador de pilha. No Faíska o apontador de pilha é na verdade um dos registradores de uso geral, r15. Para comodidade, a linguagem de montagem aceita sp como outro nome do registrador r15. Duas operações são disponíveis para manipulação direta da pilha pelo usuário: push e pop. PUSH Empilha Sintaxe Operação Flags Codificação 31 push rf onte sp ← sp − 4 mem[sp] ← rdest – 0 rf onte 50 31 0 – POP Desempilha Sintaxe Operação Flags Codificação 31 pop rf onte ← mem[sp] sp ← sp + 4 – 51 0 rf onte 31 0 – Os operandos das instruções push e pop são sempre registradores. Assim, somente é possível empilhar e desempilhar palavras inteiras. Conjuntos menores de bits, como bytes, não podem ser empilhados individualmente. A figura abaixo ilustra a execução da instrução push. A instrução pop é similar. Exemplo 1 Exemplo de instrução empilha registrador 1 2 push r5 ; essa instrução é codificada usando uma ; palavra: 50000005h 4.1. CHAMADA E RETORNO DE PROCEDIMENTO Processador 13 Memória 00800004h r5 33 33 33 33 sp 00 80 00 00 ip 00 00 04 00 00 00 00 00 00800000h ff ff ff ff 007ffffch 00404h Antes Processador 50 00 00 05 00400h Memória 00800004h r5 33 33 33 33 sp 00 7f ff fc ip 00 00 04 04 00 00 00 00 00800000h 33 33 33 33 007ffffch 00404h Depois 50 00 00 05 00400h 4.1 Chamada e retorno de procedimento A implementação de procedimentos em processadores modernos faz uso do registrador apontador de pilha. No Faíska duas instruções específicas, que manipulam a pilha de forma indireta, são utilizadas. Uma para a chamada de procedimento, que empilha o endereço de retorno e desvia para o início do procedimento; e uma para o retorno do procedimento, que retira o endereço de retorno da pilha e executa o desvio correspondente. 14 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES CALL Chamada de procedimento Sintaxe Operação Flags call expr32 sp ← sp + 4 mem[sp] ← ip + 8 ip ← imd32 – sp ← sp + 4 mem[sp] ← ip + 4 ip ← rdest – Codificação 31 call rdest 0 54 31 0 imd32 31 0 55 rdest RET Retorno de procedimento Sintaxe Operação Flags ret ip ← mem[sp] sp ← sp + 4 – Codificação 31 0 56 A instrução chamada de procedimento empilha o endereço de retorno na pilha e executa o desvio para o início do procedimento. Como na instrução de desvio incondicional, o endereço alvo (início do procedimento) pode ser especificado através de um rótulo ou de um registrador. A instrução retorno de procedimento simplesmente desempilha o endereço de retorno da pilha e executa o desvio correspondente. Exemplo 2 Procedimento para zerar os registradores r0, r1, r2 e r3. 1 2 3 4 5 6 7 8 9 10 11 ; ZeraRegs ; procedimento para zerar os registradores r0, r1, r2 e r3 ; entrada: nenhuma ; saída: r0, r1, r2 e r3 zerados ; destrói: nada ZeraRegs: set r0,0 set r1,0 set r2,0 set r3,0 ret 4.2. PASSAGEM DE PARÂMETROS 15 É interessante ressaltar que em linguagem de montagem o conceito de procedimento é muito mais flexível que em uma linguagem de alto nível: não é feita qualquer verificação se o operando de um comando call representa mesmo o endereço inicial de um procedimento (isto apresenta algumas vantagens mas muitas desvantagens é preciso muita disciplina para programar corretamente). Assim, é possível utilizar o mesmo trecho de código para mais de um “procedimento”, como no exemplo seguinte. Exemplo 3 Dois procedimentos: um para zerar os registradores r0, r1, r2, r3, r4 e r5; e outro para zerar apenas os registradores r0, r1, r2 e r3. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ; Zera6Regs e Zera4Regs ; Zera6Regs zera os registradores r0, r1, r2, r3, r4 e r5 ; Zera4Regs zera os registradores r0, r1, r2 er3 ; entrada: nenhuma ; destrói: nada ; aqui é o ponto de entrada do primeiro procedimento Zera6Regs: set r5,0 set r4,0 ; aqui é o ponto de entrada do segundo procedimento Zera4Regs: set r3,0 set r2,0 set r1,0 set r0,0 ret 4.2 Passagem de parâmetros Até o momento consideramos apenas procedimentos sem parâmetros. Como podemos implementar a passagem de parâmetros? Uma primeira idéia é utilizar os registradores para a passagem de parâmetros. 4.2.1 Passagem de parâmetros por registradores Este método é bastante eficiente, e pode ser usado se o procedimento não é recursivo e o número de parâmetros é pequeno em relação ao número de registradores disponíveis. Exemplo 4 Procedimento para preencher uma região de memória com um valor dado. Os parâmetros são passados por registradores; o byte menos significativo de r0 contém o valor a ser usado no preenchimento, r1 contém endereço inicial da região 16 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES de memória, r2 contém o número de bytes a serem preenchidos com o valor dado. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ; ; ; ; ; ; ; ; Preenche procedimento para preencher com um dado byte uma região de memória entrada: r0 tem byte a ser usado no preenchimento r1 tem endereço inicial da região de memória r2 tem o número de bytes da região que devem ser preenchidos saída: nenhuma destroi: r0, r1, r2, r3 e flags Preenche: sub js stb add jmp final: ret r2,1 final (r1),r0 r1,1 Preenche ; ; ; ; continua a preencher? desvia se terminou de preencher preenche um byte com valor dado avança apontador ; e retorna quando toda região estiver preenchida Para preencher 100 bytes a partir da posição 1000h com o valor 0ffh, podemos utilizar o procedimento Preenche da seguinte maneira: 1 2 3 4 4.3 set set set call r0,0ffh r1,1000h r2,100 Preenche ; valor para preencher memória ; endereço inicial ; número de bytes a preencher Passagem de parâmetros pela pilha Apesar de eficiente, a passagem de parâmetros por registradores tem muitas limitações e não pode ser utilizada em todos os casos, particularmente no caso de procedimento recursivos. Em geral, a melhor maneira de passar parâmetros é utilizando a pilha. Para passar parâmetros utilizando a pilha, devemos empilhar os parâmetros antes da chamada do procedimento. Dentro do procedimento, para acessar os parâmetros podemos utilizar o registrador apontador de pilha. Suponha que desejemos chamar um procedimento proc com dois parâmetros param1 e param2. O código abaixo mostra a preparação da chamada do procedimento, com os dois parâmetros sendo empilhados antes da instrução de chamada de procedimento: 4.3. PASSAGEM DE PARÂMETROS PELA PILHA 1 2 3 4 5 6 17 ; supondo que param1 esteja armazenado em r4 e param2 armazenado em r5 ; exemplo de chamada de procedimento push r4 ; empilha primeiro parâmetro push r5 ; empilha segundo parâmetro call proc ; agora efetua a chamada ... A configuração da pilha após a execução da chamada é mostrada na Figura 4.3 Memória endereços crescentes param1 sp+8 param2 sp+4 end.retorno sp Figura 4.1: Diagrama ilustrando a configuração da pilha durante a execução de um procedimento com dois parâmetros. Para acessar os parâmetros, o código do procedimento deve incluir algo como: 1 2 3 4 proc: ld ld r0,(sp+4) r1,(sp+8) ; carrega segundo parâmetro em r0 ; e primeiro parâmetro em r1 ; ... Note que para acessar os parâmetros na pilha não deve ser usada a instrução pop, pois o endereço de retorno também está na pilha (foi empilhado após os parâmetros, pela instrução call) e seria desempilhado. Portanto, os parâmetros empilhados devem permanecer na pilha até o retorno do procedimento, e devem ser desempilhados após o retorno do procedimento. O código completo do exemplo de chamada de procedimento é 1 2 3 4 5 6 ; supondo que param1 esteja armazenado em r4 e param2 armazenado em r5 ; exemplo de chamada de procedimento push r4 ; empilha primeiro parâmetro push r5 ; empilha segundo parâmetro call pro ; agora efetua a chamada add sp,8 ; retira os parâmetros da pilha Note que não é necessário utilizar várias instruções pop para retirar os parâmetros da pilha; é mais eficiente manipular diretamente o apontador de pilha com 18 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES a instrução add. Dessa forma, várias palavras podem ser “desempilhadas” com uma única instrução. Diferentes linguagens de alto nível utilizam diferentes esquemas para empilhar os parâmetros. Nas implementações da linguagem C, por exemplo, os parâmetros são empilhados na ordem inversa em que foram declarados. Para o procedimento TesteC declarado abaixo 1 void TesteC(a: int, b: int) os parâmetros seriam empilhados assim: 1 2 3 4 5 6 ld push ld push call add r0,b r0 r0,a r0 TesteC sp,8 ; último parâmetro declarado ; primeiro parâmetro declarado ; retira os dois parâmetros da pilha. Em linguagem de montagem, podemos fazer a nossa própria convenção sobre a ordem com que empilhamos os parâmetros. Mas se um procedimento escrito em linguagem de montagem vai fazer parte de uma biblioteca, podendo ser invocado por programas escritos em uma linguagem de alto nível, é necessário obedecer as convenções estabelecidas pela implementação da linguagem em questão. 4.4 Retorno de valores de funções No caso de funções (procedimentos que retornam valores), o valor ou valores podem ser retornados ou em registradores ou pela pilha. Exemplo 5 Escreva um procedimento que devolva em r0 o número de bits 1 de uma palavra de 32 bits passada como parâmetro pela pilha. 4.4. RETORNO DE VALORES DE FUNÇÕES 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19 ; ContaUm ; procedimento para contar o número de bits 1 ; entrada: palavra de 32 bits passada pela pilha ; saída: número de bits 1 em r0 ; destrói: r1, r2 e r3 ContaUm: ld r1,(sp+4) ; carrega parâmetro set r3,32 ; vamos contar 32 bits xor r0,r0 ; zera registrador resultado proxbit: mov r2,r1 ; copia em rascunho and r2,1 ; isola bit menos significativo add r0,r2 ; e soma ao resultado ror r1,1 ; prepara para proximo bit sub r3,1 ; verifica se chegou ao final jnz proxbit ; se nao verificou todos os bits, desvia ret ; retorna com valor em r0 Exemplo 6 Escreva um procedimento que verifique se duas palavras de 32 bits passadas como parâmetro na pilha têm o mesmo número de bits 1. Caso ambas tenham o mesmo número, o procedimento deve retornar o bit de estado C desligado, e o registrador r0 deve conter o número de bits 1. Caso contrário, o bit de estado C deve retornar ligado. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ; ComparaUm ; procedimento para comparar o número de bits 1 ; entrada: duas palavras de 32 bits passadas pela pilha ; saída: se iguais, número de bits 1 em r0 e flag C desligada ; se diferentes, flag C ligada ; destrói: r1 e flags ComparaUm: ld r0,(sp + 8) ; carrega primeiro parâmetro push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta o número de bits 1 do primeiro add sp,4 ; retira parametro da pilha mov r1,r0 ; guarda resultado intermediario em registrador ld r0,(sp + 4) ; carrega segundo parâmetro push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta o número de bits 1 do segundo add sp,4 ; retira parametro da pilha cmp r0,r1 ; compara resultados jz final ; se iguais, pode retornar (e C já está ; com o valor correto) stc ; caso contrário, liga bit de estado C ; (se r0 > r1 ainda estava desligado) final: ret ; retorna com valor em r0 20 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES Esta versão de ComparaUm apresenta um problema, porque ContaUm destrói o registrador r1, que é usado em ComparaUm para guardar o número de bits 1 do primeiro parâmetro. Quando ContaUm é invocado pela segunda vez, o resultado da primeira chamada é perdido. Uma solução para este problema seria utilizar um outro registrador que não r1, r2 ou r3, que sabidamente são alterados por ContaUm, para armazenar o resultado da primeira chamada. Mas como o número de registradores é finito, em um programa real em algum momento será necessário reutilizar registradores. Como fazer para preservar o valor dos registradores durante uma chamada de procedimento, já que eles podem ser usados dentro do procedimento? A resposta é mais uma vez o uso da pilha: antes da chamada do procedimento os valores dos registradores que se deseja preservar podem ser guardados na pilha. Após o retorno do procedimento, os registradores podem ser restaurados com os valores anteriores. Vamos re-escrever o procedimento ComparaUm utilizando esta idéia: Exemplo 7 Escreva um procedimento que devolva em r0 o número de bits 1 de uma palavra de 32 bits passada como parâmetro pela pilha. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ; ComparaUm --- segunda versão ; procedimento para comparar o número de bits 1 ; entrada: duas palavras de 32 bits passadas pela pilha ; saída: se iguais, número de bits 1 em r0 e flag C desligada ; se diferentes, flag C ligada ; destrói: r1 e flags ComparaUm: ld r0,(sp + 4) ; carrega primeiro parâmetro push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta bits 1 do primeiro parâmetro add sp,4 ; retira parametro da pilha mov r1,r0 ; guarda resultado em registrador ld r0,(sp + 8) ; carrega segundo parâmetro push r1 ; salva para nao ser destruido por ContaUm push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta bits 1 do segundo parâmetro add sp,4 ; retira parametro da pilha pop r1 ; recupera resultado para primeiro cmp r0,r1 ; compara resultados jz final ; se iguais, pode retornar (e C está ; com o valor correto) stc ; caso contrário, liga bit de estado C ; (se r0 > r1 ainda estava desligado) final: ret ; retorna com valor em r0 (Nesse caso particular é possível diminuir o código do procedimento, alterando as instruções das linhas 12 ˜ 14 para 4.4. RETORNO DE VALORES DE FUNÇÕES 1 2 3 21 push r0 ; guarda resultado na pilha ld r0,(sp + 8) ; carrega segundo parâmetro ; linha suprimida mas o importante no exemplo é a ilustração da técnica de salvar temporáriamente o valor de registradores na pilha.) Se um programa está sendo todo escrito em linguagem de montagem, e não envolve recursão, é possível (e necessário, embora custoso) fazer um mapeamento dos registradores de forma a minimizar o conflito de registradores utilizados pelos diversos procedimentos. Um bom compilador para uma linguagem de alto nível também faz esse trabalho de otimização do uso de registradores, principalmente para máquinas com muitos registradores (o processador MIPS, da MIPS Technologies, tem 32 registradores de propósito geral). Exemplo 8 Escreva um procedimento que inverta uma cadeia de caracteres cujo endereço inicial é passado na pilha. O final da cadeia é marcado com o caractere ‘$’. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ; ; ; ; ; Inverte procedimento para inverter uma cadeia entrada: endereço da cadeia; final da cadeia é caractere ’$’ saída: nenhuma destroi: r1 e flags Inverte: ld r2,(sp+4) ; carrega endereço inicial da cadeia ; primeiro procura final da cadeia mov r1,r2 ; guarda apontador inicio da cadeia achaFinal: ldb r0,(r2) add r2,1 ; avança apontador cmp r0,$ ; chegamos ao final? jnz Acha Final ; desvia se ainda nao é final sub r2,1 ; r2 agora aponta para último ... ; elemento da cadeia ; agora começa a a inversão, que continua enquanto... ; apontador do inicio é menor que apontador do final da cadeia proxchar: cmp r1,r2 ; verifica se cadeia terminou jc final ; sim, terminou, vai retornar ldb r0,(r1) ; elemento do início ldb r3,(r2) ; elemento do final stb (r1),r3 stb (r2),r0 ; faz a troca add r1,1 ; avança ponteiro do inicio sub r2,1 ; retrocede ponteiro do final jmp proxchar final: ret 22 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES Esta solução tem um detalhe interessante. Nos processadores modernos, a execução de um desvio é bem mais demorada se o desvio é tomado (ou seja, se a condição do desvio é verdadeira) do que se o desvio não é tomado e a execução seqüencial de instruções continua. No processador Intel Pentium, por exemplo, a execução de um desvio condicional é cerca de 20 vezes mais demorada se o desvio é tomado (veja as razões na seção x). Na solução acima, o loop onde é feita a inversão poderia ser re-escrito com o sentido da condição da linha 22 invertido: 22 23 24 25 26 27 28 29 30 31 32 33 proxchar: cmp jnc ret continua: ldb ldb stb stb add sub jmp r1,r2 continua ; verifica se cadeia terminou ; nao terminou, vai inverter r0,(r1) r3,(r2) (r1),r3 (r2),r0 r1,1 r2,1 proxchar ; elemento do início ; elemento do final ; faz a troca ; avança ponteiro do inicio ; retrocede ponteiro do final O trecho acima executa exatamente o mesmo trabalho que o trecho original, mas leva muito mais tempo, pois a maioria das vezes a condição da linha 3 é verdadeira e o processador toma o desvio. Portanto, este simples “detalhe” faz com que o programa execute muitas vezes mais lentamente. É necessário estar sempre atento ao sentido da condição dentro de loops, especialmente no caso de loops encaixados, quando o efeito é multiplicado. 4.4.1 Passagem de parâmetros por referência e por valor Parâmetros em linguagens de alto nível como C ou Pascal podem ser passados por referência ou por valor, como na declaração abaixo, em que os parâmetros c e v são passados por valor, e o parâmetro r é passado por referência: void ExemploParams(char c, integer v, integer *r) Em linguagem de montagem, a distinção entre referência e valor é exatamente a mesma, mas o controle é feito exclusivamente pelo programador. Se um valor é passado para um procedimento (pela pilha ou por registrador), o esquema é obviamente de passagem por valor. Se um endereço é passado, de forma que o procedimento pode alterar o valor contido naquele endereço, a passagem é por referência. Mas como não há verificação de tipos, é necessário tomar bastante cuidado para não cometer erros na passagem de parâmetros. Como exemplo, uma chamada ao procedimento Params acima 4.4. RETORNO DE VALORES DE FUNÇÕES 23 ExemploParams(’a’, val, &ref); poderia ser traduzida em linguagem de montagem para 1 2 3 4 5 6 7 8 set push ld push set push call add r0, ’a’ r0 r0, val r0 r0, ref r0 ExemploParams sp, 12 ; carrega primeiro parâmetro ; carrega valor do segundo parâmetro ; carrega endereço do terceiro parâmetro ; desempilha parâmetros 4.4.2 Variáveis locais a procedimentos Variáveis locais a um procedimento também devem ser alocadas na pilha, se o procedimento for recursivo ou se utiliza muitas variáveis locais e o número de registradores não é suficiente para alocá-las. O espaço para as variáveis locais é reservado na entrada do procedimento, e desalocado ao final do procedimento. Para alocar espaço na pilha, basta decrementar sp pelo número de bytes desejado. No entanto, ao reservarmos espaço para variáveis locais na pilha, o deslocamento necessário para acessar os parâmetros se altera, em relação a procedimentos sem variáveis locais. Além disso, como vimos, o valor de sp pode variar durante a execução do procedimento, pela necessidade de armazenar temporariamante algum valor na pilha. Isto também faz com que os deslocamentos necessários para acessar os parâmetros e variáveis locais se altere. Ou seja, apesar de o deslocamento normal para acessar o primeiro parâmetro ser 4, se houver um dado temporário na pilha esse valor passa para 8. Para evitar que os deslocamentos sejam alterados durante a execução do procedimento, gerando confusão, é comum utilizarmos mais um registrador, geralmente chamado apontador de quadro (em inglês, frame pointer), que é mantido fixo, apontando para a posição inicial de sp no procedimento. No Faíska, o registrador r14 é usado para esse fim, e a linguagem de montagem aceita o nome fp como sinônimo de r14. Como o valor antigo de fp não deve ser destruído, é necessário que o empilhemos logo na entrada do procedimento. Durante a execução de um procedimento com três parâmetros e duas variáveis locais a pilha tem portanto a configuração mostrada na figura abaixo: 24 CAPÍTULO 4. PROCEDIMENTOS E FUNÇÕES Memória endereços crescentes fp+16 param3 fp+12 param2 fp+8 param1 fp+4 end.retorno fp fp anterior sp+8 fp-4 varlocal1 sp+4 fp-8 varlocal2 sp Note que, em relação a fp, parâmetros têm deslocamentos positivos e variáveis locais têm deslocamentos negativos, e esses deslocamentos se mantém constantes durante a execução do procedimento, independente de mais valores serem empilhados. Como exemplo, vamos re-escrever o procedimento ComparaUm usando uma variável local: 4.4. RETORNO DE VALORES DE FUNÇÕES 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ; ComparaUm --- versão com variável local ; procedimento para comparar o número de bits 1 ; de duas palavras passadas como parâmetro ; entrada: duas palavras de 32 bits passadas pela pilha ; saída: se iguais, número de bits 1 em r0 e flag C desligada ; se diferentes, flag C ligada ; destrói: r1 e flags ComparaUm: push fp ; guarda fp antigo mov fp, sp ; apontador de quadro sub sp, 4 ; reserva espaço para variável local i ld r0, (fp+8) ; carrega primeiro parâmetro push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta bits 1 do primeiro parâmetro add sp, 4 ; retira parâmetro da pilha st (fp-2), r0 ; guarda resultado na variável local ld r0, (fp+4) ; carrega segundo parâmetro push r0 ; empilha como parâmetro para ContaUm call ContaUm ; conta bits 1 do segundo parâmetro add sp, 4 ; retira parametro da pilha ld r1, (fp-2) ; recupera resultado para primeiro parâmetro cmp r0, r1 ; compara resultados jz final ; se iguais, pode retornar (e C está desligado) stc ; caso contrário, liga bit de estado C (pois se ; r0 > r1 ainda estava desligado) final: add sp, 4 ; desaloca variável local pop fp ret ; retorna com valor em r0 25