Baixe o app para aproveitar ainda mais
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
Compartilhar