Buscar

COMPILADORES (59)

Prévia do material em texto

Construindo Compiladores Utilizando o Microsoft Phoenix
Framework
Guilherme Amaral Avelino
1
1 Phoenix Framework
Phoenix é uma plataforma desenvolvida para a construção de compiladores e de ferramentas de
análise otimização de código. Desenvolvida pela Microsoft, utilizando C++/CLI, ele consegue obter
o melhor de dois mundos: desempenho, proporcionado pela linguagem C++ e extensibilidade, devido
a integração com o ambiente .NET.
Os compiladores atuais funcionam como caixas pretas, onde todo o processo interno é escondido
do usuário e não é permitido fazer alterações no seu funcionamento. Tudo que o usuário pode fazer é
fornecer o código fonte como entrada, passar alguma diretivas de compilação e aguardar o programa
compilado. Phoenix objetiva abrir esta caixa. Um compilador escrito utilizando Phoenix é formado
por uma lista de fases, sendo cada fase responsável por uma etapa do processo de compilação. Através
do mecanismo de plugins, Phoenix permite que alteremos o comportamento do compilador acrescen-
tando, retirando ou alterando fases. A existência de uma representação intermediária própria, bem
como uma rica API para manipulação desta, facilitam a manipulação do compilador e a construção
de ferramentas de análise e otimização.
Phoenix proporciona um rico ecossistema para três públicos diferentes:
• Acadêmicos: permite uma melhor compreensão do funcionamento de um compilador e das
técnicas empregadas em sua construção.
• Pesquisadores: fornece um ambiente maleável, extensível e redirecionável que permite fácil
incorporação e testes de novas características a serem pesquisadas, bem com a incorporação de
novas técnicas.
• Desenvolvedores: facilita a criação de ferramentas customizadas para análise do código e per-
mite a manipulação de programas já compilados.
Além de todas estas características, Phoenix é a base para a construção dos novos compiladores
(JITs, PreJITs e C++ Backend) e ferramentas da Microsoft.
1.1 Exemplos de Utilização
Phoenix é uma ferramenta bastante poderosa, dentre outras coisas podemos utilizá-la para
• Testar novas técnicas de compilação. Um pesquisador que tenha desenvolvido uma nova técnica
de compilação, por exemplo um novo algoritmo de otimização de código, necessita incorporá-
la em um compilador para poder validar sua funcionalidade. Antes do Phoenix você teria duas
2
alternativas, uma seria criar seu próprio compilador, o que além de trabalhoso provavelmente
resultaria num compilador não muito usual e a outra seria tentar incorporar sua técnica em
algum compilador de código aberto existente.
Com o Phoenix, o código da nova técnica deveria ser escrito em uma nova fase e incorporado a
um plugin o qual seria responsável por integrá-lo ao compilador, inserindo o na lista de fases do
compilador. Vale ressaltar que o SDK do Phoenix vem com um compilador C/C++ completo,
o qual poderia ser utilizado, sem que necessitássemos alterar uma linha sequer de seu código.
• Criação de Add-ons para o Visual Studio. Podemos usar o Phoenix para obter informações do
código do programa e, através destas, extender as funcionalidades do Visual Studio. O RDK
do Phoenix provê dois exemplos de tal funcionalidade, um gráfico de fluxo de controle e uma
ferramenta de de slicing dinâmica.
• Instrumentação de código. Phoenix permite a alteração de código após sua compilação. Através
de um Reader é feita a transformação de um arquivo executável na representação intermediária
provida por Phoenix. Utilizando a API Phoenix, podemos manipular esta representação e após
o término da manipulação utilizamos um Writer, que fará a conversão da representação inter-
mediária gerando, novamente, o programa. Esta técnica é largamente empregada para adição de
novas características em programas já compilados, para análise dinâmica de programas e para
prover técnicas de programação orientada a aspectos.
• Criação de compiladores. Principal foco do Phoenix, a construção de compiladores é facilitada
devido ao suporte provida por sua API tanto para a manipulação da representação intermediária,
como da geração de código final para diversas arquiteturas.
Estes exemplos demonstram a larga utilização possível deste framework. Entretanto, muito mais
pode ser feito utilizando sua API, readers e writers.
2 Inicializando a Infra-estrutura de um Compilador
Nesta seção serão apresentados classes e métodos fornecidos pelo framework Phoenix que auxil-
iam na implementação de um compilador.
2.1 Inicializando o Ambiente Phoenix
A primeira tarefa a ser executada por nosso compilador é inicializar o ambiente Phoenix e setar
arquitetura e ambiente de execução alvos da compilação.
3
Para trabalharmos com binários x86 devemos setar a arquitetura e o ambiente de execução da
seguinte forma:
1 Phx.Targets.Architectures.Architecture arch =
2 Phx.Targets.Architectures.X86.Architecture.New();
3 Phx.Targets.Runtimes.Runtime runtime =
4 Phx.Targets.Runtimes.Vccrt.Win32.X86.Runtime.New(arch);
5 Phx.GlobalData.RegisterTargetArchitecture(arch);
6 Phx.GlobalData.RegisterTargetRuntime(runtime);
Código 1: Setando arquitetura e ambiente de execução x86
Os dois primeiros comandos criam, respectivamente, a arquitetura e o ambiente de execução, os
quais, são posteriormente adicionados a infraestrutura de dados do Phoenix.
Seguindo o mesmo modelo, para trabalhar com binários MSIL devemos inserir o seguinte código:
1 Phx.Targets.Architectures.Architecture msilArch =
2 Phx.Targets.Architectures.Msil.Architecture.New();
3 Phx.Targets.Runtimes.Runtime winMSILRuntime =
4 Phx.Targets.Runtimes.Vccrt.Win.Msil.Runtime.New(msilArch);
5 Phx.GlobalData.RegisterTargetArchitecture(msilArch);
6 Phx.GlobalData.RegisterTargetRuntime(winMSILRuntime)
Código 2: Setando arquitetura e ambiente de execução MSIL
Após setarmos arquitetura e ambiente de execução alvos de nosso compilador devemos inicializar
a infra-estrutura do Phoenix. O método Phx.Initialize.BeginInitialization sinaliza o começo do
processo de inicialização da infra-estrutura. Após este método devemos adicionar os Controls que
alteram o funcionamento do nosso compilador e finalizar a inicialização da infra-estrutura através da
chamada ao método Phx.Initialize.EndInitialization. Esta inicialização de infra-estrutura é respon-
sável por inicializar aspectos chaves do Phoenix, tais como: gerenciamento de memória, criação da
unidade global e de sua tabela de símbolos, etc...
O método Phx.Initialize.EndInitialization recebe dois parâmetros: uma string (parserString)
contendo a forma de como deve ser feita o parse e um array de string (argv) contendo a linha de
comandos. O parseString especifica a ordem de parse dos elementos, consiste de um ou mais ítens
de inicialização separados por ". Em todos os exemplos disponibilizados no Phoenix RDK 2007 o
argumento parseString é preenchido com "PHX|*|_PHX_|", o que significa que primeiro será feito
4
o parse no ambiente de variáveis PHX, depois na string de linha de comandos e então no ambiente
_PHX_.
2.2 Phases
O processo de compilação de um programa pode ser dividido em diversas fases (Phases), onde
cada fase executa uma atividade diferente sobre o programa a ser compilado. Desta forma, uma fase
pode, por exemplo, ser responsável por alocar registros enquanto outra pode gerar funções inline. É
através de uma lista de fases que são executados todas as etapas da compilação de um programa no
ambiente Phoenix.
2.2.1 Gerando uma Lista de Fases
Após a inicialização do Phoenix, devemos construir a lista de fases que compõem o compilador. Uma
lista de fases permite que as fases sejam executadas seqüencialmente seguindo a ordem que as fases
foram inseridas. Uma vez que uma lista de fases herda da classe Phase, elementos de uma lista
de fases podem conter outraslistas de fases aninhadas, devendo todos as fases da lista aninhada ser
executadas antes que se execute o próximo elemento da lista.
A lista de fases deve ser inserida em um objeto Phx.Phases.PhaseConfiguration, este objeto
é um envoltório que guarda a lista de fases e possui métodos que permitem a execução da lista.
Cada PhaseConfiguration pode ter associado a ele dois eventos: Phx.Phases.PrePhaseEvent e
Phx.Phases.PostPhaseEvent que são levantados, respectivamente, antes e após a execução das fases.
Os comandos necessários para criar uma lista de fases e inserir uma fase pode ser observados no
código 3.
1 Phx.Phases.PhaseConfiguration config =
2 Phx.Phases.PhaseConfiguration.New(Phx.GlobalData.GlobalLifetime,
3 "Hello Phases");
4 Phx.Phases.PhaseList phaseList = config.PhaseList;
5 phaseList.AppendPhase(Phx.Types.TypeCheckPhase.New(config));
Código 3: Criação de uma lista de fases
Neste exemplo primeiro criamos objeto PhaseConfiguration (linhas 1 a 3) através do método New,
criamos em seguida um PhaseList que passa a apontar para a lista contida no objeto config. Através
do método AppendPhase adicionamos uma nova fase. A fase adicionada é uma fase já existente no
5
Phoenix, responsável por fazer a checagem de tipos. Podemos adicionar nossa própria fase, o que
será necessário para a construção de um compilador. A próxima seção explica como criar uma fase.
O método PhaseList.DoPhaseList é o responsável por executar a lista de fases. Este método
recebe como parâmetro um objeto Phx.Unit sobre o qual todas as fases serão executadas.
2.2.2 Criando Uma Fase
Para criar uma fase, devemos construir uma classe que herde da classe abstrata Phx.Phase e imple-
mente os métodosNew eExecute. Além destes dois métodos existem outros métodos que são virtuais
e, desta forma, podem ser sobrescritos, sendo suas implementações opcionais para a construção de
uma fase. No método New deve ser criada uma instância da nova fase, já o método Execute é o
responsável por realizar as operações da fase, devendo nele conter todo o código da funcionalidade
da fase.
O método Execute recebe como argumento um objeto do tipo Phx.Unit. Embora, uma fase possa
operar sobre qualquer membro da hierarquia Unit a maioria das fases opera sobre FunctionUnits.
É comum termos Controls associados a fases. Controls são objetos que encapsulam opções de
linha de comando. Desta forma é comum utilizar Controls para habilitar e desabilitar a execução de
fases. O controle da execução ou não de um fase é feita através do método ShouldExecute, o qual,
é responsável por verificar o estado do Control correspondente, caso este tenha sido explicitamente
especificado em linha de comando. Caso contrário, SouldExecute irá verificar a propriedade IsOn-
ByDefault para tomar sua decisão. Tal propriedade tem como valor default true, mas seu valor pode
ser setado como falso no método New.
Seguindo este modelo vamos criar uma fase, TestPhase. Esta fase atuará imprimindo o nome de
todas as unidades que forem submetidas a lista de fases. O código 4 mostra como fazemos isto.
No método New criamos uma nova instância do objeto TestPhase e inicializamos a fase através do
método Initialize (linha 7), o qual recebe como parâmetro um objeto PhaseConfiguration e uma
string descrevendo o nome da fase adicionada. É através desta string que é feita a busca por fases
dentro de uma lista de fases. O método Execute contém o código funcional da fase criada.
1 class TestPhase : Phx.Phases.Phase {
2
3 public static TestPhase New (Phx.Phases.PhaseConfiguration config)
4 {
5 TestPhase phase = new TestPhase;
6
6
7 phase.Initialize(config, "Lista Nomes das Unidades");
8
9 // Um controle poderia ser adicionado aqui para controlar a fase
10
11 return phase;
12 }
13
14 override void Execute (Phx.Unit unit)
15 {
16 Phx.Output.WriteLine("TestPhase {0}", unit->ToString());
17 }
18 }
Código 4: Criando uma fase
Esta fase pode ser adicionada a nossa lista de fases, criada no código 3, através do comando
AppendPhase.
Duas fases essenciais de um compilador são EncodePhase e EmitPhase, responsáveis, respectiva-
mente, por gerar a representação intermediária e por emitir o nosso programa binário no final. Estas
duas fases são específicas para cada código gerado devendo ser criadas e adicionadas ao final da lista
de fases do compilador.
2.3 Plugins
Um artifício bastante poderoso provido por Phoenix é o mecanismo de plugins. Plugins são mó-
dulos externos criados através de código gerenciado e armazenado em um arquivo dll que pode ser
adicionado a programas construídos utilizando o Phoenix. Através deste mecanismo podemos mod-
ificar a lista de fases que compõe um programa Phoenix substituindo, alterando ou inserindo fases.
Esta funcionalidade permite a modificação destes programas após sua compilação sem que para isto
precisemos alterar seu código fonte.
Na figura 1 podemos observar a utilização do pluginMyPlugin.dll. Este plugin atuará modificando
o comportamento do compilador cl (compilador para código C/C++ construído utilizando o Phoenix).
O compilador cl é dividido em dois módulos, um frontend (C1.exe) e o backend (C2.exe). O C2 é
responsável pela geração de código final e foi construído utilizando o framework Phoenix. O plugin
7
irá modificar a lista de fases que compõem o backend c2 alterando assim seu funcionamento, o que
poderá ser refletido no programa gerado pelo compilador (App.exe).
Figura 1: Utilizando um plugin para alterar o comportamento do compilador cl
2.3.1 Criando um Plugin
Para criarmos um plugin devemos criar uma nova classe que estenda da interface Phx.Plugin e im-
plementarmos seus métodos RegisterObjects e BuildPhases. O primeiro método é responsável por
criar e registrar possíveis controles (Phx.Controls) que atuem sobre o plugin, já o segundo é respon-
sável por percorrer a lista de fases do programa alvo e inserir a fase encapsulada pelo plugin no local
escolhido.
No código 5 construímos um plugin que insere a fase TestPhase, construída anteriormente, na
lista de fases de um programa, antes da fase Encoding. Podemos observar que o plugin não tem seu
comportamento influenciado por nenhum controle e por isto o método RegisterObjects está vazio.
Já o método BuildPhases faz a busca na lista de fases do programa e insere a fase TestPhase antes da
fase escolhida.
1 public class PlugIn : Phx.Plugin
2 {
3 public override void RegisterObjects() { }
4
5 public override void BuildPhases
6 (Phx.Phases.PhaseConfiguration config)
7 {
8 Phx.Phases.Phase encodingPhase;
9 Phx.Phases.Phase testPhase = TestPhase.New(config);
10 encodingPhase = config.PhaseList.FindByName("Encoding");
8
11 encodingPhase.InsertBefore(testPhase);
12 }
13 }
Código 5: Criando um plugin
Para permitir que plugins alterem as fases que compõe nosso programa devemos habilitar tal opção.
Para isto após criarmos o objeto PhaseConfiguration, que encapsula a lista de fases do programa,
devemos chamar o método Phx.GlobalData.BuildPlugInPhases passando ele como argumento.
A instalação do RDK do Phoenix se integra automaticamente ao Visual Studio e traz consigo
alguns assistentes de criação. Dentre estes assistente existe um para a criação de plugins. Este recurso
é bastante útil pois o VS faz todo o trabalho repetitivo, como criação de fase inserção no local indicado
da lista de fases, permitindo que o programador se preocupe exclusivamente com a funcionalidade do
plugin.
3 Principais Elementos de um Compilador
Esta seção se dedica a demonstrar como utilizar o Phoenix para criar as estruturas necessárias
para que um compilador possa gerar um programa. Serão abordados nesta seção a representação
intermediária (IR), sistema de símbolos, sistema de tipos,lifetimes e hierarquia de unidades. Seções
subseqüentes demonstrarão como utilizar estas estruturas para representar componentes específicos
de um programa tais como: funções, variáveis, chamadas de funções, desvios condicionais, etc...
3.1 Representação Intermediária Phoenix (IR)
Phoenix utiliza uma representação intermediária (IR) fortemente tipada e linear para representar
o fluxo de instruções de uma função como uma série de controles de fluxo de dados. A IR permite
que uma função seja representada em diversos níveis de abstração, podendo representar uma função
desde uma forma independente de máquina, alto nível(HIR), ou mesmo, uma forma dependente da
máquina alvo, baixo nível(LIR). Existem quatro níveis de representação provido por Phoenix, são eles
HIR, MIR, LIR e EIR.
Phoenix armazena a IR de uma função como uma lista de elementos duplamente ligados. Ela é
duplamente ligada porque cada instrução possui um operador que é ligado a duas listas de operandos:
uma contendo os operadores de origem e a outra com os de destino. Esta representação mostra de
forma explícita todos os efeitos coletarais possíveis de uma instrução, uma vez que, todos os recursos
9
lidos aparecem na lista de origem e todos recursos potencialmente alterados estão especificados na
lista de destino. A representação IR de uma instrução pode ser observada na figura 2.
Figura 2: HIR de um comando X = ADD X, *P
3.1.1 Tipos e Campos
Phoenix armazena tipos em operandos que referenciam dados. O tipo de de uma instrução IR é deter-
minada pelo operador e pelos tipos dos operandos de origem. Uma vez que, a IR é fortemente tipada
o framework executa uma checagem de tipos dos operandos para garantir que eles estão corretos.
Os tipos armazenados em um operando indica o tipo atual que está sendo referenciado. Em alguns
operandos, tais como VariableOperand, existe um símbolo opcional de referência. Este símbolo
pode também ter um símbolo, mas não necessariamente o mesmo do operando (casting em C/C++).
Para garantir que operandos de tipos e campos reflitam informações precisas, a IR permite que oper-
ações de casting sejam armazenadas dentro deste operandos.
Assim como tipos, referências a campos (fields) podem ser guardadas dentro de um operando. Um
campo consiste de um tipo, um offset, e um enclosing type (tipo ao qual o campo está confinado).
Campos permitem acesso a dados dentro de tipos agregados aggregate types tais como estruturas,
uniões, ou classes.
3.1.2 Operandos
Phoenix representa cada operando como um nó folha do gráfico da IR. Operandos aparecem tanto na
lista de origem quanto na de destino. Uma vez que, que todos efeitos das instruções são representa-
dos explicitamente, operandos refletem todos os potenciais recursos usados. O que inclui, registros,
alocações de memória e códigos condicionais.
A IR armazena cada única instrução de referência como uma cópia única desse operando. Desta
maneira, operandos podem ser conectados dentro de gráficos de fluxo de dados tais como Static Single
Assignment (SSA), use-definition (use-def) ou definition-use (def-use) chains, e árvores de expressões.
10
O framework mantém uma cópia única e constante, ou seja, cada surgimento de um operando é único.
A tabela 1 mostra os operandos existentes na IR.
Operandos Descrição
VariableOperand Uma variável de usuário ou um operando temporária do compilador ar-
mazenada em um registro ou na memória. VariableOperands pode
especificar se uma instrução usa o endereço ou o conteúdo de uma var-
iável.
MemoryOperand Operando utilizado pela IR para referenciar recursos na memória, tais
como o conteúdo de um endereço de memória ou o próprio endereço.
ImmediateOperand Operando utilizado para referenciar constantes tais como constantes de
inteiros, float, string e simbólicas, tais como FRAMESIZE.
AliasOperand Descreve efeitos colaterais de instruções may-use ou may-define (may-
def). AliasOperand frequentemente especifica informações sobre con-
juntos de registros mortos ou variáveis reference-definition (ref-def)
para chamada de funções.
LabelOperand Representam labels gerados pelo usuário ou pela ferramenta em uma
função. É utilizado por Phoenix para descrever fluxos de controle.
FunctionOperand Representa uma referência a um símbolo de uma função ou uma
chamada direta a uma função.
Tabela 1: Unidades da Representação Intermediária de Phoenix
3.1.3 Instruções
Uma vez que já definimos como Phoenix representam os operandos trataremos, agora, da represen-
tação IR das instruções. Cada instrução na IR expressa um fluxo de dados, fluxo de controle ou uma
operação. Cada instrução contém um opcode, que específica a operação, um lista de operandos fonte
e outra de operandos destinos. Tais instruções são classificadas como sendo pseudo ou real.
Instruções Reais Representam operações que modificam dados e o fluxo de controle do programa.
As instruções reais podem ser classificadas nos seguintes tipos:
• Simple: operação aritmética ou lógica que produz um valor.
11
• Complex: uma chamada direta ou indireta a um outro procedimento.
• Control flow: operações de fluxo de controle do programa tais como desvios condicionais e
incondicionais.
• Summary: instruções para retirada do fluxo principal de instruções, tal como um bloco de
assembly inline.
Estas instruções são mapeadas para uma ou mais instruções de máquinas. A tabela 2 descreve a
instruções reais da IR.
Operandos Descrição
ValueInstruction Uma operação aritmética ou lógica que produz um valor.
CallInstruction Um procedimento de invocação, direto ou indiretamente.
CompareInstruction Uma instrução de comparação que gera um código condicional
BranchInstruction Um controle de fluxo para desvios condicionais e incondicionais, bem
como para diretos e indiretos
SwitchInstruction Um controle de fluxo para um switch, um desvio com multiplas alter-
nativas.
OutlineInstruction Uma instrução para retirada do fluxo principal de instruções, tal como
um bloco de assembly inline.
Tabela 2: Instruções Phoenix
3.1.4 Pseudo Instruções
Representam instruções que não alteram diretamente o comportamento do programa. São dividadas
em:
• label criam labels definidos pelo usuário. Determina pontos do código úteis para a construção
de programas de análise.
• Pragma representam diretivas e dados fornecidos pelo usuário.
• Data criam dados estaticamente alocados. Podem armazenar qualquer seqüencia de bytes de
baixo nível.
12
3.1.5 API em Dois Níveis
Para facilitar a leitura das propriedades dos operandos e instruções de uma lista de instruções, Phoenix
disponibiliza todas as propriedades das classes que derivam deOperand e Instruction ainda em suas
classes base. Com esta implementação evitamos ter de fazer o casting dos elementos da lista para
poder obter as propriedades específicas das subclasses. Por outro lado, tal abordagem tem como
custo adicional overheads para fazer chamada aos métodos virtuais das propriedades. Se este custo
for relevante para a tarefa em questão deve-se fazer o casting dos objetos. Para tal tarefa, Phoenix
disponibiliza os métodos Instruction.As... e Instruction.As... responsável por fazer o casting para a
subclasse indicada.
3.2 Hierarquia de Unidades
Para representar cada uma das estruturas que compõem um programa, Phoenix possui classes
especiais que derivam da subclasse Phx.Unit. Estas unidades podem ser aninhadas formando uma
estrutura hierárquica, onde o a unidade mais externa é GlobalUnit. A tabela 3 lista cada uma das
unidades presentes na Representação Intermediária de Phoenix. Além de outras unidades, estas
unidades armazenam fluxos de instruções e variáveis inicializadas.
O método New, responsável pela instanciação de um objeto, de cada Unit tem sua própria assi-
natura.Sendo que todos recebem um objeto Lifetime correspondente. Para mais informações sobre
os parâmetros do construtor de cada unidade consulte o manual do Phoenix[?].
3.3 Lifetimes
Lifetime é um dos componentes centrais de Phoenix, ele é muito útil na criação de tempo de
vida de variáveis e funções. Estas estruturas representam uma forma que Phoenix encontrou de ele
mesmo manipular a memória, alocando memória quando cria um Lifetime e liberando quando este
objeto é deletado. Alocação de memória em compiladores possui como característica esta diferença
de tempo de vidas, onde algumas estruturas permanece durante todo o processo de compilação, en-
quanto outras duram apenas um pedaço de tempo. Para armazenar cada um das diferentes unidades
e estruturas utilizadas por um compilador temos diferentes tipos de Lifetimes. Assim, por exem-
plo, temos GlobalLifetime para representar alocações de memória que duram todo o processo de
compilação e TmpLifetime que representam alocações temporárias de memória.
A criação de um Lifetime é feita através do método Phx.Lifetime.New. Este método recebe dois
parâmetros, o primeiro é do tipo Phx.LifetimeKind que representa o tipo de Lifetime e o segundo é
do tipo Phx.Unit que será a unidade ao qual o Lifetime estará associado.
13
3.4 Sistema de Símbolos
Símbolos Phoenix são associados com entidades tais como variáveis, labels, tipos, nomes de
funções, endereços, entidades de metadados e módulos importados e exportados. Símbolos provém
uma representação estrutural dos relacionamentos entre as entidades de um programa, tais como o
relacionamento entre uma função e seus argumentos e variáveis locais. Phoenix utiliza os símbolos
para referenciar tais entidades em sua representação intermediária.
Símbolos aparecem no namespace Phx.Symbols e tem em comum a classe base Phx.Symbols.Symbol.
Phoenix disponibiliza uma grande variedade de símbolos, os quais, podem ser agrupados em:
• Símbolos básicos. LocalVariableSymbol, GlobalVariableSymbol, FunctionSymbol, ConstantSym-
bol, etc.
• Símbolos que representam aspectos de módulos no formato portable Executable (PE). Import-
Symbol, ImportModuleSymbol e ExportSymbol.
• Símbolos para elementos de metadados. AssemblySymbol, ResourceSymbol, FileSymbol, etc.
A descrição de cada um dos símbolos foge ao escopo deste trabalho, para mais informações con-
sulte o manual do Phoenix cite[].
Os símbolos são agrupados dentro de tabelas, objetos Symbols.Table. Toda tabela de símbolo é
associada a uma unidade (Unit). A estrutura hierárquica das unidades provê mecanismo, básico, de
escopo para os símbolos. Desta forma, um símbolo declarado dentro de um módulo deve aparecer
dentro da tabela de símbolos de umaModuleUnit e estará dentro do escopo desta unidade.
Uma tabela de símbolos não possui, por si só, nenhum mecanismo de busca. Para realizar uma
busca numa tabela devemos associar a ela um objeto Symbol.Map, que permitirá fazer a busca na
tabela utilizando como chave uma das propriedades do símbolo. Toda tabela possui pelo menos um
um mapeamento do tipo LocalIdMap, o qual podemos utilizar para fazer a busca na tabela através
do LocalId, que é único para cada símbolo contido na tabela. ExternIdMap e NameMap são outros
exemplos de mapeamento permitidos por Phoenix.
3.4.1 Proxies
Um proxy é uma forma especial de símbolos que permite que um mesmo símbolo apareça em mais de
uma tabela de símbolo. Por exemplo, uma variável estática que é definida dentro de uma função usa
14
um proxy para indicar que é tanto, logicamente, um membro do escopo da função como, fisicamente,
uma variável global.
Um exemplo de quando devemos utilizar um proxy é quando uma instrução em uma FunctionUnit
faz referência a uma variável global. Sabendo-se, que os operandos de uma instrução só podem
referenciar símbolos na tabela de símbolos da unidade da função, para acessar uma variável global
será necessário criar um proxy para esta variável na tabela de símbolos da função.
Para criar um proxy devemos criar um objeto da classe Phx.Symbols.NonLocalVariableSymbol,
utilizando para isto seu método New que recebe como parâmetros a tabela de símbolo onde o proxy
será inserido e o símbolo externo.
3.5 Sistema de Tipos
O sistema de tipos do Phoenix provê uma flexível e extensível base para criação de funcionalidades,
tais como: geração de código, pointer tracking, otimizações de alto nível e debug do código objeto.
Além disso, melhora a robustez de compiladores e ferramentas que usam a infra-estrutura de Phoenix,
permitindo que estes executem a checagem de tipos nos diversos níveis da representação intermediária
(HIR, MIR, LIR).
O sistema de tipos do Phoenix disponibiliza diferentes tipos e modos de construção de regras para
a checagem de tipo. Desta forma, um compilador pode criar um conjunto de tipos e provê regras
customizadas para sua checagem de tipos. O sistema de tipos pode expressar tanto tipos de alto nível
como tipos de nível de máquina.
A classe abstrata Phx.Types.Type é a classe base para todas as classes suportadas por Phoenix.
Ela contém propriedades compartilhadas por todos tipos Phoenix, tais como tamanho e campos de
informação.
Um sistema de tipos Phoenix é representado por um conjunto de tipos armazenados em um objeto
Phx.Types.Table e um conjunto de regras prescritas por um objeto Phx.Types.Check. Assim, para
criarmos um compilador ou ferramenta precisamos criar apenas uma tabela, que deve ser compartil-
hada por todo a ferramenta. Vale observar que esta tabela é particular para uma arquitetura alvo uma
vez que cada arquitetura possui seu próprio sistema de tipos. Certos tipos, tais como tipos primitivos,
são disponibilizados como propriedades da tabela, sendo criados quando Phoenix gera a instância da
tabela.
15
3.5.1 PrimitiveTypes
Os tipos primitivos Phoenix são representados utilizando objetos da classe Phx.Types.PrimitiveTypes.
Esta classe pode ser utilizada para representar inteiros com e sem sinal, números de ponto flutuante
de diversos tamanhos, void, bitfields e tipos desconhecidos em tempo de compilação (unknown). O
tipo Unknown é especialmente utilizado para representar um tipo referenciado por um tipo ponteiro
que é desconhecido ou desnecessário em tempo de compilação. Uma situação em que isto ocorre é na
compilação rápida just-in-time (JIT), onde é necessário saber se um ponteiro é coletado pelo coletor
de lixo, mas não é necessário saber seu tipo.
O sistema de tipos da IR inclui tipos primitivos normalmente encontrado na maioria das linguagens
e arquiteturas, tais como tipos inteiros e de ponto flutuante.
3.5.2 PointerTypes
O tipo ponteiro é criado através da classe Phx.Types.PointerType. As duas principais propriedades
de um objeto deste tipo são: ReferentType, que indica o tipo referenciado pelo ponteiro, e Point-
erTypeKind, tipo do ponteiro. A tabela 4 apresenta os possíveis tipos de ponteiros que podem ser
criados com Phoenix.
3.5.3 AggregateTypes
Um tipo agregados é um tipo que possui membros (dados, métodos ou outros tipos). Desta forma,
classes, estruturas e interfaces são tipos que devem ser representados utilizando a classe Phx.Types.
AggregateType.
Todo objeto AggregateType define um tipo distinto, não havendo equivalência estrutural entre
tipos agregados. Para representar categorias diferentes de tipos agregados, tais como classes, structs
e interfaces, a classe AggregateTypes possui meta-propriedades que especificam as diferenças fun-
cionais entre tipos diferentes. Ou seja, a combinação de meta-propriedades é que descrevem qual tipo
esta sendo modelado.
A lista de meta-propriedades é bastante extensa e deve ser consultada no manual do Phoenix. A
tabela 5 mostra meta-propriedades de tipos agregados MSIL e C++.
16
3.5.4 ArrayTypesUm tipo array define uma coleção de variáveis, de um tipo particular, que são acessadas utilizando
índices. Normalmente esta coleção possui um tamanho específico, setado em sua instanciação. Podem
ser de dois tipos, representados pelas classes:
• Phx.Types.ManagedArrayType, representa um arrays gerenciados pelo coletor de lixo, é uma
especialização de tipos agregados.
• Phx.Types.UnmanagedArrayType, representa tipos de arrays nativos, não gerenciado pelo
coletor de lixo.
3.5.5 FunctionTypes
Um tipo função descreve as estruturas que compõem uma função. São similares a protótipos de função
em linguagens como C++. Diferentemente dos outros tipos, o tipo função não possui um tamanho
associado a ele e é composto pelos seguintes elementos:
• Uma lista de argumentos (Phx.Types.FunctionArgument).
• Uma lista de valores de retorno, também do tipo (Phx.Types.FunctionArgument).
• Uma convenção de chamada (FastCall, ClrCall, CDecl, ThisCall, StdCall ou IllegalSentinel).
Depende de como é feita a chamada a uma função na arquitetura alvo.
• Um valor que especifica se a função tem um número variável de argumentos.
Existem duas formas de construir uma função no Phoenix. A mais simples e direta é através do
método Phx.Types.Table.GetFunctionType, utilizada no código 11. Esta abordagem, entretanto,
limita a função a ter apenas um valor de retorno e no máximo quatro argumentos. A outra maneira
é utilizando a classe auxiliar Phx.Types.FunctionTypeBuilder, que permite a construção de uma
maior variedade de funções. A utilização desta ultima forma pode ser visto no código 6, que cria uma
função que referencia a função printf.
1 //int __cdecl printf(const char *, ...);
2
3 Phx.Types.FunctionTypeBuilder funcTypeBuilder =
4 Phx.Types.FunctionTypeBuilder.New(typeTable);
5
17
6 funcTypeBuilder.Begin();
7
8 funcTypeBuilder.CallingConventionKind =
9 Phx.Types.CallingConventionKind.CDecl;
10
11 funcTypeBuilder.AppendReturnType(module.RegisterIntType);
12
13 Phx.Types.Type charType = typeTable.Character8Type;
14 Phx.Types.PointerType ptrToCharType =
15 typeTable.GetUnmanagedPointerType(charType);
16 funcTypeBuilder.AppendArgumentType(ptrToCharType);
17
18 Phx.Types.FunctionArgument ellipsisArg =
19 Phx.Types.FunctionArgument.New(typeTable,
20 Phx.Types.FunctionArgumentKind.Ellipsis, typeTable.UnknownType);
21 funcTypeBuilder.AppendArgumentFunctionArgument(ellipsisArg);
22
23 Phx.Types.FunctionType printfType = funcTypeBuilder.GetFunctionType();
24
25 Phx.Name printfName = Phx.Name.New(lifetime, "_printf");
26
27 printfSym = Phx.Symbols.FunctionSymbol.New(moduleSymTable, 0,
28 printfName, printfType, Phx.Symbols.Visibility.GlobalReference);
Código 6: Construindo uma função printf utilizando FunctionTypeBuilder
3.5.6 Fields
Phoenix utiliza a classe Phx.Types.Field para descrever subpartes de outros tipos, tais como membros
de tipos agregados ou padrões de bits em tipos primitivos.
Um tipo Field é composto por:
• Um offset do início do tipo a que ele faz parte.
• O seu tamanho (tamanho da subparte).
• Seu tipo (tipo da subparte).
18
4 Mapeando Estruturas
Para facilitar a tradução de um programa para a Representação Intermediária serão criados Tem-
plates que representarão cada uma das estruturas básicas de uma linguagem. Os templates aqui cri-
ados tentarão ser o mais genérico possível não tendo em vista nenhuma linguagem específica. O
objetivo é dar uma idéia de como representar estruturas básicas que possam ser, posteriormente, mod-
ificadas para modelar linguagens específicas.
4.1 Escopo e Funções Auxiliares
Um compilador durante a compilação de um programa necessita armazenar o conjunto de símbolos
já declarados (variáveis e funções) que podem ser utilizados num dado momento. Este conjunto de
declarações acessíveis em um dado momento representam o contexto no qual o compilador esta tra-
balhando. Na realidade, um compilador armazena uma pilha de contextos, onde sempre que críamos
uma nova função empilhamos um novo contexto e ao sairmos desta desempilhamos o contexto. Desta
forma o conceito de contexto está intimamente ligado ao conceito de escopo. Uma forma de se
representar o contexto de um compilador e, conseqüentemente, escopo é através de uma lista de ma-
peamentos NameMap. A criação deste mapeamento, bem como, de alguns métodos auxiliares pode
ser vista no código 7
1 List<Phx.Symbols.NameMap> nameMapList =
2 new List<Phx.Symbols.NameMap>();
3
4 public void BeginScope()
5 {
6 nameMapList.Insert(0, Phx.Symbols.NameMap.New(funcSymTable, 64));
7 }
8
9 public void BeginScope(Phx.Symbols.NameMap nameMap)
10 {
11 nameMapList.Insert(0, nameMap);
12 }
13
14 public void EndScope()
15 {
16 nameMapList.RemoveAt(0);
19
17 }
18
19 Phx.Symbols.Symbol LookupSymbol(string name)
20 {
21 Phx.Name symName = Phx.Name.New(funcLifetime, name);
22 foreach (Phx.Symbols.NameMap nameMap in nameMapList)
23 {
24 Phx.Symbols.Symbol s = nameMap.Lookup(symName);
25 if (s != null)
26 return s;
27 }
28 return null;
29 }
Código 7: Métodos que manipulam o escopo de um símbolo
Nas linhas 1 e 2 é criada a lista de NameMap responsável por armazenar os diversos contextos
existentes num programa. BeginScope, linhas 4-12, utilizam o método Insert para criar um novo
escopo onde serão definidos os símbolos. Este novo escopo (NameMap) é inserido no início da lista.
EndScope, linhas 14-17, remove o escopo quando este não é mais necessário. Por último, linhas
19-29, temos o método LookupSymbol responsável por buscar na lista de escopos um determinado
símbolo. Esta busca se inicia no escopo corrente e desce até o escopo global.
Outra maneira, ainda mais direta, de se representar contextos utilizando o Phoenix é através dó
método Phx.Threading.Context.GetCurrent(), que retorna um contexto através do qual podemos
obter a unidade corrente. Utilizando esta metodologia, devemos pós o fim do uso de uma unidade
retirá-la do contexto corrente através do método PopUnit() de seu contexto. A busca por símbolos
utilizando esta forma de contexto deve ser feita utilizando as próprias tabelas de símbolos da unidade
corrente e de suas unidades pais, sua implementação pode ser observado no código 8.
1 Phx.Symbols.Symbol LookupSymbol(string name)
2 {
3 Phx.Unit currentUnit = Phx.Threading.Context.GetCurrent().Unit;
4
5 Phx.Name symName = Phx.Name.New(funcLifetime, name);
6
7 while (currentUnit != null)
20
8 {
9 Phx.Symbols.Symbol s = currentUnit.SymbolTable.NameMap.Lookup(
symName);
10 if (s != null)
11 return s;
12 else
13 currentUnit = currentUnit.ParentUnit;
14 }
15 return null;
16 }
Código 8: Método LookupSymbol utilizando Phx.Threading.Context
4.2 Criando Variáveis
A tarefa de criar variáveis em um programa consiste, basicamente, em fazer o mapeamento do
nome da variável a um símbolo e armazenar este mapeamento na tabela de símbolos do contexto
atual. Este contexto pode ser global, ou seja vale para todo o programa, ou ainda ser local, valendo
apenas para uma parte específica do programa (bloco, função, etc).
4.2.1 Variáveis Globais
O contexto das variáveis globais é o primeiro a ser criada e so é descartado quando o programa chega
ao fim. O código 9, descreve a criação do contexto global e a inserção de variáveis nele.
1 Phx.Symbols.GlobalVariableSymbol sym
2 = Phx.Symbols.GlobalVariableSymbol.New(
3 moduleSymTable,
4 externalIdCounter++,
5 Phx.Name.New(lifetime, name),
6 type,
7 Phx.Symbols.Visibility.GlobalDefinition
8 );
9 sym.AllocationBaseSectionSymbol = moduleSymTable.ExternIdMap.Lookup(
10 (uint)Phx.Symbols.PredefinedCxxILId.Data) as
11 Phx.Symbols.SectionSymbol;
12 sym.Alignment = Phx.Alignment.NaturalAlignment(sym.Type);21
13 Phx.Section globalSection = sym.AllocationBaseSectionSymbol.Section;
14
15 // Cria uma instrução de inicialização
16 Phx.IR.DataInstruction initializerInstr =
17 Phx.IR.DataInstruction.New(globalSection.DataUnit, 4);
18 initializerInstr.WriteInt32(0, initialValue);
19 globalSection.AppendInstruction(initializerInstr);
20 sym.Location = Phx.Symbols.DataLocation.New(initializerInstr);
Código 9: Criando variáveis globais
A primeira coisa a ser feita é criar um símbolo para representar a variável. As linhas 1 a 8 mostram
o código. O método New requer como parâmetros a tabela de símbolos onde o símbolo será inserido,
o valor da Id do variável, o nome do símbolo, seu tipo e visibilidade do símbolo. Uma variável
global deve ser armazenada na seção de dados do código. Nas linhas 9 a 11 buscamos pelo símbolo
que representa esta seção e o transformamos em um objeto Phx.Symbols.SectionSymbol para então
setarmos como propriedade do símbolo criado. Linha 12 define o alinhamento do símbolo de acordo
com o seu tipo. A linha 13 é responsável por criar uma seção para definição de variáveis, a qual é
utilizada para a construção da instrução de inicialização da variável (linhas 16 a 20).
4.2.2 Variáveis Locais
Variáveis locais tem seu escopo reduzido ao contexto onde foi criado, por exemplo uma função ou
bloco. Desta forma, seu mapeamento deve ser inserido na lista de contextos na posição correta que se
refere ao contexto em que foi criado. O código 10 demonstra como criar uma variável local.
1 Phx.Symbols.LocalVariableSymbol sym
2 = Phx.Symbols.LocalVariableSymbol.New(
3 table,
4 externID,
5 name,
6 type,
7 storageClass
8 );
9
10 sym.Alignment = Phx.Alignment.NaturalAlignment(sym.Type);
22
Código 10: Criando variáveis locais
O primeiro comando, linhas 1-8, criam um símbolo que representa uma variável local. O método
New recebe como argumento a tabela onde o símbolo deverá ser armazenada (tabela da unidade
a que a variável pertence), um inteiro não sinalizado que serve de identificador, o nome e tipo da
variável e um objeto do tipo Phx.Symbol.StorageClass. O objeto StorageClass define a forma de
armazenamento da função, podendo ser: uma variável local de uma função, um parâmetro de uma
função, etc. O próprio método New se encarrega de inserir o símbolo na tabela de símbolos. Na linha
10 definimos o tipo de alinhamento do símbolo, ou seja qual o espaço que ele ocupa na memória o
que depende do tipo da variável. Caso estejamos utilizando uma lista de mapeamento para gerenciar
o contexto devemos inserir o símbolo no contexto corrente.
4.3 Construindo Funções
Função é a unidade básica de qualquer programa. Para que um programa execute é necessário que
ele possua pelo menos uma função, a função _main(). É dentro de uma função que definimos todo o
fluxo de instruções que realizam as operações que compõe um programa. Nesta seção será mostrado
como criar uma função e adicionar o fluxo de instruções que define sua funcionalidade. Diferente
do que foi mostrado na seção 3.5.5, onde criamos um tipo função utilizando FunctionBuilder para
representar uma função externa, aqui utilizaremos um método da tabela de símbolos para construir o
nosso tipo função.
1 Phx.Types.FunctionType fnType = typeTable.GetFunctionType(
2 Phx.Types.CallingConventionKind.CDecl,
3 retType,
4 paramTypes[0],
5 paramTypes[1],
6 paramTypes[2],
7 paramTypes[3]
8 );
9
10 Phx.Symbols.FunctionSymbol fnSym = Phx.Symbols.FunctionSymbol.New(
11 moduleSymTable,
12 0,
13 Phx.Name.New(lifetime, fnname),
23
14 fnType,
15 Phx.Symbols.Visibility.GlobalDefinition);
16
17 // Verificar aqui se o identificador da função já existe no contexto
corrente
18
19 func = Phx.FunctionUnit.New(
20 Phx.Lifetime.New(Phx.LifetimeKind.Function, null),
21 fnSym,
22 Phx.CodeGenerationMode.Native,
23 typeTable,
24 module.Architecture,
25 module.Runtime,
26 module,
27 funcCounter++
28 );
29
30 AddParametersToFuncSymbolTable(paramList);
31
32 // Build ENTERFUNC
33
34 Phx.IR.LabelInstruction enterInstr = Phx.IR.LabelInstruction.New(
35 func,Phx.Common.Opcode.EnterFunction, fnSym);
36
37 foreach (Parser.ParamInfo param in paramList)
38 {
39 Phx.Symbols.LocalVariableSymbol sym =
40 (Phx.Symbols.LocalVariableSymbol)LookupSymbol(param.name);
41 Phx.IR.VariableOperand opnd
42 = Phx.IR.VariableOperand.New(
43 func,
44 sym.Type,
45 sym
46 );
24
47 enterInstr.AppendDestination(opnd);
48 }
49 func.FirstInstruction.InsertAfter(enterInstr);
50
51 Phx.IR.LabelOperand labelOpnd = Phx.IR.LabelOperand.New(func,
52 enterInstr);
53 func.FirstInstruction.AppendLabelSource(
54 Phx.IR.LabelOperandKind.Technical, labelOpnd);
55
56 // Local para inserção das intruções da função
57
58
59 exitInstr = Phx.IR.LabelInstruction.New(func,
60 Phx.Common.Opcode.ExitFunction);
61
62 functionUnit.LastInstruction.InsertBefore(
63 exitInstruction)
Código 11: Construindo uma função
Através do método GetFunctionType da tabela de símbolos criamos o tipo que define nossa
função, linhas 1 a 8. Os argumentos para este métodos são: a convenção de chamada, o tipo de
retorno da função e os tipos de até quatro argumentos da função. Nas linhas 10 a 15 é criado o sím-
bolo que representa a função. Em seguida é criado a unidade da função, seu método New recebe um
lifetime, o símbolo da função, o modo de geração de código, a tabela de tipos, arquitetura, ambiente
de execução, o módulo ao qual a função pertence e um identificador inteiro como argumentos. A
linha 30 chama um método responsável por criar símbolos do tipo LocalVariableSymbol para cada
um dos argumentos da função. Já nas linhas 37 a 48 cada um destes símbolos é transformado em
um operando de destino para a instrução EnterInstruction definida na linha 34. Esta instrução é um
label que determina o pondo entrada e deve ser inserida após a primeira instrução da função. As lin-
has 51 a 54 criam um operando label que será passada para primeira instrução da função para indicar
onde começa o fluxo de instruções da função. Após isto é aberto o bloco para inserção das instruções
que compõem a funcionalidade da função. As ultimas linhas (59 a 63) criam e inserem no fluxo de
instruções a instrução de saída da função.
25
4.4 Comandos e Expressões
O fluxo de controle e as operações realizadas em um programa são definidas através de comandos
ou cálculo de expressões. Estes podem ser traduzidos diretamente em uma instrução na representação
intermediária ou em um conjunto de instruções. As seções a seguir definem como construir alguns co-
mandos e calcular expressões utilizando o Phoenix. Estas construções devem ser inseridas na função
logo após label EnterFunction, no local indicado no código 11.
4.4.1 Operação Binária
Operações binárias são bastante utilizadas para o cálculo de expressões onde um valor é calculado
a partir da aplicação de uma operação em dois valores. A construção de operações binárias é bem
direta, como pode ser observado no código 12. Este código deve ser utilizado através de uma função
que irá ter como retorno um objeto Phx.Ir.Operand, o qual é uma referência para o valor calculado
pela operação binária e pode ser utilizado em outras instruções.
1 Phx.Opcode opcode = null;
2 switch (op)
3 {
4 case "+": opcode = Phx.Common.Opcode.Add; break;
5 case "-": opcode = Phx.Common.Opcode.Subtract; break;
6 case "*": opcode = Phx.Common.Opcode.Multiply; break;
7 case "/": opcode = Phx.Common.Opcode.Divide; break;
8 default: parser.UhOh("Unknown binop: " + op); break;
9 }
10
11 Phx.IR.Instruction instr = Phx.IR.ValueInstruction.NewBinaryExpression(
12 func, opcode, type, src1, src2);
13
14 func.LastInstruction.InsertBefore(instr);15
16 return instr.DestinationOperand;
Código 12: Operação binária
26
4.4.2 Atribuição
Representam instruções que atribuem valor a variáveis já instanciadas. A primeira coisa a se fazer é
fazer a busca do símbolo da variável e utilizá-lo para criar um operando (linhas 2 a 4). A linha cinco
cria um ImmediateOperand que representa uma constante numérica a ser adicionada a variável,
entretando poderíamos substituir este operando por qualquer outro operando, como por exemplo o
operando retornado por uma expressão binária. Nas linhas 9 a 12 críamos a instrução de atribuição
e setamos seus operandos de origem e destino. A última instrução insere a instrução no fluxo de
instruções da função.
1 // Cria os operandos de origem (expressão) e destino (variável)
2 Phx.Symbols.Symbol sym = symbolTable.LookupByName(symbolName);
3 Phx.IR.VariableOperand dst = Phx.IR.VariableOperand.New(
4 functionUnit, sym.Type, sym);
5 Phx.IR.Operand src = Phx.IR.ImmediateOperand.New(
6 functionUnit, typeTable.Int32Type, 1);
7
8 // Cria a instrução de atribuição
9 Phx.IR.Instruction instr = Phx.IR.ValueInstruction.New(
10 funcUnit, Phx.Common.Opcode.Assign);
11 instr.AppendSource(src);
12 instr.AppendDestination(dst);
13
14 // Insere a instrução de atribuição
15 functionUnit.LastInstruction.InsertBefore(instr);
Código 13: Atribuição de valor a uma variável
4.4.3 Chamadas de Funções
Toda linguagem moderna possui algum mecanismo de chamada de função. A utilização de função
evita a repetição de códigos e dá maior expressividade a uma linguagem. Uma vez que já foi demon-
strado como criar uma função, será mostrado aqui como fazer a chamada a uma função passando a
ela seus parâmetros.
Inicialmente, deve ser feita a busca pelo símbolo que representa a função. Este então será utilizado
para a criação da instrução de chamada de função, linhas 4 a 8. Todos os parâmetros da função devem
27
ser adicionados como operandos de origem. Como operando de destino devemos passar o operando
que receberá o valor de retorno da função e por último adicionamos a instrução de chamada ao fluxo
de instruções.
1 Phx.Symbols.FunctionSymbol myFnSym =
2 (Phx.Symbols.FunctionSymbol)LookupSymbol(name);
3
4 Phx.IR.Instruction callInstr = Phx.IR.Instruction.NewCall(
5 func,
6 Phx.Common.Opcode.Call,
7 myFnSym
8 );
9
10 foreach (Phx.IR.Operand opnd in args)
11 {
12 callInstr.AppendSource(opnd);
13 }
14
15 callInstr.AppendDestination(dstOpnd);
16
17 func.LastInstruction.InsertBefore(callInstr);
Código 14: Chamada a uma função
28
Unit Descrição
FunctionUnit Um repositório para o fluxo de instruções, tabela de símbolos, gráfico de fluxo
e inter-referência (aliasing) de informações específicas para um método ou
função. Unidade alvo da maioria das alterações proporcionadas pela lista de
fases
DataUnit Uma coleção de dados relacionados, tais como um conjunto de variáveis ini-
cializadas ou o resultado de uma codificação de uma FunctionUnit. Provê
dados necessários para processar uma unidade.
ModuleUnit É uma coleção de funções que normalmente representa um programa ou ar-
quivo fonte. Pode ter uma DataUnit.
PeModuleUnit Tipo especial de ModuleUnit que representa um arquivo PE (portable Exe-
cutable), que pode ser um executável Windows (EXE) ou uma biblioteca de
link dinâmico (DLL).
AssemblyUnit Unidade de compilação de um assembly do Framework .NET. Contém uma
lista de objetos ModuleUnit. Menor unidade de re-uso, segurança e version-
amento.
ProgramUnit Unidade de compilação correspondente a uma imagem executável, podendo
ser um arquivo EXE ou um DLL. Contém uma lista de AssemblyUnits e uma
lista de ModuleUnit. A razão para conter duas listas é que arquivos Win32
não são formados por assembly e desta forma um objeto ProgramUnit pode
conter diretamente módulos que não estão dentro de assemblies.
GlobalUnit Unidade de compilação mais externa, contém uma lista de objetos Progra-
mUnit. Criada quando inicializamos a infra-estrutura phoenix, armazena,
entre outras coisas, as tabelas de símbolos e de tipos
Tabela 3: Unidades da Representação Intermediária de Phoenix
29
PointerTypeKind Descrição
ObjectPointer Ponteiro gerado que aponta para o início de um objeto, podendo ser
utilizado para referenciar um objeto por completo.
ManagedPointer Ponteiro gerenciado pelo coletor de lixo
UnmanagedPointer Ponteiro não gerenciado pelo coletor de lixo
NullPointer Representa um ponteiro null
Tabela 4: Tipos de PointerTypes
Unit Descrição
Classe Msil Phx.Types.AggregateType.IsSelfDescribing,
Phx.Types.AggregateType.IsPrimary
Estrutura Msil Phx.Types.AggregateType.IsPrimary,
Phx.Types.AggregateType.IsSealed, mas não
Phx.Types.AggregateType.IsSelfDescribing
Interface Msil Phx.Types.AggregateType.IsPureInterface
Enumeração Msil Phx.Types.AggregateType.IsPrimary,
Phx.Types.AggregateType.IsSealed, não
Phx.Types.AggregateType.IsSelfDescribing
Classe C++ Phx.Types.AggregateType.IsSelfDescribing se
tiver função virtual ou multiplas classes base, normal-
mente é não Phx.Types.AggregateType.IsPrimary
Struct C/Enumeração C++ Phx.Types.AggregateType.IsPureData
Tabela 5: Meta-propriedade de tipos agregados
30

Continue navegando