Capítulo 4 Procedimentos e funções - IC

Propaganda
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
Download