Geração de Código Simão Melo de Sousa RELEASE - RELiablE And SEcure Computation Group Computer Science Department University of Beira Interior, Portugal [email protected] http://www.di.ubi.pt/˜desousa/ S. Melo de Sousa (DIUBI) Geração de Código 1 / 92 Este documento é uma tradução adaptada do capítulo "Generation de Code" da sebenta "Cours de Compilation" de Christine Paulin (http://www.lri.fr/˜paulin). S. Melo de Sousa (DIUBI) Geração de Código 2 / 92 Ponto da situação As análises léxica e sintáctica permitam construir uma primeira versão da árvore abstracta e eliminar programas incorrectas. A análise semântica opera recursivamente sobre a árvore de sintaxe abstracta para calcular informações actualizadas (porte, tipos, sobrecarga de operadores, etc.) que são arquivadas na árvore ou fora. Novos erros são detectados nesta fase. Outras representações como o grafo de fluxo (que não abordamos, infelizmente, aqui) levantarão informações ligadas à execução do programa. A geração de código produz um novo programa, executável. S. Melo de Sousa (DIUBI) Geração de Código 3 / 92 Programa para este capítulo Geração de código para o Assembly MIPS. Introdução: modelo de execução com base numa pilha Código intermédio: modelo baseado numa pilha I I I Instruções Casos de base Compilação das expressões condicionais Chamadas a procedimentos Linguagens funcionais (se tivermos tempo) Linguagens objectos (se tivermos tempo) S. Melo de Sousa (DIUBI) Geração de Código 4 / 92 Plano 1 Introdução Modelo de Execução 2 Geração de código na presença de uma pilha 3 Dados Compostos 4 Compilação de chamadas a funções S. Melo de Sousa (DIUBI) Geração de Código 5 / 92 Modelo de Execução O tamanho do programa é conhecido durante a fase de compilação Uma parte da memória fica atribuída às instruções do programa um apontador (pc — program counter) aponta para o ponto actual no programa onde se encontra a execução o programa executa-se de forma sequencial. Excepto no caso explícito de instruções de salto, as instruções do programa são executadas de forma sequencial, uma a seguir a outra. S. Melo de Sousa (DIUBI) Geração de Código 6 / 92 Modelo de Execução Os dados do programa são arquivadas na memória ou nos registos da máquina. A memória encontra-se organizada em palavra (32 ou 64 bits). É acedida pelo meio de um endereço (inteiro). Os valores simples são arquivados numa unidade de memória Os valores complexos são arquivados em unidades de memória consecutivas (estruturas, vectores, etc.), ou então em estruturas encadeadas de unidades de memória (listas encadeadas de unidades de memória, onde se arquiva uma componente do valor por arquivar e aponta-se para o resto da lista, i.e. a próxima unidade que arquiva a componente seguinte, etc.). Os registos permitam aceder rapidamente aos dados simples Mas, o número de registos depende da arquitectura alvo e em geral é bastante limitado. S. Melo de Sousa (DIUBI) Geração de Código 7 / 92 Alocação Certos dados são explicitamente manipulados pelo programa fonte pelo intermédio de variáveis. Outros são criados pelo próprio compilador: I I I Valores intermédios num cálculo aritmético não trivial Variáveis aquando de uma chamada a função Valores objecto, valores funcionais... Certos dados têm um tempo de vida/de utilização conhecido na fase de compilação: regras de porte, analise de liveness. S. Melo de Sousa (DIUBI) Geração de Código 8 / 92 Alocação As variáveis globais do programa podem ficar alocadas para endereços fixos, desde que se conheça antecipadamente o tamanho destes. A gestão das variáveis locais, dos blocos, dos procedimentos ou ainda do arquivo dos valores intermédios podem ser feitos alocando memória na pilha (stack) Outros dados podem ter um tempo de vida que não é conhecida na fase de compilação. Este é o caso quando se manipulam apontadores (que são expressões cujo valor é um endereço). Estes dados vão ser alocados em geral numa outra parte da memória, organizada em heap (a estrutura de dados Heap na wikipedia). S. Melo de Sousa (DIUBI) Geração de Código 9 / 92 Memória O espaço reservado na heap deverá ser liberto quando deixa de ser utilizado. De forma explícita: C, Pascal. De forma autómatica e imlpícita: a execução de um programa utiliza um programa geral de recuperação de memória, comunamente designado de GC ou Garbage Collector (Caml, Java, etc.) S. Melo de Sousa (DIUBI) Geração de Código 10 / 92 Esquema organizativo da memória sp : stack pointer gp : global pointer pc : program counter S. Melo de Sousa (DIUBI) Geração de Código 11 / 92 Código intermedio Código/programa independente da máquina alvo factoriza grande parte do trabalho de compilação torna o compilador mais modular/adaptável/genérico A escolha de uma linguage intermédia é muito importante deve ser suficientemente rico para permitir uma fácil codificação das operações da linguagem sem criar uma longa sequência de código deve ser suficientemente limitada (baixo nível) para que a tradução final não seja demasiada custosa S. Melo de Sousa (DIUBI) Geração de Código 12 / 92 Linguagem assembly numero de registos limitado, mas indispensáveis para os cáculos Necessidade da salveguarda de determinados em pilha. Passagem da linguagem de alto nível para o assembly via várias linguagens intermédias O próprio assembly utiliza pseudo-instruções de mais alto nível do que código máquina e labels simbólicos S. Melo de Sousa (DIUBI) Geração de Código 13 / 92 Plano 1 Introdução 2 Geração de código na presença de uma pilha Modelo de execução na presença de uma pilha MIPS Expressões aritméticas simples Variáveis Condicionais 3 Dados Compostos 4 Compilação de chamadas a funções S. Melo de Sousa (DIUBI) Geração de Código 14 / 92 Notações Vamos especificar uma função code que aceita em argumento uma árvore de sintaxe abstracta e devolve uma sequência de instrução para a máquina de pilhas. Notação Se E ::= E1 + E2 é uma regra da gramática da linguagem então code(E1 + E2 ) = . . . code(E1 ) . . . code(E2 ) especifica o valor da função code sobre a árvore de sintaxe abstracta que corresonde à soma de duas expressões. Se C1 e C2 representam sequências de instruções então C1 |C2 é a notação para a sequencia de instruções resultante da concatenação de C1 com C2 . A notação para a sequência vazia é []. S. Melo de Sousa (DIUBI) Geração de Código 15 / 92 Máquina de pilhas Arquivar os valores intermédios na pilha Trocar os valores entre memória e registos para os cálculos A pilha pode conter inteiros, flutuantes ou endereços Certos dados de grande tamanho como cadeias de caracteres são alocados num espaço suplementar. Quando for necessário manipular uma cadeia de caracteres, manipular-se-á na pilha o seu endereço. S. Melo de Sousa (DIUBI) Geração de Código 16 / 92 Arquitectura e assembly MIPS Máquina alvo para estas aulas. MIPS na wikipedia Um bom livro de Arquitectura de computador que introduz a arquitectura MIPs: Computer Organization & Design: The Hardware/Software Interface, Second Edition. By John Hennessy and David Patterson. Published by Morgan Kaufmann, 1997. ISBN 1-55860-428-6. uns acetatos completos e pedagógicos sobre o MIPS: (link1) (link2) SPIM: Simulador MIPS I I (link1) (link2) S. Melo de Sousa (DIUBI) Geração de Código 17 / 92 MIPS em resumo li $r0, C lui $r0, C move $r0, $r1 $r0 <- C $r0 <- 2^16*C $r0 <- $r1 add addi sub div div mul neg $r0, $r0, $r0, $r0, $r1, $r0, $r0, $r1, $r1, $r1, $r1, $r2 $r1, $r1 $r2 C $r2 $r2 slt slti sle seq sne $r0, $r0, $r0, $r0, $r0, $r1, $r1, $r1, $r1, $r1, $r2 C $r2 $r2 $r2 $r2 S. Melo de Sousa (DIUBI) $r0 $r0 $r0 $r0 $lo $r0 $r0 <<<<<<<- $r1 + $r1 + $r1 $r1 / $r1 / $r1 * -$r1 $r0 $r0 $r0 $r0 $r0 <<<<<- 1 1 1 1 1 se se se se se Geração de Código $r2 C $r2 $r2 $r2, $hi <- $r1 mod $r2 $r2 (sem overflow) $r1 $r1 $r1 $r1 $r1 < < <= = <> $r2, C, $r2, $r2, $r2, $r0 $r0 $r0 $r0 $r0 <<<<<- 0 0 0 0 0 senão senão senão senão senão 18 / 92 MIPS em resumo la $r0, adr $r0 <- adr lw $r0, adr $r0 <- mem[adr] sw $r0, adr mem[adr] <- $r0 beq beqz bgt bgtz $r0, $r0, $r0, $r0, salto salto salto salto $r1, label label $r1, label label se se se se $r0 $r0 $r0 $r0 = = > > $r1 0 $r1 0 beqzal $r0, label bgtzal $r0, label salto se $r0 = 0, $ra <- $pc + 1 salto se $r0 > 0, $ra <- $pc + 1 j jal jr jalr salto salto salto salto label label $r0 $r0 S. Melo de Sousa (DIUBI) Geração de Código para para para para label label, $ra <- $pc + 1 $r0 $r0, $ra <- $pc + 1 19 / 92 MIPS em resumo Registers Calling Convention Name Number Use Callee must preserve? $zero $0 constant 0 N/A $at $1 assembler temporary No $v0-$v1 $2-$3 values for function returns and expression evaluation No $a0-$a3 $4-$7 function arguments No $t0-$t7 $8-$15 temporaries No $s0-$s7 $16-$23 saved temporaries Yes $t8-$t9 $24-$25 temporaries No $k0-$k1 $26-$27 reserved for OS kernel N/A $gp $28 global pointer Yes $sp $29 stack pointer Yes $fp $30 frame pointer Yes $ra $31 return address N/A S. Melo de Sousa (DIUBI) Geração de Código 20 / 92 MIPS - syscall - Chamadas ao sistema Pode depender do simulador utilizado (consultar documentação) Serviço print_int print_float print_double print_string read_int Código em $v0 1 2 3 4 5 read_float read_double 6 7 read_string 8 sbrk/malloc exit 9 10 S. Melo de Sousa (DIUBI) Argumentos Resultados $a0 = o inteiro por imprimir $f12 = o float por imprimir $f12 = o double por imprimir $a0 = endereço da string por imprimir $v0 = o inteiro devolvido $f0 = o float devolvido $f0 = o double devolvido $a0 = endereço da string por ler $a1 = comprimento da string $a0 = quantidade de memória por alocar $v0 = o código devolvido Geração de Código endereço em $v0 21 / 92 MIPS - syscall - Chamadas ao sistema #Print out integer value contained in register $t2 li $v0, 1 # load appropriate system call code into register $v0; # code for printing integer is 1 move $a0, $t2 # move integer to be printed into $a0: $a0 = $t2 syscall # call operating system to perform operation #Read integer value, store in RAM location with label int_value #(presumably declared in data section) li $v0, 5 # load appropriate system call code into register $v0; # code for reading integer is 5 syscall # call operating system to perform operation sw $v0, int_value # value read from keyboard returned in register $v0; # store this in desired location #Print out string (useful for prompts) .data string1 .asciiz "Print this.\n" # declaration for string variable, # .asciiz directive makes string null terminated .text main: li $v0, 4 # load appropriate system call code into register $v0; # code for printing string is 4 la $a0, string1 # load address of string to be printed into $a0 syscall # call operating system to perform print operation S. Melo de Sousa (DIUBI) Geração de Código 22 / 92 MIPS - um exemplo : Fib #-----------------------------------------------# fib - recursive Fibonacci function. # http://www.cs.bilkent.edu.tr/~will/courses/ # CS224/MIPS%20Programs/fib_a.htm # # a0 - holds parameter n # s0 - holds fib(n-1) # v0 - returns result #-----------------------------------------------# Code segment .text fib: sub $sp,$sp,12 # save registers on stack sw $a0,0($sp) sw $s0,4($sp) sw $ra,8($sp) bgt $a0,1,notOne move $v0,$a0 b fret S. Melo de Sousa (DIUBI) # fib(0)=0, fib(1)=1 # if n<=1 Geração de Código 23 / 92 MIPS - um exemplo : Fib notOne: sub $a0,$a0,1 jal fib move $s0,$v0 fret: # param = n-1 # compute fib(n-1) # save fib(n-1) sub $a0,$a0,1 jal fib add $v0,$v0,$s0 # set param to n-2 # and make recursive call # add fib(n-2) lw $a0,0($sp) lw $s0,4($sp) lw $ra,8($sp) add $sp,$sp,12 jr $ra # restore registers # data segment .data endl: .asciiz "\n" S. Melo de Sousa (DIUBI) Geração de Código 24 / 92 Instruções macros para a máquina de pilhas Um registo $sp (stack pointer) que aponta para a primeira célula livre da pilha O endereçamento faz-se com base em bytes: uma palavra de 32 bits cabe em 4 bytes. Funções de arquivo e de carregamento na pilha: l e t p u s h r r = sub $sp , $sp , 4 sw r , 0 ( $ s p ) | l e t p o p r r = lw r , 0 ( $ s p ) | add $sp , $sp , 4 S. Melo de Sousa (DIUBI) Geração de Código 25 / 92 Esquema de compilação de expressões O resultado da expressão por compilar está no registo $a0. Os valores intermédios são arquivados na pilha O código correspondente é: code ( i n t e i r o ) c o d e ( E1 + E2 ) = = l i $a0 i n t e i r o c o d e ( E1 ) | p u s h r $a0 | c o d e ( E2 ) | p o p r $ t 0 | add $a0 $ t 0 $a0 Funciona, mas ... código ineficiente Podemos melhorar a situação utilizando registos temporários $ti (i = 0 . . . 9) e $si (i = 0 . . . 7) Pode ser realizado de forma simplista (passando uma pilha de registos disponíveis como parâmetro) ou pela utilização de analises estáticas muito mais pertinentes mais também mais complexas. S. Melo de Sousa (DIUBI) Geração de Código 26 / 92 Geração de código A função de geração de código é recursiva sobre a AST das expressões aritméticas. Invariante: Execução de code(E) num estado em que sp = n O valor da expressão E é calculada no registo $a0 Os valores de P[m] para m<n não foram modificados. S. Melo de Sousa (DIUBI) Geração de Código 27 / 92 As variáveis globais e locais As instruções lw e sw permitam deslocar valores entre um endereço memória e um registo. sw: atribuir um valor calculado para um registo reservado para as variáveis globais; lw carregar num registo um valor contido numa variável global Notação: se $r é um registo cujo valor é um endereço a na pilha e b é uma constante inteira, então n($r) designa o endereço a+n. S. Melo de Sousa (DIUBI) Geração de Código 28 / 92 Código para as variáveis globais E ::= ident Uma variável x poderá ser arquivada na zona de dados associada a um label lab(x) .data lab_x: .word 0 .text Assim code(id) = lw $a0 lab(id) Podemos também localizar uma variável (global, mas também local) x pelo offset adr(x) relativamente a um apontador global $gp. code(id) = lw k ($gp) se k=adr(id) O espaço necessário às variáveis é reservado antes da execução retirando a $sp 4 vezes o numero de variáveis por arquivar. S. Melo de Sousa (DIUBI) Geração de Código 29 / 92 Atribuições Imaginemos uma linguagem composta de sequências I de atribuições da forma id:=E code(epsilon) = [] code(I1 id:=E;) = code(I1) | code(id:=E) Code(id:=E) = code(E) | sw $a0 lab(id) code(E) | sw $a0 k($gp) S. Melo de Sousa (DIUBI) Geração de Código se id global se k=adr(id) 30 / 92 Condicionais As expressões condicionais e os ciclos usam instruções de saltos para navegar nas instruções As instruções poderão ser referenciadas por endereços simbólicos inseridos no programa. label: introduz um endereço simbólico labelque corresponde ao endereço real da instrução que segue. salto incondicional: j label (existe também outras variantes como b label) salto condicional (uma das variantes...) beqz $r label (salto se $r = 0) Convenção: false = 0, true = qualquer inteiro positivo (1, em particular) S. Melo de Sousa (DIUBI) Geração de Código 31 / 92 Condicionais e ciclos code(if E then I) = code(E) | beqz $a0 next | code(I) | next: code(if E then I1 else I2) = code(E) | beqz $a0 lab1 | code(I1) | j lab2 | lab1: code(I2) | lab2: code(while E do I done) = lab1: code(E) | beqz $a0 next | code(I) | j lab1 | next: Estas traduções introduzem novas etiquetas (next, lab1, lab2). S. Melo de Sousa (DIUBI) Geração de Código 32 / 92 Expressões booleanas A tradução de expressões booleanas processa-se como para o caso as expressões aritméticas. Vamos manipular os valores 0 e 1 (i.e. colocar na pilha...) com base na utilização das operações aritméticas que nos permitirão simular operações sobre os booleanos. code(true) = li $a0 1 code(false) = li $a0 0 code(B1 and B2) = code(B1) | pushr $a0 | code(B2) | popr $t0 | mul $a0, $t0, $a0 code(not B1) = code(B1) | neg $a0 | add $a0, $a0, 1 code(B1 or B2) = exercício.... S. Melo de Sousa (DIUBI) Geração de Código 33 / 92 Esquema de compilação com condicionais É comum compilar as expressões booleanas da seguinte forma: I I I B1 and B2 ! if B1 then B2 else false B1 or B2 ! if B1 then true else B2 not B1 ! if B1 then false else true No entanto, numa linguagem com efeitos laterais, o facto de calcular ou não as sub-expressões (aqui B1 ou B2) pode levar a comportamento globais bem diferentes. S. Melo de Sousa (DIUBI) Geração de Código 34 / 92 Esquema de compilação com saltos As expressões booleanas servem frequentemente como operação de controlo. Sejam E-true e E-false dois labels que usaremos neste contexto. A execução de E resulta em pc = E-true quando o valor de E é true ou resulta em pc = E-false no caso contrário. Definimos assim uma função code-bool que toma em entrada uma expressão booleana, os dois labels e que devolve o código de controlo S. Melo de Sousa (DIUBI) Geração de Código 35 / 92 Código gerado code-bool(true,lab-t,lab-f) = j lab-t code-bool(false,lab-t,lab-f) = j lab-f code-bool(not B1,lab-t,lab-f) = code-bool(B1,lab-f,lab-t) code-bool(B1 or B2,lab-t,lab-f) = code-bool(B1,lab-t,new-l) | new-l: code-bool(B2,lab-t,lab-f) code-bool(B1 and B2,lab-t,lab-f) = code-bool(B1,new-l,lab-f) | new-l: code-bool(B2,lab-t,lab-f) code-bool(E1 relop E2,lab-t,lab-f) = code(E1) | pushr $a0 | code(E2) | popr $t0 | code(relop) $a0, $t0, $a0 | beqz $a0 lab-f | j lab-t new-l: novo label. code(relop) = instrução de comparação para o operador relop (sle, slt, . . .) S. Melo de Sousa (DIUBI) Geração de Código 36 / 92 Compilação do teste Evitemos o empilhamento de valores intermédios: code(if E then I1 else I2) = code-bool(E,new-true,new-false) | new-true: code(I1) | j new-next | new-false: code(I2) | new-next: S. Melo de Sousa (DIUBI) Geração de Código 37 / 92 Plano 1 Introdução 2 Geração de código na presença de uma pilha 3 Dados Compostos Tipos produtos Vectores 4 Compilação de chamadas a funções S. Melo de Sousa (DIUBI) Geração de Código 38 / 92 Tipos estruturados Vamos aqui admitir que os vectores suportados são vectores à la Pascal. Ou seja que estes são declarados com a informação do primeiro índice e do último índice. Relembremos que em C, OCaml ou Java (etc.) um vector é declarado com o seu tamanho n type typ = Tbool | Tint | ... | Tprod of typ*typ | Tarr of int * int * typ A cada tipo está associada o tamanho dos dados correspondente let rec size = Tbool | Tint | Tprod (t1,t2) | Tarr (f,l,t) function -> 1 -> size t1 + size t2 -> max 0 (l-f+1) * size t S. Melo de Sousa (DIUBI) Geração de Código 39 / 92 Pares representados por valores em pilha Juntemos na linguagem de expressões a possibilidade em expressar pares de expressões, aceder a cada uma das duas componentes do par E ::= (E1,E2) | fst E | snd E O valor de uma expressão já não cabe por inteiro num registo Podemos então arquiva-la na pilha O valor de um objecto composto (e1,e2) pode ser I I I um bloco formado pelo valor de e1 seguido do valor de e2; o endereço do local em memória onde começa o arquivo dos dois valores (endereço que cabe num registo) é preciso escolher como posicionar as componentes e os endereços relativamente às componentes elas próprias. S. Melo de Sousa (DIUBI) Geração de Código 40 / 92 Pares representados por valores em pilha (1,(2,3)) .data par: .word 1 .word 2 .word 3 S. Melo de Sousa (DIUBI) Geração de Código 41 / 92 Cálculo dos valores na pilha Esquema escolhido: Reservar espaço num bloco, na pilha Quando queremos manipular um bloco estruturado, precisamos conhecer: I I I o endereço da parte inferior do bloco no qual o valor é arquivado o tamanho dos dados (conhecida estaticamente ou dinamicamente) o offset relativo às componentes (no caso das estruturas com vários campos). S. Melo de Sousa (DIUBI) Geração de Código 42 / 92 Código para os produtos Uma função codep coloca na pilha o valor do resultado Uma variável fica associada ao seu endereço e ao seu tamanho codep(E) = code(E) | pushr $a0 codep((E1,E2)) (sendo E uma expressão aritmética) = codep(E2) | codep(E1) codep(snd_(p1,p2)(E)) = codep(E) | add sp,sp,(4*p1) codep(fst_(p1,p2)(E)) = (copiar os valores certos no sítio certo) codep(id_n) = la $a1 adr(id) | lw $a0 (4 * (n - 1))($a1) | pushr $a0 | ... | lw $a0 0($a1) | pushr $a0 codep(id_n:= E) = codep(E) | la $a1 adr(id) | popr $a0 | sw $a0 0($a1) | ... | popr $a0 | sw $a0 (4 * (n-1))($a1) S. Melo de Sousa (DIUBI) Geração de Código 43 / 92 Vectores O espaço necessário para arquivar um vector depende do tipo dos dados arquivados neste (inteiros, reais, vectores, estruturas) e da estratégia de representação dos dados. Vamos aqui supor que o tamanho dos dados é sabido em tempo de compilação. Os vectores são representado por sequências de células adjacentes. vectores multidimensionais são linearizados (as linhas são concatenadas umas atrás das outras) Os acessos a vectores: offset calculados na execução via aritmética sob endereços O endereço por resolver depende de um endereço de base a e de um offset n calculado em tempo de execução S. Melo de Sousa (DIUBI) Geração de Código 44 / 92 Código : Expressões sob vectores Juntamos então os vectores (unidimencionais) a nossa linguagem. E ::= id[E] I ::= id[E1] := E2 Caso simples: Vector com índices a partir de 0 e de tamanho n de objectos de tamanho 1. adr(id) : endereço de base do vector code(id[E]) = | code(id[E1]:=E2) = | S. Melo de Sousa (DIUBI) code(E) | la $a1 adr(id) add $a1, $a1, $a0 | lw $a0, 0($a1) code(E1) | la $a1 adr(id) | add $a1, $a1, $a0 pushr $a1 | code(E2) | popr $a1 | sw $a0, 0($a1) Geração de Código 45 / 92 Vectores - Cálculos de índice Se o vector t tem índices entendidos entre m e M, e começa no endereço a e contém elementos de tamanho k, então este ocupa (M m + 1) ⇥ k Para calcular o endereço que corresponde a uma expressão t[E], é necessário calcular o valor n resultante da expressão E e em seguida aceder ao endereço (a + (n m) ⇥ k). Se conhecemos o valor de m em tempo de compilação então podemos parcialmente avaliar esta expressão calculando antecipadamente a m ⇥ k e guardando este valor na tabela de símbolos base(t). Bastará assim efectuar a operação base(t) + n ⇥ k S. Melo de Sousa (DIUBI) Geração de Código 46 / 92 Código - Expressões sob vectores - caso geral Valores pre-calculados (vector de índice entre m e M) adr (id) endereço de base do vector k = size(id) tamanho de um elemento do vector base(id) = adr (id) O código gerado é codep(id[E]) = | | ... | codep(id[E1] := E2) = | | | | ... | | S. Melo de Sousa (DIUBI) m ⇥ size(id) code(E) | mul $a0. $a0, k la $a1 base(id) | add $a1, $a1, $a0 lw $a0, (k-1)($a1) | pushr $a0 | ... lw $a0, 0($a1) | pushr $a0 code(E1) | mul $a0, $a0, k | la $a1 base(id) | add $a1, $a1, $a0 pushr $a1 | codep(E2) lw $a1, (4 * size(E2))$sp popr $a0 | sw $a0, 0($a1) ... popr $a0 | sw $a0, (k-1)($a1) addi $sp, $sp, 4 Geração de Código 47 / 92 Em resumo Manipular valores estruturados obriga à reserva atempada de espaço memória. Pode ser feita com base na pilha quando se controla o tempo de vida destes dados (por cause do cálculo de endereço por realizar). Uma solução mais uniforme pode passar por manipular objectos estruturados como apondadores para a heap. Para uma programação mais segura, pode juntar-se ao código gerado testes de segurança, como por exemplo a verificação de que os índices estão no âmbito do vector em causa. S. Melo de Sousa (DIUBI) Geração de Código 48 / 92 Plano 1 Introdução 2 Geração de código na presença de uma pilha 3 Dados Compostos 4 Compilação de chamadas a funções Exemplo de uma função recursiva Parâmetros de funções e procedimentos Tabela de activação Chamadas de funções e assembly Passagem por referência Funções Recursivas S. Melo de Sousa (DIUBI) Geração de Código 49 / 92 Introdução Compilação modular: código para a função utilizando parâmetros formais, código para a chamada à função que instância estes parâmetros (passagem de parâmetros, dos efectivos aos formais). Várias semânticas possíveis para o modo de ligação/instanciação dos parâmetros (por valor, referência etc.) Alocação dinâmica de novas variáveis (parâmetro, variáveis locais) S. Melo de Sousa (DIUBI) Geração de Código 50 / 92 Exemplo Alocação memória para os parâmetros de uma função recursiva no contexto de chamadas por valor int j; void p(int i; int j) {if (i+j) {j=j-1; p(i-2,j); p(j,i);}} main () {read(j); p(j,j);} O número de execução de p depende dos valores passados em entrada. À cada entrada no procedimento p duas novas variáveis são alocadas. À saída do procedimento, a memoria alocada é liberta. S. Melo de Sousa (DIUBI) Geração de Código 51 / 92 Exemplo - execução S. Melo de Sousa (DIUBI) Geração de Código 52 / 92 Acesso memória e valores Certas expressões em programas representam endereços memória I I I I variáveis introduzidas no programa; células de um vector; componentes de uma estrutura; apontadores. Nestas localizações em memória são arquivados valores Certas expressões em programas podem designar estas localizações ou então os valores que aí estão arquivados I I o valor esquerdo (ou left-value) de uma expressão designa o local em memória a partir do qual é arquivado o objecto (utilizado nas atribuições) o valor direito (ou right-value) de uma expressão designa o valor do objecto arquivado em memória no dito local. A passagem de parâmetro nos procedimentos pode fazer-se transmitindo os valores esquerdos ou valores direitos. S. Melo de Sousa (DIUBI) Geração de Código 53 / 92 Variáveis: ambiente e estado um ambiente liga nomes à endereços memória: é uma função parcial que associa um endereço a um identificador. um estado que liga endereços a valores: uma função parcial que associa um valor a um endereço. Aquando de uma atribuição de um valor a uma variável, só o estado muda Aquando da chamada de um procedimento (ou função) com parâmetros ou variáveis locais, o ambiente muda. A variável introduzida corresponde a uma nova ligação entre nome e endereço memória S. Melo de Sousa (DIUBI) Geração de Código 54 / 92 Procedimentos e Funções Um procedimento tem um nome, parâmetros, declarações de variáveis ou procedimentos locais e um corpo uma função é um procedimento que devolve um valor Os parâmetros formais são variáveis locais ao procedimento que serão instanciados aquando da chamada ao procedimento pelos parâmetros efectivos. O procedimento pode declarar variáveis locais que serão inicializadas no corpo do procedimento. S. Melo de Sousa (DIUBI) Geração de Código 55 / 92 Passagem de parâmetro Os parâmetros formais do procedimento são variáveis que são inicializados aquando da chamada ao procedimento. Existem várias formas de realizar tal inicialização. Admitindo o procedimento p com um parâmetro formal x que é invocado com o parâmetro efectivo e. Vamos examinar os diferentes modelos de passagem de parâmetros. S. Melo de Sousa (DIUBI) Geração de Código 56 / 92 Passagem por valor Na passagem por valor, x é uma nova variável alocada localmente pelo procedimento e cujo valor é o resultado da avaliação de e. Após o fim do procedimento, o espaço memória alocado à variável x é devolvido. as modificações que a variável x sofreu deixam de serem visíveis. Na ausência de apontadores, as únicas variáveis alteradas são as variáveis não locais ao procedimento explicitamente referenciado nas instruções do programa. É necessário reservar um espaço proporcional ao tamanho do parâmetro, o que pode ter custos elevados, no caso de vectores por exemplo. S. Melo de Sousa (DIUBI) Geração de Código 57 / 92 passagem por referência ou por endereço Calcula-se o valor esquerdo de uma expressão e. (se e não tem valor esquerdo, então cria-se uma variável que é inicializada com o valor direito de e e utiliza-se o valor esquerdo da variável criada) O procedimento aloca uma variável x que é inicializada pelo valor esquerdo de e. Qualquer referência a x no corpo do procedimento é interpretado como uma operação sobre um objecto cujo endereço está arquivado em x. Este modo de passagem de parâmetro ocupa um espaço memória independente do tamanho do parâmetro (um endereço) Algumas notas: Em C, a passagem por referência é explicitamente programada pela passagem por valor de um apontador (i.e. endereço memória) Em Java, a passagem dos parâmetro é por valor, mas no caso dos objectos, este valor é uma referência (ao dito objecto). em Ada e Pascal, ambas as passagens são possíveis, existam palavras reservadas que permitam indicar que tipo de passagem se pretende (e.g. var, in ou ainda out) S. Melo de Sousa (DIUBI) Geração de Código 58 / 92 Passagem por nome Substituição textual no corpo do procedimento dos parâmetros formais pelos parâmetros efectivos Este mecanismo pode gerar fenómenos (problemáticos) de captura de variáveis Um exemplo: swap (int x; int y) {int z; z = x; x = y; y = z;} Se z e t são variáveis globais do programa então a chamada swap(z,t) com passagem por nome resulta em {int z; z = z; z = t; t = z;} S. Melo de Sousa (DIUBI) Geração de Código 59 / 92 Renomeação Renomear as variáveis locais swap (int x; int y) {int zz; zz = x; x = y; y = zz;} Infelizmente, não é suficiente. Por exemplo a chamada swap(i,a[i]) resulta em {int zz; zz = i; i = a[i]; a[i] = zz;} Método no entanto útil para a compilação de procedimento de tamanho pequeno (o custo da gestão da chamada é importante) S. Melo de Sousa (DIUBI) Geração de Código 60 / 92 Passagem por Copy-Restore Aquando da chamada ao procedimento, o valor direito e serve para inicializar a variável x. À saída do procedimento o valor direito de x serve para actualizar o valor esquerdo de e. Possíveis diferenças de comportamento com a passagem por referência. int a; p(int x) {x=2; a=0;} main () {a=1; p(a); write(a);} Equivalente numa chamada por referência a {a=1; a=2; a=0; write(a);} Equivalente numa chamada por copy restore a {a=1; {int x=a; x=2; a=0; a=x;}; write(a);} S. Melo de Sousa (DIUBI) Geração de Código 61 / 92 Avaliação preguiçosa/ estrita Nas linguagens funcionais, distinguimos as linguagens estritas das linguagens ditas preguiçosas. Linguagem estrita: os valores dos argumentos são calculados antes de serem passados como parâmetros a uma função (CAML, SML por exemplo) Linguagem preguiçosa: a expressão passada em parâmetro para uma função f só será avaliada se f precisa deste valor (chamada por necessidade, em Haskell) avaliação preguiçosa combina dificilmente com efeitos laterais (porque é difícil controlar quando a expressão irá ser avaliada e produzir os efeitos laterais) S. Melo de Sousa (DIUBI) Geração de Código 62 / 92 Avaliação preguiçosa Imaginemos que temos f(x) = t. Pretendemos calcular f(e). Quando a avaliação de t necessitar do valor de x então o cálculo de e será realizado. Mesmo se t requer x várias vezes, o calculo será feito uma so vez. A expressão e é acompanhada do seu ambiente (os valores das variáveis que e utiliza), o que evita problemas de captura. Mecanismo diferente do mecanismo de substituição textual (i.e. macros) S. Melo de Sousa (DIUBI) Geração de Código 63 / 92 Compilação de funções e de procedimentos Declaração f(x) = e Chamada f(a) corresponde a let x = a in e Compilar o código de e fazendo uma assumpção sobre a localização de x Colocar a no local desejado. Semântica de x = a I I I I valor de a código de a localização em memoria de a etc. S. Melo de Sousa (DIUBI) Geração de Código 64 / 92 Compilação de funções e de procedimentos Não se controla nem a quantidade de chamadas a um procedimento (ou função), nem o momento onde estas ocorrem. Não é possível dar estatisticamente os endereços das variáveis que apareçam no corpo do procedimento Estes endereços poderão ser calculados relativamente ao estado da pilha no momento da chamada ao procedimento. O espaço memória alocado para os parâmetros e as variáveis locais é devolvido (free) aquando do fim da execução do procedimento. S. Melo de Sousa (DIUBI) Geração de Código 65 / 92 Organização da memória Numa chamada de um procedimento ou função, a memória envolvida é organizada em tabelas de activação (frame em inglês). A tabela de activação é uma porção da memória que é alocada aquando da chamada de um procedimento devolvida no fim desta. contém todas as informações necessárias a execução do procedimento (parâmetros, variáveis locais) arquiva os dados que deverão ser devolvidas aquando do retorno (fim da execução do procedimento) Para facilitar a comunicação entre código compilado a partir de linguagens fontes distintas (por exemplo uma função C invocar uma função assembly, etc...), é frequente que um determinado formato para as tabelas de activação seja recomendado para uma dada arquitectura. S. Melo de Sousa (DIUBI) Geração de Código 66 / 92 Dados por guardar Aquando duma chamada a procedimento o controlo do código é modificado: Retorno normal do procedimento (sem considerar saltos para processamento de erro ou de instrução de tipo goto): a sequência de execução deve continuar na instrução que segue a chamada. O program counter deve assim ser salvaguardado de cada vez que é processada uma chamada. Os dados locais ao procedimento organizam-se na pilha a partir de um endereço para um bloco de activação (designado em inglês de frame pointer) que é determinado em tempo de execução e guardado num registo particular (o registo fp). Quando um novo procedimento é chamado, este valor muda, pode assim ser necessário arquivar o valor corrente que poderá ser restaurado no final da chamada. S. Melo de Sousa (DIUBI) Geração de Código 67 / 92 Caller vs Callee As operações por efectuar aquando de uma chamada de procedimento são partilhadas entre quem chama o procedimento (o caller) e que é chamado (o callee). É preciso decidir se os parâmetros e os valores de retorno são arquivados em registos ou na pilha O código gerado pelo caller deve estar escrito para cada chamada enquanto o código por escrever no callee so o é uma única vez. O caller realiza se necessário a reserva do valor de retorno (no caso de uma função) e avalia os parâmetros efectivos, coloca-os na pilha ou nos registos pensados para esse efeito. O callee inicializa os seus dados locais e inicia a sua execução No momento do retorno, o callee coloca, se necessário, o resultado da avaliação no lugar reservado pelo caller e restaura os registos. O Caller e o callee devem ter uma visão concertada e coerente da organização da memória S. Melo de Sousa (DIUBI) Geração de Código 68 / 92 Sub-rotinas Reutilizar em diferentes locais a mesma sequência de código I Isola-se esta parte do código e atribuímos-lhe um label No momento da chamada, é preciso arquivar o ponto de retorno num registo dedicado (o registo $ra, a instrução jal). No fim do código da sub-rotina efectua-se um salto para o ponto guardado no registo $ra. Se o corpo duma sub-rotina chama outra sub-rotina, é preciso então cuidar do valor actual do registo $ra e arquivá-lo (preservá-lo para uso futuro). S. Melo de Sousa (DIUBI) Geração de Código 69 / 92 Sub-rotina - Código Junta-se à linguagem alvo definições e chamadas a sub-rotinas D ::= procP; begin I end label-p é um label único (“fresco”) associado ao I ::= call p procedimento p code (proc p; begin I end) = label-p: pushr $ra | code(I) | popr $ra | jr $ra code (call p) = jal label-p S. Melo de Sousa (DIUBI) Geração de Código 70 / 92 procedimentos com parâmetros Se o procedimento tem parâmetros então esta aloca na pilha o espaço para aí guardar as variáveis um registo fp pode então ser posto a apontar para os valores locais, no início da chamada ao procedimento. À saída do procedimento o espaço é devolvido. S. Melo de Sousa (DIUBI) Geração de Código 71 / 92 Convenção para as chamadas (MIPS) os 4 primeiros argumentos podem ser guardados nos registos $a0, $a1, $a2 e $a3. Os restantes ficam na pilha. os registos $v0 e $v1 são utilizados para o retorno da função o registo $ra é utilizado para passar o endereço de retorno da função. Os registos $ti e $si podem ser utilizados para os cálculos temporários. Um número qualquer de chamadas a funções pode estar activo em simultâneo: os valores dos registos devem então estar guardados para utilizações futuras. É sempre necessário guardar $ra (endereço de retorno da função) e $fp (endereço da tabela de activação) caso este seja utilizado. Certos registos ($ti ) são, por convenção, guardados pelo caller (caller-save), outros pelo callee (callee-save). Os registos $si assim como $ra e $fp são salvaguardados pelo callee. S. Melo de Sousa (DIUBI) Geração de Código 72 / 92 Organização da tabela de activação A tabela de activação contém os parâmetros da função (excepto, eventualmente, os 4 primeiros que podem ser colocados nos registos $a0, $a1, $a2 e $a3), as variáveis locais,. Or registos $ra e $fp. A instrução jal label realiza um salto para o código situado no endereço label e preserva (arquiva) o endereço de retorno no registo $ra. A instrução jr $ra permite voltar para a instrução que segue a chamada, após o restauro dos registos necessários. O registo frame pointer ($fp) fica posicionado para um local fixo dentro da tabela. Este permite aceder aos doados via o offset fixo e independentemente do estado da pilha. S. Melo de Sousa (DIUBI) Geração de Código 73 / 92 Tabela de activação Parâmetros da função: e1 . . . en Variáveis locais ou registos por salvaguardar: v1 . . . vm S. Melo de Sousa (DIUBI) Geração de Código 74 / 92 Protocolo de chamadas caller Salvaguarda os registos dos quais tem responsabilidade e de que precisará a seguir à chamada avalia os elementos e1 . . . en nos registos e/ou na pilha. Salta para a instrução correspondente à etiqueta label da função sem esquecer, antes, de arquivar o ponto de retorno dentro de $ra (instrução jal label). Restaura os registos salvaguardados pop dos argumentos previamente empilhados. S. Melo de Sousa (DIUBI) Geração de Código 75 / 92 Protocolo de chamadas callee Reserva o espaço em pilha necessário para o procedimento: os valores de v1 , . . . , vm são utilizados para os registos por salvaguardar ou para variáveis locais. Salvaguardar o valor do registo $fp do caller. Salvaguardar o seu próprio valor de retorno (porque o registo $ra pode ficar alterado por uma chamada interna). Posiciona o registo $fp na tabela de activação. Salvaguardar eventuais registos adicionais do qual o callee é responsável. Executar as instruções do corpo da função/procedimento. Colocar o valor de retorno no registo $v0 ou no local previsto na pilha Restaura o valor de $ra e os outros registos do qual é responsável. Restaura o registo $fp do caller. pop de todo o espaço alocado para a tabela de activação. Salta para a instrução cujo endereço está em $ra com a ajuda da instrução jr. S. Melo de Sousa (DIUBI) Geração de Código 76 / 92 Exemplo Bem definir a tabela de activação; seguir escrupulosamente o protocolo exposto... let rec fact n = if n <= 0 then 1 else n * fact (n - 1) Compilação, de forma informal: O valor de $a0 deve ficar salvaguardado e restituído. S. Melo de Sousa (DIUBI) Geração de Código 77 / 92 Tabela de activação de fact O argumento é passado para o registo $a0 e o valor de retorno em $v0. S. Melo de Sousa (DIUBI) Geração de Código 78 / 92 Código MIPS associado $a0 contém n (salvaguardado). O valor de retorno está em $v0 S. Melo de Sousa (DIUBI) Geração de Código 79 / 92 Sintaxe da linguagem com função À la C: Vs V D ::= ::= ::= | Vs V; | ✏ T id id(Vs) {Vs l} T id (Vs) {Vs I return E; } S. Melo de Sousa (DIUBI) Geração de Código 80 / 92 Organização de uma tabela de activação Para cada função ou procedimento f, podemos calcular estaticamente: nreturn(f) : tamanho do valor de retorno nparams(f) : tamanho dos parâmetros nvars(f): tamanho das variáveis locais Para cada variável x, arquivamos: offset(x): inteiro representando a posição relativa $fp onde é arquivada a variável: I I os parâmetros são endereços maiores do que $fp (offset positivo); as variáveis locais são endereços menores do que $fp (offset negativo). size(x): se as variáveis podem ter um tamanho maior do que 1. Modo de passagem de x se existe também a possibilidade de uma passagem por referência S. Melo de Sousa (DIUBI) Geração de Código 81 / 92 Esquema geral de uma chamada de função (por valor) code(f (e1 , . . . , en )) = code(e_1) [ | pushr $a0] | code(e_2) | [pushr $a0 ou move $a_1,$a_0] ... | code(e_n) | pushr $a0 ... salvaguarda os registos caller-saved | jal f ... restituí os registos caller-saved | addiu $sp,$sp,4*nparams(f) S. Melo de Sousa (DIUBI) Geração de Código 82 / 92 Declaração de uma função code(T f (T1 x1 ; . . . Tn xn ){U1 z1 ; . . . Up zp I return E}) = pushr $fp | pushr $ra | move $fp,$sp | addiu $sp,$sp,-4 * nvars(f) | salvaguarda os registos callee-saved code(I) | code(E) | move $v 0,$a0 | restaura os registos callee-saved addiu $sp,$sp,4 * nvars(f) | popr $ra | popr $fp | jr $ra S. Melo de Sousa (DIUBI) Geração de Código 83 / 92 Passagem por referência No caso onde uma variável é passada por referência, é o seu endereço que é arquivado na tabela de activação (ou nos registos). As funções de acesso e de actualização deverão tratar sempre da indirecção subjacente S. Melo de Sousa (DIUBI) Geração de Código 84 / 92 Exemplo f (ref int x; int y;) {y:=x+y; x:=x*y; } int u=3; main(){f(u,u);print(u)} Organização da memória: Resultado (se tiver) Função f: a variável x pode ser passada para o registo $a0 e y para o registo $a1. Os registos $fp e $ra não serão apagados Função main: I O registo $ra deve ser salvaguardado (chamada de f). S. Melo de Sousa (DIUBI) Geração de Código 85 / 92 Exemplo MIPS f (ref int x; int y;) {y:=x+y; x:=x*y; } main(){f(u,u);print(u)} .data u: .word 3 .text f: lw $a2,0($a0) add $a1,$a2,$a1 mul $a2,$a2,$a1 sw $a2, 0($a0) jr $ra S. Melo de Sousa (DIUBI) main: add $sp,$sp,-4 sw $ra,0($sp) la $a0,u lw $a1,u jal f lw $a0,u li $v0,1 syscall lw $ra,0($sp) add $sp,$sp,4 jr $ra Geração de Código 86 / 92 Cálculo do valor esquerdo de uma expressão O valor esquerdo: endereço onde é arquivada a expressão. Só algumas expressões podem ser valores esquerdos: aqui, variáveis e vectores. codeg($r ,e) coloca o valor esquerdo de e no registo $r. codeg($r , x) = la $r , adr(x) if not islocal(x) codeg($r , x) = add $r , $fp, offset(x) if islocal(x) codeg($r , x[E]) = code(E) | pushr $a0 | codeg($r , x) | popr $a0 | add $r , $r , $a0 Se uma variável deve ser passada por referência, é necessário alocar na pilha e não em registo S. Melo de Sousa (DIUBI) Geração de Código 87 / 92 Código para as expressões code(x) = lw $a0, code(x) = lw $a0, code(x) = lw $a0, | lw $a0, adr(x) decal(x)($fp) decal(x)($fp) 0($a0) se not islocal(x) se islocal(x), por valor se islocal(x), por referência Código para o caller f(e1 , . . . , en ) Substituir code(ei ) por codeg(ei ) se o i-ésimo argumento é passado por referência. S. Melo de Sousa (DIUBI) Geração de Código 88 / 92 Funções Recursivas Cada chamada de função cria novas variáveis No caso das funções (mutuamente) recursivas, I I I Os registos são insuficientes para arquivar as variáveis A tabela de activação deve ser alocado na pilha varias tabelas de activação da mesma função co-existem em simultâneo O número de tabelas de activação na pilha depende dos valores dos parâmetros e logo não é conhecido em tempo de compilação. A recursão arquiva implicitamente valores intermédios e pode simplificar a programação (por exemplo backtracking). A recursão, dita terminal (tail recursive), é um caso particular que pode ser compilada de forma eficaz. S. Melo de Sousa (DIUBI) Geração de Código 89 / 92 Exemplo let rec hanoi i j k n = if n > 0 then begin hanoi i k j (n-1); Printf.printf "%d->%d \n" i k; hanoi j i k (n-1) end Limites: let rec fact n = if n <= 0 then 1 else n * fact (n-1) # let _ = fact 1000000;; Stack overflow during evaluation (looping recursion?). Versão recursiva terminal let rec factt k n = if n <= 0 then k else factt (n * k) (n - 1) let fact = factt 1 S. Melo de Sousa (DIUBI) Geração de Código 90 / 92 Recursão terminal Supomos que a função f(x,y) faz uma chamada a f(t,u) A chamada é terminal se não há calculo em espera na altura da chamada recursiva: os valores de x e y não serão reutilizados após o cálculo de f(t,u). A tabela de activação e os registos da chamada de f(x,y). É preciso ter um cuidado particular no momento da atribuição (x,y) (t,u), salvaguardar o valor de x se necessário. A chamada recursiva no corpo transforma-se assim num simples salto Uma função recursiva terminal é assim compilada tão eficazmente quanto um ciclo. S. Melo de Sousa (DIUBI) Geração de Código 91 / 92 Exemplo - factt O argumento k está no registo $a0, e n no registo $a1. O valor de retorno está no registo $v0 O registo $ra não precisa de ser salvaguardado internamente (já que não há chamadas internas). fact: blez $a1 out mul $a0,$a0,$a1 addi $a1,$a1,-1 j fact out: move $v0,$a0 jr $ra S. Melo de Sousa (DIUBI) Geração de Código 92 / 92