2.3 Aspectos Práticos
2.3.2 Como salvar variáveis em chamadas de procedimento
Conforme descrito na seção 2.3.1, tanto parâmetros quanto variáveis locais devem ser alocados preferencialmente em registra- dores para minimizar o acesso à memória e consequentemente melhorar o desempenho dos programas.
Isto significa que no programa apresentado no algoritmo 26 (reimpressão do algoritmo 24), as variáveis da funçãofatdeve- riam ser alocadas em registradores. Pela tabela 10, a variávelresdeve ser alocada ao registrador%rdie a variávelnno registra- dor%rsi. Já para a variávelrdeve ser escolhido outro registrador. Como%raxe%rbxsão usados como intermediários, vamos considerar que seja mapeado no registrador%rcx.
1
void fat (long int *res, long int n)
2{
3int r;
4if (n<=1)
5*res=1;
6else {
7fat (res, n-1);
8r = *res * n;
9*res=r;
10}
11}
12
main (long int argc, char **argv)
13{
14
long int x;
15fat (&x, 3);
16return (x);
17}
Algoritmo 26: Reimpressão do programa recursivo do algoritmo 24
A questão interessante é o que acontece a partir da segunda chamada recursiva. Como o código é estático, o mesmo ma- peamento é usado, sobrescrevendo o valor contido nos mesmos registradores usados para armazenar as variáveis do primeiro registro de ativação. Na “ida” das chamadas recursivas, tudo funciona. O problema é que na volta, os valores anteriores estarão perdidos.
Este problema não ocorre só na recursão, mas em qualquer chamada de procedimento. Seja R1o conjunto de registradores usado em um procedimentop1e seja R2o conjunto de registradores usado em um procedimentop2. Se R1∩R26= ;, então existe pelo menos uma variável emp2que, se alterada, alterará o valor de uma variável dep1.
Por exemplo, considere o algoritmo 27. Suponha que a variávelié mapeada no registrador%rdi. Suponha que a subrotina
2.3. Aspectos Práticos 49
subrotinaprintfsó será executada uma das 100 vezes pretendida.
1 . . .
2 for (i=0; i<100; i++) {
3 printf ("}
Algoritmo 27: Programa com um printf dentro de um for
Seja R1a tabela contendo a lista dos registradores e os valores contidos em cada um destes registradores antes da chamada de um procedimento, digamos antes de chamar a funçãoprintf. Seja R2uma tabela contendo a lista dos registradores e os valores contidos em cada um deles no retorno da funçãoprintf.
A primeira impressão é que R1= R2, porém raramente isto será verdadeiro, uma vez que a funçãoprintfpoderá utilizar alguns destes registradores, sobrescrevendo-os. Por esta razão, o normal é que R16= R2.
Por exemplo, considere que%rcx=0x10antes da instruçãocall printf. Quando do retorno da função (ou seja, na linha seguinte à instruçãocall), podemos ter%rcx=0xff, ou qualquer outro valor, inclusive%rcx=0x10.
Se%rcxfosse usado para armazenar uma variável local antes da chamada aprintf, então esta variável misteriosamente iria mudar de valor ao retornar (bem, já não é tão misterioso agora...).
Para contornar este problema, a solução é salvar os valores contidos em registradores na pilha. Para tal existem duas aborda- gens:
salvar registradores no procedimento chamador (caller save) Aqui, o conjunto de registradores utilizados pelo chamador são
salvos antes de empilhar os parâmetros da chamada do procedimento e restaurados na volta.
salvar registradores no procedimento chamado (callee save) Aqui, o conjunto de registradores que serão usados na função são
empilhados após alocar as variáveis locais (se houver) e desempilhados antes de liberar as variáveis locais.
Estas duas abordagens podem ser facilmente implementadas com o uso das instruçõespushepop. Porém, a convenção adotada usa um método diferente: assim que finaliza a construção de um registro de ativação (após alocar as variáveis locais), o procedimento chamado abre um espaço na pilha para abrigar os dois conjuntos de variáveis.
O algoritmo 28 descreve como proceder para salvar registradores que o procedimento chamador quer manter inalterados pela chamada da linha 8 (call funcao).
Considere que este procedimento quer salvar os parâmetros passsados para ele (em%rsi, %rdi e %rdx). O primeiro passo é abrir espaço para salvá-los na pilha como indicado na linha 2 do algoritmo 28. Antes de carregar os parâmetros do procedimento deve salvar os registradores na pilha (linhas 4, 5 e 6). Após retornar do procedimento, deve restaurar os registradores (linhas 10, 11 e 12). Ao final do procedimento, é necessário liberar o espaço alocado para os salvamentos (linha 14).
O algoritmo 29 descreve como proceder para salvar registradores que serão alterados no procedimento chamado. Suponha que o procedimentofuncaoirá alterar os valores contidos nos registradores%rbx,%r12e%r13. Para que o valor destes registra- dores na entrada do procedimento sejam iguais a valor na saída do procedimento, ele salva os valores na entrada (linhas 6, 7 e 8)
e os restaura na saída 10, 11, e 12.
1 . . .
2 subq $24, %rsp # Aloca espaço para três variáveis pelo procedimento chamador
3 . . .
4 movq %rsi, -8(%rbp) # salva %rsi
5 movq %rdi, -16(%rbp) # salva %rdi
6 movq %rddx, -24(%rbp) # salva %rdx
7 <carrega Parâmetros>
8 call funcao
9 <descarrega Parâmetros>
10 movq -8(%rbp), %rsi # restaura %rsi
11 movq -16(%rbp), %rdi # restaura %rdi
12 movq -24(%rbp), %rdx # restaura %rdx
13 . . .
14 addq $24, %rsp
15 . . .
Algoritmo 28: Abordagem de salvar registradores pelo caller
1 funcao:
2 pushq %rbp
3 movq %rsp, %rbp
4 . . .
5 subq $32, %rsp %rsp # Aloca espaço para quatro variáveis
6 movq %rbx, -8(%rbp) # salva %rbx 7 movq %r12, -16(%rbp) # salva %r12 8 movq %r13, -24(%rbp) # salva %r13 9 . . . # usa %rbx, %r12, %r13 10 movq -8(%rbp), %rbx # restsaura %rbx 11 movq -16(%rbp), %r12 # restsaura %r12 12 movq -24(%rbp), %r13 # restsaura %r13 13 addq $32, %rsp 14 . . . 15 popq %rbp 16 ret
Algoritmo 29: Abordagem de salvar registradores pelo callee
Cada uma das duas abordagens tem contexto diferente: enquanto o chamador defende-se dos atos do procedimento cha- mado, o procedimento chamado atua preventivamente. Porém, isto pode fazer com que um registrador seja salvo por ambos.
Para evitar este problema, a ABI-AMD64 [43] indica quais registradores são preservados entre chamadas de procedimento (ou seja que devem ser salvos pelo callee se usados). A tabela 11 apresenta esta convenção.
A tabela mostra que o procedimento chamadocalleeé reponsável por garantir que os valores contidos nos registradores
%rbx,%r12-%r15sejam, na saída do procedimento, os mesmos que eram na entrada (veja modelo no algoritmo 29).
Os registradores%rax,%rcx,%rdx,%rsi,%rdi,%r8,%r9,%r10e%r11devem ser salvos pelo chamadorcaller(veja modelo no algoritmo 28).
Os registradores%rspe%rbpsão preservados no registro de ativação do procedimento chamado.