compiladores

9
GERAÇÃO DE CÓDIGO OBJETO Marcelo de Avila Rosa 1 Cliceres Mack Dal Bianco 2 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 [email protected] 2 Mestra em Ciência da Computação Professora de Compiladoresda URI[email protected]

Upload: marcelo-rosa

Post on 20-Jul-2015

62 views

Category:

Education


3 download

TRANSCRIPT

Page 1: Compiladores

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 –[email protected] 2Mestra em Ciência da Computação – Professora de Compiladoresda URI– [email protected]

Page 2: Compiladores

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;

Page 3: Compiladores

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.

Page 4: Compiladores

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.

Page 5: Compiladores

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.

Page 6: Compiladores

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

Page 7: Compiladores

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.

Page 8: Compiladores

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

Page 9: Compiladores

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.