Compiladores

400 visualizações

Publicada em

Pequeno artigo sobre compiladores

Publicada em: Educação
0 comentários
0 gostaram
Estatísticas
Notas
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

Sem downloads
Visualizações
Visualizações totais
400
No SlideShare
0
A partir de incorporações
0
Número de incorporações
2
Ações
Compartilhamentos
0
Downloads
5
Comentários
0
Gostaram
0
Incorporações 0
Nenhuma incorporação

Nenhuma nota no slide

Compiladores

  1. 1. GERAÇÃO DE CÓDIGO OBJETO Marcelo de Avila Rosa1 Cliceres Mack Dal Bianco2 Resumo: A última fase de um compilador é o gerador de código objeto. Ele recebe como entrada a representação intermediário (RI) do compilador e com as informações relevantes da tabela verdade produz como saída um código objeto semanticamente equivalente à entrada. Este artigo traz alguns conceitos, tentando explicar o funcionamento do Gerador de Código de um compilador. Palavras-chaves: Compiladores; geração de código objeto; máquina objeto. Abstract: The last phase of a compiler's code generator object. It receives as input the intermediate representation (IR) compiler with the relevant information and the truth table produces output a semantically equivalent to the input object code. This article presents some concepts, trying to explain the operation of Code Generator a compiler. Key words: Compilers; generation of object code; machine object. INTRODUÇÃO A fase de geração de código final é a última fase da compilação. A geração de um bom código objeto é difícil devido aos detalhes particulares das máquinas para os quais o código é gerado. Contudo, é uma fase importante, pois uma boa geração de código pode ser, por exemplo, duas vezes mais rápida que um algoritmo de geração de código ineficiente. Nem todas as técnicas de otimização são independentes da arquitetura da máquina-alvo. Otimizações dependentes da máquina necessitam de informações tais como os limites e os recursos especiais da máquina-alvo a fim de produzir um código mais compacto e eficiente. O código produzido pelo compilador deve se aproveitar dos recursos especiais de cada máquina-alvo. Segundo Aho (1995), o código objeto pode ser uma seqüência de instruções absolutas de máquina, uma seqüência de instruções de máquina relocáveis, um programa em linguagem assembly ou um programa em outra linguagem. 1 Acadêmico do Curso de Ciência da Computação URI-FW - Universidade Regional Integrada do Alto Uruguai e das Missões - Campus Frederico Westphalen – RS –lelo.c.inf@gmail.com 2 Mestra em Ciência da Computação – Professora de Compiladoresda URI– adrianec@uri.edu.br
  2. 2. 2 Um gerador de código é composto por três tarefas principais: seleção de instrução, alocação e atribuição de registrador, e escalonamento de instrução. A seleção de instrução compreende a escolha de instruções apropriadas da arquitetura alvo para implementar os comandos da RI. A alocação e a atribuição de registrador decidem que valores devem ser mantidos em registradores e também quais registradores usar. O escalonamento de instrução envolve a decisão sobre a ordem em que a execução das instruções deve escalonar. PRINCIPAIS REQUISITOS IMPOSTOS As exigências tradicionalmente impostas a um gerador de código são severas. O código de saída precisa ser correto e de alta qualidade, significando que o mesmo deve tornar efetivo o uso dos recursos da máquina-alvo. Sobretudo, o próprio gerador de código deve rodar eficientemente. O que se deve ser levado em consideração é impossível, matematicamente, gerar um código perfeito. Na verdade, o desenvolvedor deve se contentar com as técnicas heurísticas que geram um bom código, mas não necessariamente um ótimo código. A escolha de um método heurístico é importante, pois na medida em que um algoritmo de geração de código cuidadosamente projetado pode produzir um código que seja várias vezes mais rápido do que aquele produzido por um algoritmo concebido às pressas. Pode-se definir isto nos seguintes itens a seguir:  O código gerado deve ser correto e de alta qualidade;  O código deve fazer uso efetivo dos recursos da máquina;  O código gerado deve executar eficientemente;  O problema de gerar código ótimo é insolúvel (indecidível) como tantos outros. CONSIDERAÇÕES Quatro aspectos que devem ser levado em consideração no momento em que for fazer um gerador de código.  Forma do código objeto ser gerado: o Com uma linguagem absoluta, relocável ou assembly;  Seleção das instruções de máquina: o A escolha da seqüência apropriada pode resultar num código mais curto e mais rápido;
  3. 3. 3  Alocação de registradores;  Escolha da ordem de avaliação: o A determinação da melhor ordem para execução das instruções é um problema insolúvel; o Alguns computadores requerem menos registradores para resultados intermediários. MÁQUINA OBJETO Deve existir uma familiaridade com a máquina que criará o objeto assim como com o conjunto de instruções. As arquiteturas de Máquinas alvos mais comuns são: RISC (ReducedInstuction Set Computer), CISC (ComplexInstruction Set Computer) e baseadas em Pilha. Três aspectos básicos que devem ser analisados em um projeto que são:  Forma do Código Objeto o Linguagem absoluta:  A geração de um programa em linguagem absoluta de máquina tem a vantagem de que o programa objeto pode ser armazenado numa área de memória fixa e ser imediatamente executada.  Compiladores deste tipo são utilizados em ambientes universitários, onde é altamente conveniente diminuir o custo de compilação.  Os compiladores que geram código absoluto e executam imediatamente são conhecidos como loadandgocompilers. o Linguagem relocável:  Ageração de código em linguagem de máquina relocável permite a compilação de subprogramas.  Módulos e objetos relocáveis podem ser ligados e carregados por um ligador/carregador.  Essa estratégia dá flexibilidade para compilar sub-rotinas separadamente e para chamar outros programas previamente compilados. o Linguagem assembly:  A tradução para linguagem assembly facilita o processo de geração de código.
  4. 4. 4  São geradas instruções simbólicas e podem ser usadas as facilidades de macro instruções.  O preço pago é um passo adicional – tradução para linguagem de máquina.  É uma estratégia razoável, especialmente, para máquinas com pouca memória, nas quais o compilador deve desenvolver-se em vários passos.  Escolha da seqüência da execução das instruções que é necessária, quanto mais acertarem mais tempo economizará e o código será mais compacto.  Escolha da ordem de avaliação. Sabe-se que é necessária a definição de uma ordem para acertar e se aproximar da melhor seqüência, mas como fazer isso? É um grande problema. As vezes é necessário optar por uma técnica de menor número de registradores para alcançar no mínimo pelo menos resultados intermediários. ALOCAÇAO DE REGISTRADORES Os trabalhos que necessitam do uso de registradores são processamentos mais ágeis e menores, que levarão um curto período de tempo para serem executados. Por essa razão o uso de registradores deve cumprir suas alocações com eficiência. Instruções com registradores são mais curtas e mais rápidas do que instruções envolvendo memória. Portanto, o uso eficiente de registradores é muito importante. A atribuição ótima de registradores a variáveis é muito difícil e muito problemática quando a máquina trabalha com registradores aos pares (para instruções de divisão e multiplicação), ou provê registradores específicos para endereçamento e para dados. O problema do uso otimizado de registradores fica simples quando a máquina possui um único registrador para realizar operações aritméticas. O problema deixa de existir quando as operações aritméticas são realizadas sobre uma pilha. Neste caso, deixa de ser necessária, inclusive a utilização de variáveis temporais. Pode-se dizer que o uso dos registradores frequentemente é subdividido em dois subproblemas que são:  Alocação de registradores: etapa na qual seleciona-se o conjunto de variáveis que residirão nos registradores em cada ponto do programa.
  5. 5. 5  Atribuição de registradores: etapa na qual determina-se um registrador específico em que uma variável residirá. GERENCIAMENTO DE MEMÓRIA EM TEMPO DE EXECUÇÃO Para o gerenciamento de memória é necessário conhecer os endereços de código objeto. Os endereços são determinados de acordo com o tipo de alocação de memória utilizada. Em um compilador é utilizado dois tipos de alocação. A alocação estática e a alocação em pilhas. Mas também tem os endereços em tempo de execução, conhecido como dinâmico.  Alocação Estática: O tamanho e o layout dos registros de ativação são determinados pelo gerador de código a partir de informações sobre os nomes, armazenados na tabela de símbolos. Para implementar o caso mais simples é necessário uma máquina alvo que execute as duas instruções. Conforme a figura. OST salva o endereço de retorno no registrador de ativação e o BR transfere o controle para o código objeto do procedimento chamado callee. O calle.static.Area é uma constante que fornece o endereço de início do registro de ativação para callee. A variável callee.codeArea é uma constante que refere ao endereço da primeira instrução do procedimento chamado callee. O #here+20 faz o ponteiro de instrução corrente a percorrer vinte bytes, ou seja, cinco palavras a frente da instruçãoST. O que caracteriza as alocações estáticas é o conhecimento do seu endereço no momento em que se constrói o código objeto.  Alocação de Pilha: Quando se utiliza endereços relativos para armazenamento nos registros de ativação de uma alocação estática, esta na verdade transformando essa alocação em um outro tipo, em uma alocação dinâmica. No entanto a posição de um registro só é conhecida quando é executado o código objeto. Essa posição é armazenada em um registrador que forma a palavra pode ser acessada como deslocamento a partir desse valor nesse registrador. O modo de endereçamento indexado da máquina alvo é conveniente para essa finalidade.
  6. 6. 6 O deslocamento pode ser positivo ou negativo, dependendo para onde o ponteiro de deslocamento se mova. Neste exemplo considera-se um deslocamento positivo com o SP apontando para o início do registro de ativação que está no topo da pilha.  Alocação em Tempo execução para os nomes (Dinâmica): A abordagem de endereçamento em tempo de execução torna o compilador mais portável, pois o front-end não precisa ser substituído nem mesmo quando o compilador for transferido para uma máquina diferente, onde é necessária uma otimização em tempo de execução. Para fazer o acesso, o nome é substituído pelo código para acessar os endereços de memória. BLOCOS BÁSICOS E GRAFOS DE FLUXO Nos algoritmos de três endereços, uma solução muito útil para a sua compreensão é a representação de três endereços chamados grafo de fluxo. Os nós deste grafo representam computações e os lados representam o fluxo de controle. Blocos Básicos: São trechos de programa cujas instruções são sempre executadas em ordem (em linha reta), da primeira até a última. É uma região de código seqüencial sem qualquer salto de execução, onde o controle entra no início e o deixa no fim. Arestas direcionadas são usadas para representar tais saltos na estrutura de controle. Ainda há dois blocos especiais, o bloco de entrada e o bloco de saída, de onde se começa e termina o fluxo respectivamente. Um bloco básico computa um conjunto de expressões que são os valores dos nomes vivos à saída de um bloco. Dois blocos básicos são equivalentes, se computarem o mesmo conjunto de expressão. Várias transformações podem ser aplicadas a um bloco básico sem que o conjunto de expressões computadas seja alterado. Muitas dessas transformações ajudam na melhora da qualidade do novo código que será gerado a partir do bloco básico. Transformações Primárias: Primeiramente é feito a eliminação de sub-expressões comuns. Para isto é usado a Alocação estática. Suponha-se o seguinte bloco básico
  7. 7. 7 Como o segundo e o quarto enunciados computam a mesmaexpressão, esse bloco base pode ser transformado no bloco equivalente. Eliminação de código morto. É o código no programa que nunca será executado com nenhum tipo de dados ou em outras condições. Supondo que um programa continha código morto (deadcode), isto é, código que não pode ser alcançado durante a execução de um programa, este programa pode ser otimizado pela remoção deste código. Renomeação de variáveis temporárias. As variáveis temporárias durante a geração de código intermediário podem não ser estritamente necessário. Normalmente esta eliminação é feita dando nomes para as variáveis (temporariamente ou não) que vão guardar os valores temporários. Por exemplo: se tiver no código fonte x:=a+b; o código intermediário terá t1 = a +b; x = t1; ea variável t1 pode ser eliminada. Assim, podemos sempre transformar um bloco básico num bloco equivalente, onde cada enunciado que define um temporário passa a definir um equivalente. Intercâmbio de enunciados. Supondo ter um bloco com dois enunciados adjacentes t1:= b + c e t2:= x + y, pode-se então intercambiar os enunciados sem alterar o valor do bloco somente se nem x bem y forem t1, nem b nem c forem t2. Um bloco básico na forma normal permite qualquer que seja a troca de enunciados possíveis. Transformações Algébricas. Pode-se aplicar transformações baseadas também em propriedades algébricas, como comutatividade, associatividade, identidade, entre outros casos. Por exemplo: X:=a+b*c, como a soma é comutativa, pode-se transformar em X:=b*c+a. Grafos de Fluxo. É uma representação que usa notação de grafo para descrever todos os caminhos que podem ser executados por um programa de computador, onde cada nó representa um bloco básico. Através do grafo de fluxo é possível adicionar informações a respeito do fluxo de controle de blocos básicos. Eliminação de código inalcançável.
  8. 8. 8 Elimina também o problema do desvio sobre desvio, mas, neste caso o desvio sobre desvio leva o código a não entrar em uma ou mais linhas do programa. GERAÇÃO DO CÓDIGO SIMPLES É importante considerar que a maioria dos geradores deve evitar a geração de instruções de cargas e armazenamentos desnecessários. Os registradores devem:  Colocar nos registradores todos os operandos para realizar uma operação;  Tem-se que guardar valores globais calculados em um bloco básico que é usado em outros blocos;  Serem usados para auxiliar o gerenciamento de memória em tempo de execução;  Quando uma expressão grande está sendo avaliada, guardar resultados de sub- expressões;  Tais necessidades são concorrentes, justamente pelo fato de ter um número limitado de registradores. GERAÇÃO DO CÓDIGO OBJETO A abordagem mais simples da etapa de geração de código objeto é: Para cada instrução (do código intermediário) ter um gabarito com a correspondente seqüência de instruções em linguagem simbólica do processador-alvo. Exemplo: le := ld1 + ld2 A seqüência de instruções em linguagem simbólica que corresponde a essa instrução depende da arquitetura do processador para o qual o programa é gerado. PRODUÇÃO DE CÓDIGO EXECUTÁVEL O resultado da compilação é um arquivo em linguagem simbólica. Montagem
  9. 9. 9  Processo em que o programa em linguagem simbólica é transformado em formato binário, em código de máquina;  O programa responsável por essa transformação é o montador. Montadores  Traduzem código em linguagem simbólica para linguagem de máquina. CONCLUSÃO O gerador de código recebe como entrada uma representação intermediária do programa fonte e o mapeia em uma linguagem objeto. Se a linguagem objeto for código de máquina de alguma arquitetura, devem-se selecionar os registradores ou localizações de memória para cada uma das variáveis usadas pelo programa. Depois, os códigos intermediários são traduzidos em sequências de instruções de máquina que realizam a mesma tarefa. Um aspecto crítico da geração de código está relacionado à cuidadosa atribuição dos registradores às variáveis do programa REFERÊNCIAS BIBLIOGRÁFICAS AHO, A. V.; ULLMAN, J. D. Compiadores: Princípios, Técnicas e Ferramentas. Guanabarra, Koogan, 1995. Gerador de Código Objeto Disponível em <http://www.ybadoo.com.br/ead/cmp/09/CMP_slides.pdf>, acesso realizado em 02/06/2014.

×