137
Dissertação submetida à UNIVERSIDADE DE COIMBRA Para Obtenção do Grau de Mestre em Informática e Sistemas Instrumentação de Código na Plataforma .NET Bruno Miguel Brás Cabral Dissertação orientada por Doutor Paulo Jorge Pimenta Marques Departamento de Engenharia Informática Universidade de Coimbra, Portugal Departamento de Engenharia Informática Faculdade de Ciências e Tecnologia Universidade de Coimbra Junho 2005

Instrumentação de Código na Plataforma .NET

Embed Size (px)

Citation preview

Page 1: Instrumentação de Código na Plataforma .NET

Dissertação submetida à

UNIVERSIDADE DE COIMBRA

Para Obtenção do Grau de Mestre em Informática e Sistemas

Instrumentação de Código na

Plataforma .NET

Bruno Miguel Brás Cabral

Dissertação orientada por

Doutor Paulo Jorge Pimenta Marques Departamento de Engenharia Informática Universidade de Coimbra, Portugal

Departamento de Engenharia Informática Faculdade de Ciências e Tecnologia

Universidade de Coimbra

Junho 2005

Page 2: Instrumentação de Código na Plataforma .NET
Page 3: Instrumentação de Código na Plataforma .NET

Departamento de Engenharia Informática Faculdade de Ciências e Tecnologia

Universidade de Coimbra

ISBN 972-95988-2-7 Coimbra – Portugal, Junho 2005

Page 4: Instrumentação de Código na Plataforma .NET
Page 5: Instrumentação de Código na Plataforma .NET

Esta dissertação foi parcialmente suportada pela Fundação para a Ciência e Tecnologia através da

Unidade de Investigação CISUC (Unidade R&D 326/97) e por uma bolsa Microsoft Research Rotor-SSCLI Grant.

Page 6: Instrumentação de Código na Plataforma .NET
Page 7: Instrumentação de Código na Plataforma .NET

⎯ para a avó Lurdes ⎯

Page 8: Instrumentação de Código na Plataforma .NET
Page 9: Instrumentação de Código na Plataforma .NET

Resumo

A instrumentação de código é uma técnica que permite a modificação do código e estrutura de um programa, após este ter sido compilado. Esta técnica não é recente. Já em 1995, a EEL fazia a modificação do código binário de programas escritos em C++. No entanto, mais recentemente, a instrumentação de código tem ganho um maior destaque devido à crescente disseminação das plataformas de execução de código gerido como a Java e a .NET. O ciclo de vida do software orientado para estas plataformas permite que, por exemplo, o carregamento do código binário de um programa seja interceptado e consequentemente modificado antes de ser executado. Isto é possível porque o formato intermédio, para o qual os programas são compilados, apresenta nestes ambientes de execução uma quantidade enorme de informação que auxilia a desassemblagem do programa e à qual se dá o nome de metadata ou “informação sobre a informação”. A intercepção e modificação do código é um mecanismo muito útil para adicionar, remover ou modificar as funcionalidades de um programa. Muitas vezes estas modificações podem ser realizadas com o programa em execução, o que é particularmente útil em ambientes de produção em que as aplicações têm de estar disponíveis 24 horas por dia, 7 dias por semana. Actualmente, em .NET, não existe qualquer ferramenta capaz de realizar instrumentação de código a alto nível, a plataforma também não possui quaisquer recursos para a manipulação do código dos programas, excepto ao nível das interfaces de programação das aplicações (API) de profiling não geridos. No entanto, existe uma crescente necessidade por ferramentas de instrumentação de código dentro da comunidade científica e empresarial. Por conseguinte, o principal objectivo desta dissertação foi desenvolver uma biblioteca para instrumentação de código em .NET e discutir os problemas/soluções encontrados durante a sua implementação. Esta biblioteca foi baptizada de Runtime Assembly Instrumentation Library ou, de uma forma abreviada, RAIL. A motivação desta dissertação provém não só do facto de tal ferramenta não existir mas também por o seu desenvolvimento permitir o estudo de determinadas áreas de aplicação da instrumentação de código. Como por exemplo, a segurança, o tratamento de excepções, a programação orientada a objectos, a perfilagem de aplicações, a injecção de falhas e a optimização de código IL. Em grande parte estes estudos vieram provar que a instrumentação de código vai decididamente assumir um papel de relevo entre as técnicas de programação actuais, seja pelas suas enormes potencialidades ou pela melhoraria de desempenho dos programas instrumentados. A RAIL disponibiliza aos programadores um API de alto nível que esconde as subtilezas do formato e estrutura dos assemblies, por conseguinte, foi dado grande ênfase ao desenvolvimento de funcionalidades de alto nível como, por exemplo, a troca de referências dentro dos programas, a cópia de classes entre assemblies e a troca de chamadas a métodos (ou a campos, ou a propriedades). O desenvolvimento destas funcionalidades

Page 10: Instrumentação de Código na Plataforma .NET

x RESUMO

de alto nível motivou a realização de investigação na área dos padrões de software para instrumentação de código, tendo sido dada grande importância ao estudo e aperfeiçoamento de padrões de alto nível para a realização de modificações complexas em aplicações de uma forma automática. A importância do desenvolvimento desta biblioteca para .NET foi comprovada ao longo destes dois anos pelo volume médio de acessos ao site do projecto (com mais de 600 downloads do código fonte) e a sua utilização em inúmeros projectos internacionais.

Page 11: Instrumentação de Código na Plataforma .NET

Agradecimentos

Esta dissertação nunca teria tido um início, se o Doutor Luís Silva não me tivesse motivado

para a sua realização. Não teria um fim, se não fosse o exemplo de responsabilidade,

qualidade, motivação, disponibilidade, entusiasmo, confiança, sabedoria e empenho que

encontro todos os dias na pessoa do meu orientador, o Doutor Paulo Marques.

Por outro lado, também não haveria um meio se a minha querida noiva não tivesse

aturado todas as minhas rabugices e maus humores e, mais importante, arrastado para

outros lugares e outras situações quando era mais preciso.

O Patrício, sempre com um script na manga e uma observação que nos alegra o dia,

também deu o seu “empurrãozinho”.

Quero agradecer ao Sacra, pela convivência ao longo destes dois anos e pelo seu trabalho

de estágio. Não posso esquecer o Nuno Santos, que embora já tenha seguido outros rumos

há muito tempo, também deu a sua contribuição para o design do API do RAIL.

Quero agradecer à Microsoft Research por acreditarem, desde muito cedo, no projecto RAIL

e, em particular, ao Eng. Vítor Santos da Microsoft Portugal, pelo papel importante que tem

na divulgação do projecto.

Não posso esquecer o Doutor João Gabriel, que apadrinhou a primeira fase deste

mestrado, e o Doutor Henrique Madeira, co-orientador do trabalho de Seminário II. A eles

o meu muito obrigado.

Por fim, quero agradecer aos meus pais, à minha manita, à minha madrinha, à minha tia

Zilda e prima Catarina, pelas palavras de incentivo nestes últimos meses.

Acima de tudo, a todos agradeço a amizade.

Page 12: Instrumentação de Código na Plataforma .NET
Page 13: Instrumentação de Código na Plataforma .NET

Índice

RESUMO........................................................................................................................................ IX

AGRADECIMENTOS ................................................................................................................. XI

ÍNDICE........................................................................................................................................ XIII

1. INTRODUÇÃO........................................................................................................................... 1 1.1. Motivação.................................................................................................................... 2 1.2. Objectivos de Investigação...................................................................................... 4 1.3. Contribuição............................................................................................................... 6 1.4. Estrutura da Dissertação........................................................................................... 7

2. INSTRUMENTAÇÃO DE CÓDIGO ...................................................................................... 9 2.1. Instrumentação para Máquinas Virtuais............................................................. 10

2.1.1. Tipos de Instrumentação.................................................................................... 12 2.1.2. Intercepção do Carregamento de Código ........................................................ 13 2.1.3. Níveis de Instrumentação de Código ............................................................... 15

2.2. Bibliotecas para a Plataforma JAVA .................................................................... 16 2.2.1. Bytecode Engineering Library................................................................................ 16 2.2.2. SERP...................................................................................................................... 17 2.2.3. JAVA Object Instrumentation Environment......................................................... 18 2.2.4. ASM ...................................................................................................................... 18 2.2.5. Javassist.................................................................................................................. 20 2.2.6. Binary Component Adaptation .............................................................................. 21 2.2.7. JMangler ................................................................................................................ 22 2.2.8. Twin Class Hierarchy Approach............................................................................ 24

2.3. Instrumentação de Código em .NET.................................................................... 26 2.3.1. API de Perfilagem de Programas ...................................................................... 26

3. INSTRUMENTAÇÃO DE CÓDIGO EM .NET .................................................................. 29 3.1. Introdução................................................................................................................. 30 3.2. Execução em .NET ................................................................................................... 30

3.2.1. Execução ............................................................................................................... 35 3.3. Arquitectura ............................................................................................................. 38

3.3.1. Leitura e Carregamento em Memória .............................................................. 39 3.3.2. Representação Orientada aos Objectos de um Programa .............................. 43

3.4. Instrumentação de Alto Nível............................................................................... 60 3.4.1. O padrão de software Visitor ............................................................................... 60 3.4.2. Substituição de Referências................................................................................ 62 3.4.3. Cópia de Classes e Métodos............................................................................... 64 3.4.4. Redireccionamento de Chamadas a Métodos.................................................. 65 3.4.5. Redireccionamento do Acesso a Campos e Propriedades ............................. 66 3.4.6. Adicionar Epílogos e Prólogos a Métodos ....................................................... 67

Page 14: Instrumentação de Código na Plataforma .NET

ÍNDICE

XIV

3.5. Trabalho Relacionado .............................................................................................71 3.5.1. AbstractIL...............................................................................................................71 3.5.2. PEAPI e PERWAPI ..............................................................................................71 3.5.3. MONO PEToolkit ................................................................................................72 3.5.4. Reflector ................................................................................................................72 3.5.5. CLIFileReader.......................................................................................................73 3.5.6. Common Language Aspect Weaver..................................................................73 3.5.7. WEAVE.NET ........................................................................................................74

4. DOMÍNIOS DE APLICAÇÃO ...............................................................................................75 4.1. Alternativa aos Proxies Dinâmicos.......................................................................76

4.1.1. Avaliação de Desempenho .................................................................................82 4.2. Avaliação dos Mecanismos de Tratamento de Excepções ................................86 4.3. Programação Orientada aos Aspectos ..................................................................99

4.3.1. Utilização de Custom Attributes ........................................................................101 4.3.2. Tratamento de Excepções Automático............................................................104

4.4. Projectos de Terceiros ...........................................................................................105

5. CONCLUSÃO..........................................................................................................................109 5.1. Avaliação do Projecto RAIL e Trabalho Futuro ...............................................110

BIBLIOGRAFIA ..........................................................................................................................113

LISTA DE PUBLICAÇÕES .......................................................................................................122 Artigos em Revistas ..........................................................................................................122 Artigos em Conferências Internacionais.......................................................................122 Relatórios Técnicos...........................................................................................................122 Palestras Convidadas........................................................................................................122

Page 15: Instrumentação de Código na Plataforma .NET

Introdução

“Se o conhecimento pode causar problemas, não é com ignorância que os vamos resolver.”

— Isaac Asimov

Esta dissertação é o resultado do trabalho realizado em instrumentação de código em

máquinas virtuais, entre Julho de 2002 e Outubro de 2004, no seio do Grupo de Sistemas

Confiáveis da Universidade de Coimbra. Na base deste trabalho está o estudo dos

problemas existentes no desenvolvimento de uma biblioteca para instrumentação de

código na plataforma .NET e a descrição das soluções encontradas.

Neste capítulo introdutório são apresentadas as motivações e os objectivos de investigação,

de forma a contextualizar o trabalho em discussão. São também enumeradas as

contribuições desta dissertação e é descrita a sua estrutura.

Capítulo

1

Page 16: Instrumentação de Código na Plataforma .NET

2 CAPÍTULO 1 — INTRODUÇÃO

1.1. Motivação A instrumentação de código é um mecanismo que permite aos programas reescrever o

código de outros programas ou o seu próprio código, após a compilação e imediatamente

antes ou durante a sua execução. Esta abordagem tem sido usada ao longo dos anos com

diferentes objectivos, como por exemplo: traçar o perfil das aplicações [Dmitriev2004],

injectar falhas em software [Fu2004], optimizar e reutilizar código [Vall1999], realizar

verificações de segurança [Chander1999], realizar migração transparente de threads

[Truyen2000] e manipular as aplicações para que se faça gestão do acesso aos recursos do

sistema [Binder2001]. Todos estes cenários de aplicação ainda são válidos mas,

recentemente, a instrumentação de código tornou-se mais atractiva com o aparecimento da

Programação Orientada aos Aspectos (AOP) [Kiczales1997]. Este paradigma de

programação teve origem nos laboratórios do Xerox Park e postula que existem vários

aspectos comuns a diferentes componentes de uma aplicação, que lhe podem ser aplicados

de uma forma transversal.

O código de um programa assume diferentes formas consoante a fase do processo de

desenvolvimento de software em que este se encontra: Código Fonte, escrito pelo

programador numa linguagem de alto nível; Código Binário, quase sempre obtido por

compilação do código fonte; Código Intermédio, executado por máquinas virtuais (e.g. JAVA

e .NET), produto da compilação de código fonte e alvo de compilação ou interpretação por

parte das máquinas virtuais. Exemplos de código intermédio são o Bytecode, da plataforma

JAVA [Lindholm1999], e o Intermediate Language (IL), da plataforma .NET [ECMA2002].

Nos últimos anos, a instrumentação de código intermédio tem ganho popularidade devido

ao crescente interesse em plataformas de execução virtuais e o aumento da sua utilização

em diferentes áreas. Um dos aspectos fundamentais das linguagens de programação

modernas é a possibilidade do código ser carregado em tempo de execução a partir de uma

fonte em formato binário (e.g. de um ficheiro ou de um endereço na rede). Um efeito

colateral desta funcionalidade é a possibilidade de modificar o código imediatamente antes

ou durante a sua execução, introduzindo ou removendo instruções e modificando as

referências para classes, métodos ou campos, isto permite que se faça, por exemplo, uma

determinada verificação de segurança que anteriormente não era realizada.

Consideremos um possível cenário de aplicação, um utilizador ao descarregar uma

aplicação da Internet, não pode assumir à partida que esta é de confiança. É essencial saber

Page 17: Instrumentação de Código na Plataforma .NET

MOTIVAÇÃO 3

quais os ficheiros que são lidos e escritos por essa aplicação, para assegurar que esta não

está a roubar informação confidencial e enviá-la para parte incerta. Uma forma de o

conseguir seria utilizar um desassemblador e procurar entender a estrutura do código. No

entanto, exceptuando aplicações triviais, esta solução não é realmente viável. Utilizando

instrumentação de código, é possível substituir transparentemente todas as referências

para as classes responsáveis por implementar os métodos de I/O por referências para

proxies [Gamma1995] que implementem as mesmas interfaces. Estas proxies podem registar

todos os acessos ao sistema de ficheiros antes de permitir a invocação dos métodos

originais, e mesmo barrar a sua execução. Desta forma, o utilizador pode examinar os logs

e verificar quais os ficheiros que são acedidos ou até mesmo permitir o acesso caso a caso.

As razões que tornam a instrumentação de código aliciante são bem visíveis nos diversos

cenários de aplicação já mencionados. Mas, o que é que torna a instrumentação de código

em máquinas virtuais ainda mais atraente? Em termos práticos, a instrumentação de

código intermédio é mais eficaz que a instrumentação de código máquina (específico de

uma plataforma), pois as modificações nos programas são propagáveis a todas as

plataformas para as quais a portabilidade é assegurada. Por outro lado, poderia ser feita a

instrumentação de código fonte. A opção de não o fazer (e apontar o código intermédio

como alvo da instrumentação) deve-se principalmente a duas razões, a primeira é o facto

do código fonte não ser normalmente disponibilizado ao utilizador, a segunda relaciona-se

com a riqueza em metadata [ECMA2002], existente nos ficheiros do formato intermédio,

que vai simplificar muito o processo de instrumentação de código. A metadata é descrita

como “dados sobre os dados” e é utilizada para caracterizar a estrutura, funcionamento e

recursos de um programa. O código intermédio é também muito mais fácil de manipular

do que o código fonte, pois só é possível invocar instruções muito simples e com um

número bem definido de argumentos.

O conceito de instrumentação de código está contido num outro mais abrangente: o de

Reflexão [Ferber1989;Malenfant1992]. Este é definido como sendo a capacidade de um

programa de “olhar para si próprio” (i.e. saber como é constituído e estruturado) e ser

capaz de modificar tanto a sua estrutura como o seu comportamento em tempo de

execução. A instrumentação de código permite adicionar mecanismos de reflexão a

linguagens de programação que não os possuem ou os implementam parcialmente (e.g.

C#, JAVA, C++). A plataforma .NET não possui um verdadeiro API de Reflexão visto que

o System.Reflection.Emit não permite alterar a estrutura ou o comportamento dos

Page 18: Instrumentação de Código na Plataforma .NET

4 CAPÍTULO 1 — INTRODUÇÃO

programas, este API permite apenas que um programa saiba como é constituído, pelo que,

a esta capacidade se dá usualmente o nome de Introspecção.

Nesta dissertação, vão ser descritos os problemas e as soluções associadas ao

desenvolvimento de uma biblioteca de instrumentação de código para a plataforma .NET,

permitindo ao programador ter assim um API completo para Reflexão. A palavra completo

tem neste contexto um papel muito importante pois uma das grandes motivações para esta

dissertação é o preenchimento de uma lacuna existente na plataforma. O Common Language

Runtime (CLR) disponibiliza um API para ler programas (embora não seja possível ver o

código IL dos métodos) e gerar novos programas em tempo de execução

(System.Reflection.Emit). No entanto, não é possível ler, modificar e gerar

novamente uma aplicação, utilizando apenas as bibliotecas da plataforma. O trabalho

desta dissertação representa uma primeira tentativa para alcançar este objectivo visto que,

como será discutido na secção de trabalho relacionado, não existe actualmente nenhuma

biblioteca capaz de realizar instrumentação de código de alto nível em .NET.

A plataforma .NET é multi-linguagem, isto significa que existem compiladores em .NET

para diferentes linguagens de programação como a C#, J# , C++, Eiffel, entre outras. Por

conseguinte, realizar instrumentação de código sobre o formato intermédio do CLR, para o

qual os programas em .NET são compilados, permite a modificação de programas

independentemente da linguagem em que estes foram escritos.

1.2. Objectivos de Investigação O objectivo principal desta dissertação foi desenvolver uma biblioteca de instrumentação

de código para a plataforma .NET. Deste objectivo derivaram muitos outros com um papel

secundário mas igualmente indispensáveis para a escrita desta biblioteca, muitos nasceram

do trabalho de análise/aprendizagem das bibliotecas de instrumentação de código em

JAVA (ver a secção 2.2) e de particularidades inerentes à plataforma .NET. Estes objectivos

são descritos nos seguintes tópicos:

Page 19: Instrumentação de Código na Plataforma .NET

OBJECTIVOS DE INVESTIGAÇÃO 5

• Produzir uma estrutura num formato Orientado aos Objectos (OO) capaz de

representar um Assembly e todos os seus componentes (Figura 1). Um Assembly

pode ser constituído por diversos ficheiros e cada ficheiro corresponde a um

módulo. Um módulo é composto por diversos tipos (i.e. classes), métodos e

campos. Cada tipo pode conter outros tipos, campos, propriedades, métodos,

construtores e eventos [ECMA2002].

• Permitir que a estrutura OO possa ser manipulada até ao nível das instruções em

Linguagem Intermédia (IL) por funcionalidades de alto nível, que escondam do

programador todos os detalhes da instrumentação de baixo nível, como o de

recalcular referências e valores de tokens (os tokens são referências entre a

metadata e o código IL existentes nos Assemblies).

• Fornecer formas de ler a metadata e carregar o código IL no ambiente de execução

sem recorrer aos mecanismos de Introspecção da plataforma visto que, estes

últimos são conhecidos pela sua fraca performance e pela lacuna de algumas

funcionalidades, como por exemplo, a disponibilização do código IL ao

programador dentro do API System.Reflection.Emit e não resolução de

tokens (referências dentro da metadata) pelo API não gerido IMetadataImport.

• O API deve ser completamente implementado em código gerido, de forma a

assegurar a portabilidade entre diferentes distribuições da plataforma .NET,

como o MONO [Novell2005a] ou o SSCLI [Stutz2002].

Figura 1 – Exemplo da estrutura de um Assembly

Page 20: Instrumentação de Código na Plataforma .NET

6 CAPÍTULO 1 — INTRODUÇÃO

• Utilizar padrões de software de programação que permitam a fácil propagação de

modificações a todos os componentes dos Assemblies, como por exemplo o padrão

de software Visitor [Gamma1995].

Um outro objectivo desta dissertação foi encontrar padrões de software para a

instrumentação de código de alto nível. Estes padrões são na realidade algoritmos que

automatizam técnicas de instrumentação de grande complexidade, como por exemplo,

substituir referências de uma classe para outra dentro de um programa. Estes mecanismos

evitam que o programador tenha de conhecer todos os pormenores do formato em que as

aplicações em .NET são guardadas para conseguir realizar complicadas instrumentações

visto que, o API em que estes novos padrões são disponibilizados lida apenas com objectos

como classes, métodos, campos, propriedades, eventos e não com tabelas de metadata,

tokens, assinaturas, streams e heaps de dados ou qualquer outra estrutura de baixo nível

existente nos assemblies.

Nesta dissertação também foram explorados alguns cenários de aplicação da Reflexão e da instrumentação de código, validando a eficácia destes mecanismos e da biblioteca desenvolvida.

1.3. Contribuição Este é o primeiro grande estudo na área de instrumentação de código, dentro da

comunidade .NET, sendo o conhecimento adquirido de grande relevância para futuros

trabalhos e investigação em todos os campos que se relacionem com a instrumentação de

código no Common Language Runtime.

Esta dissertação tem diversas implicações práticas pois, fornece aos investigadores que

utilizam o ambiente .NET como plataforma de suporte à sua investigação, uma ferramenta

muito útil para instrumentação de código. O interesse por parte da comunidade cientifica e

até empresarial é demonstrado pelos mais de 600 downloads realizados do código fonte da

biblioteca desde Outubro de 2003, altura em que o código foi colocado on-line, e pela média

mensal de 200 acessos ao site do projecto.

Uma outra contribuição desta dissertação está na identificação de novos padrões de

software de instrumentação de código de alto nível. Estes padrões são válidos não só para a

plataforma .NET, mas também, de uma forma generalista, para todas as plataformas OO.

Page 21: Instrumentação de Código na Plataforma .NET

ESTRUTURA DA DISSERTAÇÃO 7

1.4. Estrutura da Dissertação Esta dissertação está organizada em cinco capítulos:

Capítulo 1: este capítulo descreve a motivação para o trabalho desenvolvido, os

seus objectivos de investigação e a contribuição desta dissertação.

Capítulo 2: apresenta o estado da arte em instrumentação de código em máquinas

virtuais, descreve os diversos tipos de instrumentação que existe e sua

diferenciação ao nível do carregamento do código. Neste capítulo também são

descritas as bibliotecas existentes para a plataforma JAVA e o suporte para

instrumentação de código disponível em .NET.

Capítulo 3: este capítulo inicia-se com a enumeração das ferramentas mais

importantes para a instrumentação de código em .NET e descrição do trabalho

relacionado. Em seguida é discutida a arquitectura da biblioteca de

instrumentação de código, a manipulação de código intermédio e a

implementação das funcionalidades de reflexão de alto nível.

Capítulo 4: discute a utilização da biblioteca desenvolvida em diversos domínios

de aplicação, sendo também feita a avaliação do desempenho da biblioteca e da

sua utilização por terceiros.

Capítulo 5: neste capítulo são expressas as conclusões desta dissertação.

Page 22: Instrumentação de Código na Plataforma .NET
Page 23: Instrumentação de Código na Plataforma .NET

Instrumentação de Código

“É como se estivéssemos a criar um reino mágico, onde a partir de um bolo se obteria automaticamente a sua receita, e de uma receita se teria automaticamente o bolo.”

— B. C. Smith, 1983 (tradução)

Este capítulo inicia-se com a descrição do que é a instrumentação de código, quais as suas

origens, objectivos e domínios de aplicação. Em seguida, é discutida a instrumentação de

código em máquinas virtuais. Segue-se uma análise do estado da arte em bibliotecas de

instrumentação de código para a plataforma JAVA, em termos do tipo de instrumentação,

forma de intercepção do carregamento de código e nível da instrumentação.

Finalmente, são enumerados os recursos, a nível de instrumentação, existentes para a

plataforma .NET.

Capítulo

2

Page 24: Instrumentação de Código na Plataforma .NET

10 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

2.1. Instrumentação para Máquinas Virtuais A instrumentação de código não é um assunto novo. Na realidade, tem-se manipulado

código, mesmo na sua forma binária, desde que este foi gerado pela primeira vez. O que é

compreensível, pois a manipulação de código binário é de extrema utilidade em vários

cenários de aplicação da instrumentação de código, como por exemplo, traçar o perfil de

aplicações, realizar optimização de código, corrigir bugs em programas e emulação por

software.

Esta diversidade de cenários conduziu ao aparecimento de muitas e diferentes aplicações

capazes de realizar instrumentação de código com objectivos definidos e plataformas alvo

bem identificadas. Considerando o número de plataformas existentes, as dificuldades de

portabilidade entre elas, e o número de pequenas tarefas que se podem executar, é

claramente necessário desenvolver uma aplicação para instrumentação de código de

âmbito geral, evitando a escrita de uma infinidade de pequenos programas.

Em termos de bibliotecas de instrumentação, a Executable Editing Library (EEL) [Larus1995]

foi uma das primeiras a permitir a instrumentação estática de executáveis gerados a partir

de código C++. Os autores da EEL defendiam que a ideia de instrumentação de código é

conceptualmente simples mas extremamente complexa na prática, isto porque existe uma

miríade de detalhes arquitecturais específicos a cada sistema. É esta complexidade que

reduz a atractividade dos mecanismos de instrumentação de código. A EEL disponibiliza

abstracções que permitem que uma outra ferramenta analise e modifique executáveis sem

se preocupar com determinados conjuntos de instruções, múltiplos formatos de ficheiros

executáveis, com as consequências de apagar ou adicionar novas instruções. A EEL

contribuiu, de uma forma extremamente significativa, para facilitar o desenvolvimento de

ferramentas de depuração, tradução, protecção e medição da performance de programas.

Um aspecto importante de muitas linguagens de programação modernas é serem

compiladas para um formato intermédio e executadas por uma máquina virtual.

Tipicamente, este ambiente permite que o código seja carregado em tempo de execução e

posteriormente executado. Dois exemplos bem conhecidos são as plataformas JAVA

[ECMA2002;Lindholm1999] (que suporta o carregamento dinâmico de classes) e .NET

[ECMA2002;Lindholm1999] (que permite o carregamento e execução dinâmicas de

assemblies).

Page 25: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO PARA MÁQUINAS VIRTUAIS 11

Um efeito colateral do carregamento dinâmico é o de ser possível instrumentar o código,

antes da sua definição (load) na máquina virtual, introduzindo ou removendo instruções,

modificando a utilização e o acesso a classes, variáveis e constantes. O conceito chave é ser

possível modificar o código antes ou durante a sua execução, podendo estas

transformações ser operadas depois da compilação ou no carregamento do código.

O advento do uso generalizado de máquinas virtuais, que suportam carregamento

dinâmico de código, e em particular, JAVA, despoletou um enorme desenvolvimento no

campo da instrumentação de código. Surgiram diversas bibliotecas para JAVA, sendo duas

das mais importantes a Bytecode Engineering Library (BCEL) [Dahm1999], que é agora parte

integrante do projecto Apache e a JAVA Object Instrumentation Environment (JOIE)

[Cohen1998]. Conjuntamente, estas duas bibliotecas são utilizadas por mais de 37

projectos, que usam instrumentação de código para realizar as mais diversas tarefas.

Antes de enumerar e apresentar as mais importantes bibliotecas de instrumentação para

JAVA, são descritos nas subsecções que se seguem os diversos atributos que permitirão

classificar e distinguir essas bibliotecas. As bibliotecas pode ser classificadas de acordo

com:

• A capacidade realizar instrumentação estática ou dinâmica, i.e. serem somente

capazes de manipular as aplicações antes destas derem executadas ou já durante a

sua execução, respectivamente.

• O momento e o mecanismo utilizado para interceptar o carregamento de código.

A utilização de diferentes mecanismos pode limitar a utilização da biblioteca de

instrumentação visto que, as diferentes técnicas podem criar dependências da

biblioteca com plataforma, com o sistema operativo ou até com mecanismos

internos da máquina virtual, como os classloaders personalizáveis que permitem

controlar a forma como um programa é carregado.

• O nível do API de instrumentação, que pode ser de alto ou baixo nível. Dizemos

que um API é de baixo nível se a instrumentação de código obriga a manipular

directamente as estruturas existentes nos ficheiros .class, como por exemplo, a

tabela de constantes, esta tabela serve para guardar o valor de todas as constantes

existentes na execução de uma classe, as instruções que referenciam essas

constantes fazem-no por meio de índices para esta tabela. Num API de baixo

nível é o programador que tem de introduzir, remover, modificar e validar as

Page 26: Instrumentação de Código na Plataforma .NET

12 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

entradas na tabela de constantes. Um API diz-se de alto nível se todas as

estruturas existentes no formato dos ficheiros .class estão “escondidas” do

programador, ou seja a instrumentação de código lida com classes, métodos,

campos, referências entres estes objectos e não com tabelas de constantes, índices

para estas tabelas, etc.

2.1.1. Tipos de Instrumentação

Quanto ao tipo de instrumentação, as bibliotecas actuais podem ser classificadas em dois

grandes grupos:

• Com capacidade de realizar instrumentação estática;

• Com capacidade de realizar instrumentação dinâmica.

Uma biblioteca pertence ao primeiro grupo se somente permitir a instrumentação do

código de uma aplicação, antes deste ser executado pela primeira vez. Por outro lado, uma

biblioteca capaz de realizar instrumentação dinâmica de código, permite modificar classes

com objectos já instanciados.

Para facilitar a compreensão destes conceitos é útil ter em mente o ciclo de vida de um

programa em JAVA, desde da escrita do seu código fonte até à sua execução. De forma

simples e resumida: inicialmente o programa é escrito numa linguagem de alto nível; em

seguida esse código fonte é compilado para uma representação intermédia; a máquina

virtual efectua o carregamento dessa representação intermédia da aplicação em memória; e

em seguida encarrega-se da sua execução.

É possível classificar a instrumentação estática de código em três categorias:

instrumentação de código fonte; instrumentação em tempo de compilação; instrumentação

em tempo de carregamento ou execução.

A menos comum, devido à sua complexidade, é a instrumentação de código fonte. Na

realidade as ferramentas que permitem este tipo de manipulação são muito específicas

para uma determinada tarefa e muitas vezes desenvolvidas para uma única utilização,

como por exemplo, um pré-processador de código capaz de introduzir código de

monitorização da execução dentro de uma aplicação.

A instrumentação em tempo de compilação é muito frequente, esta ocorre durante a

compilação do código fonte para código intermédio. É possível identificar diversos

Page 27: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO PARA MÁQUINAS VIRTUAIS 13

compiladores devidamente adaptados para realizar transformações ao código original,

imprimindo melhoramentos e optimizações com os mais diversos objectivos, como por

exemplo, adicionar suporte para novas instruções à plataforma original (e.g. AspectJ

[Kiczales2001]).

A instrumentação em tempo de carregamento ocorre imediatamente antes do primeiro

carregamento do código na máquina virtual. Este é considerado o momento por eleição

para fazer a instrumentação de uma aplicação, pois já estão disponíveis muitas

informações que não são passíveis de ser obtidas em tempo de compilação e ainda não é

tão complicado fazer a substituição e modificação do código da aplicação como em tempo

de execução.

A instrumentação em tempo de execução seria perfeita se não fosse tão complexa. Com a

aplicação em execução já existe um conhecimento completo do ambiente virtual, dos

recursos que estão disponíveis, do perfil de execução da aplicação e de toda a informação

necessária ao funcionamento do programa. Nestas condições, a optimização de uma

aplicação seria muito mais vantajosa pois seria possível fazer uma afinação do código a

executar com base em valores actuais e não apenas em dados conhecidos antes da

execução. No entanto, não é trivial modificar a implementação de uma classe que já esteja

carregada no heap na máquina virtual pois algumas plataformas, como a JAVA e a .NET,

não o permitem. Nos capítulos seguintes desta dissertação, procura-se reduzir essa

complexidade.

2.1.2. Intercepção do Carregamento de Código

Todo o carregamento de classes na plataforma JAVA é implementado pela JAVA Virtual

Machine (JVM) [Lindholm1999] através do carregador de classes bootstrap nativo e pelas

classes descendentes de Java.lang.ClassLoader. O carregador de classes bootstrap é

responsável pelo carregamento das classes do sistema, ou seja, todas as classes que fazem

parte do JAVA Development Kit. ClassLoader é a super classe de todos os carregadores,

específicos de cada aplicação.

Os programadores de JAVA podem personalizar o carregamento das suas classes através

da implementação de uma subclasse de ClassLoader [Lian1998]. Estes carregadores

adaptados são utilizados por três razões:

• Personalização, de forma a satisfazer necessidades especiais das classes para o seu

carregamento ou para fazer o pré processamento dessas classes para, por

Page 28: Instrumentação de Código na Plataforma .NET

14 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

exemplo, introduzir código para traçar o perfil de execução de uma aplicação ou

validar as permissões de acesso a determinados recursos.

• Namespace, como cada carregador de classes personalizado utiliza o seu próprio

Namespace, quando diferentes cópias de uma classe são carregadas por diferentes

carregadores, elas vão residir em diferentes Namespaces, sendo tratadas pela JVM

como sendo diferentes classes.

• Só é possível eliminar uma classe do ambiente de execução através da utilização

de um ClassLoader.

Aceitando esta informação como ponto de partida, é possível estabelecer quatro tipos de

implementação diferentes para a intercepção do carregamento de classes em JAVA:

• Mecanismo de intercepção dependente da implementação de uma subclasse de

ClassLoader. É o mecanismo mais comum entre as bibliotecas de

instrumentação de código em JAVA e é também o mais simples de implementar.

No entanto, tem dois inconvenientes: o primeiro é não permitir a intercepção do

carregamento das classes do sistema; o segundo é não permitir a intercepção do

carregamento de aplicações que utilizem os seus próprios carregadores de classes

personalizados. Uma classe que seja carregada por uma subclasse de

ClassLoader, não pode ser novamente processada com o mesmo mecanismo.

• Mecanismo de intercepção dependente da implementação da JVM. Para

conseguir independência dos mecanismos de ClassLoader, Keller e Hölzle

[Keller1998] propuseram uma implementação dependente da JVM através da

substituição do carregador de classes bootstrap. Apesar de ser dependente da

JVM, por exigir a sua reimplementação, este mecanismo tem a vantagem de

permitir a intercepção do carregamento de classes do sistema.

• Mecanismo de intercepção dependente da plataforma. Para anular a dependência

da JVM, foi proposta uma abordagem não intrusiva, que evita a personalização

da JVM [Duncan1999] e consiste em substituir algumas das classes nativas do

sistema. Esta abordagem funciona em todas as JVM para uma determinada

plataforma mas, não é portável entre plataformas, por exigir a substituição das

bibliotecas nativas (e.g. ficheiros DLL), de acordo com o seu sistema operativo.

Page 29: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO PARA MÁQUINAS VIRTUAIS 15

• Mecanismo de intercepção genérico e portável. Este mecanismo é independente

da implementação da JVM e da plataforma e consiste em utilizar o mecanismo de

HotSwap [Sun2001]. Com recurso ao HotSwap, o método defineClass() da

classe ClassLoader base é substituído por uma nova uma nova implementação

que permite a intercepção do carregamento de código pelo programador

[Kniesel2001]. O inconveniente deste mecanismo em relação aos dois anteriores é

não permitir a intercepção do carregamento das classes de sistema.

Em conclusão, qualquer tentativa de intercepção baseada no mecanismo de

ClassLoader resulta num sistema dependente do ClassLoader e qualquer tentativa de

o conseguir através da substituição do carregador de classes bootstrap, resulta num sistema

dependente da JVM.

2.1.3. Níveis de Instrumentação de Código

As bibliotecas de instrumentação de código dividem-se em dois níveis de acordo com o

tipo de API que disponibilizam.

As bibliotecas de baixo nível são as mais poderosas, permitem modificar e manipular

todos os pormenores de uma aplicação JAVA desde as suas classes até aos atributos das

suas instruções de bytecode. O lado menos positivo desta abordagem é que, para poderem

fornecer tal versatilidade estas ferramentas são incapazes de tratar automaticamente a

resolução de referências, invalidadas pelas manipulações efectuadas. Isto obriga o

programador a tratar da recodificação dos índices e referências o que torna muito

complicada a tarefa do programador.

De forma inversa, existem ferramentas que sacrificam alguma versatilidade em

favorecimento da facilidade de utilização, são as bibliotecas de alto nível. Estas

disponibilizam um número limitado de operações, no entanto, compensam essa limitação

com a automatização das correcções a efectuar paralelamente à instrumentação principal.

A maioria da vezes, quando se pretende realizar uma modificação, o mais difícil é

compreender todos os efeitos colaterais que esta vai ter sobre o código e tomar as medidas

necessárias para evitar a ocorrência de erros. As bibliotecas de alto nível asseguram a

integridade do código de uma forma automática.

A grande maioria das ferramentas com APIs de alto nível só é capaz de realizar Reflexão

Estrutural [Ferber1989], isto é, apenas permitem obter informação e modificar a estrutura

da aplicação. Não é possível modificar o bytecode existente no corpo dos métodos. À

Page 30: Instrumentação de Código na Plataforma .NET

16 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

capacidade das bibliotecas de modificarem o comportamento dos métodos e de conhecer o

seu código dá-se o nome de Reflexão Comportamental [Malenfant1992].

As bibliotecas de alto nível são úteis porque permitem aos programadores com

conhecimentos limitados das estruturas internas das aplicações e classes JAVA ter acesso a

mecanismos de instrumentação de código.

2.2. Bibliotecas para a Plataforma JAVA A popularidade crescente da instrumentação de código em máquinas virtuais, em

particular na plataforma JAVA, conduziu ao aparecimento de diversas implementações de

bibliotecas para instrumentação de código. Esta secção apresenta um resumo das

bibliotecas mais importantes para esta plataforma.

2.2.1. Bytecode Engineering Library

A BCEL [Apache2003;Dahm1999] é actualmente utilizada por mais de 30 projectos,

inclusivé no projecto Apache, responsável pelo seu desenvolvimento. Esta biblioteca é

considerada um das mais poderosas e consequentemente uma das mais complexas de

utilizar. No entanto, resolve vários problemas comuns na manipulação de classes JAVA.

Um dos problemas comuns a todas as ferramentas de instrumentação é o da desmontagem

e consequente montagem dos ficheiros .class. A BCEL resolve este problema

transformando a sequência de bytes que compõem o ficheiro numa estrutura de objectos.

Após a conclusão da instrumentação, a BCEL trata da geração de uma nova sequência de

bytes, para um novo ficheiro .class. A representação de todos os membros da classe por

uma estrutura de objectos é composta por uma família de instâncias de classes, que

representam cada componente até ao nível das instruções. Isto significa que existe um

objecto para cada tipo de instrução bytecode.

Com esta abordagem, todos os detalhes da montagem e desmontagem das classes estão

escondidos do utilizado; ele apenas manipula a estrutura de objectos.

A BCEL é uma biblioteca de baixo nível e, por isso mesmo, oferece poderosas capacidades

de manipulação/instrumentação de código. No entanto, o programador tem de pagar o

preço desta versatilidade. No caso da BCEL, isso corresponde, por exemplo, a ter de

recalcular manualmente todos os índices para tabela de constantes, quando estes se tornam

Page 31: Instrumentação de Código na Plataforma .NET

BIBLIOTECAS PARA A PLATAFORMA JAVA 17

inválidos por causa da modificação ou substituição da tabela. Mesmo assim, a BCEL não

fornece a possibilidade de remover constantes da tabela, apenas de as adicionar ou trocar.

A manutenção da integridade nos índices relativos das instruções dentro de cada método é

outro problema associado à instrumentação de código. Estes índices são armazenados

como sendo o número de bytes entre o início de uma instrução e o início de outra. A BCEL

resolve este problema pois transforma esses índices em referências para objectos, dentro da

estrutura que representa a classe. Na fase de montagem do ficheiro .class, a BCEL volta

a converter essas referências em número de bytes. Assim o utilizador não tem de recalcular

estes índices cada vez que uma instrução é removida ou adicionada.

A BCEL também fornece um método capaz de recalcular o tamanho máximo da stack em

cada método, depois deste ter sido instrumentado. Isto é essencial pois este valor é global à

classe e é diferente consoante a instrumentação realizada. A BCEL utiliza um algoritmo de

controlo de fluxo para simular a dimensão da stack ao longo de cada método.

Esta biblioteca utiliza o mecanismo dependente de ClassLoader para realizar a

intercepção do carregamento das classes, pelo que é capaz de fazer instrumentação estática

de classes em tempo de carregamento.

2.2.2. SERP

A biblioteca SERP [White2002] utiliza uma abordagem similar à da BCEL, baseando-se

nume representação dos componentes de cada classe por uma estrutura de objectos. No

entanto, a SERP utiliza muito menos objectos que a BCEL para representar as pouco mais

de 200 instruções de bytecode. A título de exemplo, podemos dizer que a SERP usa apenas

uma classe, a MathInstruction, para representar todas as instruções de aritmética,

enquanto que a BCEL para o mesmo propósito implementa várias classes (e.g. IADD, ISUB,

IMUL, DADD, DSUB).

A SERP resolve os problemas de, montagem e desmontagem de ficheiros .class, dos

índices relativos dentro dos métodos das instruções e do cálculo do valor máximo da stack,

da mesma forma que a BCEL. A SERP, no entanto, tem uma solução melhor para a gestão

da tabela de constantes. O programador já não tem de recalcular os índices para esta tabela

pois pode passar o valor da constante directamente como um parâmetro para a instrução.

A SERP também fornece métodos para manipular a tabela de constantes na sua totalidade,

enquanto que, a BCEL só permite adicionar novas constantes. No entanto, assim como o

Page 32: Instrumentação de Código na Plataforma .NET

18 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

BCEL, a SERP também não elimina da tabela de constantes as entradas que já não são

utilizadas. A SERP ainda é considerada uma biblioteca de baixo nível.

2.2.3. JAVA Object Instrumentation Environment

A JAVA Object Instrumentation Environment (JOIE) [Cohen1998] é considerada uma das

primeiras bibliotecas de instrumentação de código para JAVA e, na verdade, é bastante

semelhante no seu funcionamento à BCEL. A JOIE foi a primeira a representar os

componentes das classes utilizando objectos até ao nível das instruções bytecode e utilizava

também as mesmas soluções que a BCEL para os problemas anteriormente enunciados.

Infelizmente a JOIE deixou de ser mantida, impossibilitando o seu download para uma

análise mais profunda.

2.2.4. ASM

A ideia fundamental da arquitectura do ASM [Bruneton2002] é não utilizar o mesmo tipo

de estrutura de objectos para representar os componentes de uma classe, utilizada pelas

suas congéneres. Este tipo de abordagem (orientada aos objectos) requer um grande

número de classes para construir a árvore que representa a estrutura de uma classe. A

título de exemplo, é possível observar que a BCEL tem cerca de 270 classes projectadas

para este fim e a SERP 80, isto é importante pois diminuiu drasticamente a complexidade

para o programador. A decisão da não adoptar uma representação OO prende-se também

com o facto do principal objectivo de desenvolvimento deste projecto ser produzir uma

ferramenta leve e de grande performance.

A solução encontrada para manter uma dimensão reduzida foi implementar um padrão de

software visitor [Gamma1995], à semelhança dos existentes em outras bibliotecas como a

BCEL e a SERP, mas sem representar explicitamente a árvore de objectos visitados. Para

facilitar a compreensão desta abordagem é apresentado o seguinte exemplo,

disponibilizado no site web da ASM:

Page 33: Instrumentação de Código na Plataforma .NET

BIBLIOTECAS PARA A PLATAFORMA JAVA 19

Para gerar o código binário da interface visível na Listagem 2.1-a), utilizando a ASM, seria

necessário executar o código presente na Listagem 2.1-b). É possível observar que para

criar uma classe com dois métodos, em vez de três objectos (um para representar a classe e

um para representar cada método), como seria necessário utilizando a BCEL, apenas são

utilizados dois: um ClassWriter e um CodeVisitor. Posteriormente, para criar a

interface, é necessário chamar os métodos do Visitor existentes para cada objecto.

Os problemas de serialização e descerealização não se colocam na ASM, assim como o

problema da gestão da tabela de constantes, pois a ASM esconde totalmente esta estrutura,

a) public interface Notifier { void notify( String msg); void addListener( Listener observer); } -------------------------------------------------------------- b) import org.objectweb.asm.ClassWriter; import org.objectweb.asm.CodeVisitor; import org.objectweb.asm.Constants; public class NotifierGenerator implements Constants { ... ClassWriter cw = new ClassWriter(false); cw.visit( ACC_PUBLIC+ACC_ABSTRACT+ACC_INTERFACE, "asm1/Notifier", // nome da classe "java/lang/Object", // super classe null, // interfaces "Notifier.java"); // ficheiro de código fonte CodeVisitor cv; cv = cw.visitMethod( ACC_PUBLIC+ACC_ABSTRACT, "notify", // nome do método "(Ljava/lang/String;)V", // descritor do método null, // excepções null); // atributos cv = cw.visitMethod( ACC_PUBLIC+ACC_ABSTRACT, "addListener", // nome do método "(Lasm1/Listener;)V", // descritor do método null, // excepções null); // atributos cw.visitEnd(); byte[] bytecode = cw.toByteArray();

Listagem 2.1 – a) Definição do Interface Notifier; b) Código ASM para gerar o Interface Notifier

Page 34: Instrumentação de Código na Plataforma .NET

20 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

não permitindo o acesso directo às suas entradas. Para manipular um campo de uma

classe, o programador tem de utilizar o método visitFieldInst, sendo que os seus

parâmetros (nome e tipo do campo) se encontram no formato String e não no formato de

índices para a tabela de constantes. Por outro lado, a ASM não permite a resolução do

problema dos índices relativos para as instruções com a utilização de referências para

objectos, como acontece na BCEL, pois não utiliza o mesmo tipo de representação OO. Na

ASM foram introduzidos os labels. Isto é, quando uma instrução referencia outra (como por

exemplo num jump), a instrução referenciada é marcada como sendo um label e o

parâmetro do jump é a identificação desse label.

A ASM também utiliza o mecanismo de ClassLoaders personalizados para realizar a

intercepção do carregamento das classes.

2.2.5. Javassist

A biblioteca Javassist [Chiba2000] é capaz de realizar instrumentação de código em tempo

de compilação ou de carregamento se existir um ClassLoader personalizado para o

efeito. Esta biblioteca também utiliza o mesmo tipo de estrutura OO que a BCEL ou a SERP

para representar a organização e os componentes de cada classe, no entanto recorre a um

nível de abstracção superior. O API da Javassist foi projectado para que o programador

utilize vocábulos associados a linguagens de programação de alto nível, como classe e

método. Em oposição, a BCEL obriga a conhecer vocabulário de baixo nível, como tabela

de constantes (constant pool) e chamadas a nível de instrução (e.g. invokevirtual).

Inicialmente, esta abordagem foi uma limitação, pois o Javassist só permitia reflexão

estrutural. Actualmente este problema já foi ultrapassado e já é possível manipular o corpo

dos métodos (reflexão comportamental).

No Javassist, um dos conceitos com maior relevância é o de reyfication. Este significa que se

modificarmos o fluxo de execução de um programa num determinado momento, por da

instrumentação de código, o contexto de execução desse método é passado para o código

de instrumentação como um objecto (i.e. algo abstracto como uma chamada a um método é

convertido em algo concreto como um objecto que a representa). A reflexão, neste caso, é a

repercussão no código original, da modificação destes objectos de contexto, no código da

instrumentação. Como estas operações estão revestidas de grandes custos no tempo de

execução dos programas, o Javassist fornece um compilador de código JAVA que permite

optimizar o bytecode gerado para a aplicação e para a instrumentação. Esta optimização

Page 35: Instrumentação de Código na Plataforma .NET

BIBLIOTECAS PARA A PLATAFORMA JAVA 21

funciona através da eliminação de todas as partes das operações de reyfication e de reflexão

que não são obrigatórias para a execução do programa. Isto é algo que não é possível fazer

noutros sistemas reflectivos [Welch1999;Welch2000] e que provoca grandes overheads na

execução.

2.2.6. Binary Component Adaptation

O Binary Component Adaptation (BCA) [Keller1998] é um sistema pensado para permitir a

modificação de componentes de programas o mais tarde possível. Este sistema actua

instrumentando as classes das aplicações JAVA mesmo antes destas serem carregadas na

JVM.

O sistema baseia-se no seguinte: Primeiro, como é visível na Figura 2, o código fonte dos

programas é escrito em JAVA e compilado para a sua forma binária. Paralelamente, ou em

qualquer outra altura, o programador escreve numa linguagem que estende a linguagem

JAVA, o código com as modificações a realizar na aplicação, sendo este código compilado

para um ficheiro denominado de Delta File, utilizando um compilador próprio. Finalmente,

quando a aplicação está para ser carregada, a mesma é interceptada por um modificador

que, através da informação disponível na Delta File, aplica as transformações pretendidas à

aplicação e faz o seu carregamento na JVM.

A intercepção do carregamento das aplicações é conseguida através da substituição do

carregador de classes bootstrap e não da personalização de um ClassLoader, como é feito 1 Imagem retirada de [Keller1998]

Figura 2 – Panorâmica do sistema BCA1

Page 36: Instrumentação de Código na Plataforma .NET

22 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

pelas bibliotecas apresentadas anteriormente. O problema desta implementação é a sua

dependência de uma JVM modificada. Para eliminar esta dependência, os autores fizeram

uma nova versão não intrusiva do sistema, através da substituição das bibliotecas de

sistema (ficheiros DLL). Ao eliminaram a dependência da JVM, introduziram a

dependência à plataforma (sistema operativo).

O BCA só permite um número muito reduzido de transformações, como por exemplo a

adição e renomeação de métodos e campos, a extensão de interfaces, a mudança de

heranças e hierarquias. Conclui-se que o BCA só permite, em ultima análise, a

implementação de mecanismos de reflexão estrutural.

2.2.7. JMangler

A JMangler [Kniesel2001] foi a primeira biblioteca com um método de intercepção de

carregamento de classes independente da JVM, da plataforma e de um ClassLoader

personalizado. A técnica apresentada na JMangler utiliza o mecanismo de HotSwap da

máquina virtual. Esta técnica consiste em dar em tempo de execução uma nova

implementação ao método defineClass() da classe ClassLoader base. O método

defineClass() é responsável pela definição das classes no heap da JVM e a nova

implementação fornecida vai permitir controlar ou modificar o carregamento de qualquer

classe no ambiente de execução.

Page 37: Instrumentação de Código na Plataforma .NET

BIBLIOTECAS PARA A PLATAFORMA JAVA 23

A Figura 3 apresenta um resumo visual dos métodos de intercepção do carregamento de

classes mencionados até ao momento. No topo desta figura estão bibliotecas como a JOIE,

a BCEL e a Javassist, que utilizam o método de personalização de ClassLoaders. Ao nível da

JVM aparece a BCA, que faz a substituição da classe bootstrap do carregamento de classes

na JVM. No centro está a JMangler.

O mecanismo de HotSwap da JVM permite modificar a implementação de uma classe

tempo de execução. A JMangler aproveita essa funcionalidade para modificar a classe pai

do mecanismo de ClassLoader, através da substituição do método defineClass()

responsável pelo carregamento das classes no heap da JVM, por uma implementação que

permite a controlar o carregamento dessas classes.

A JMangler, só por si, não é capaz de realizar instrumentação de código. No entanto,

fornece uma interface JAVA que permite interligar consigo própria qualquer biblioteca de

instrumentação de código, como por exemplo a BCEL, a JOIE ou a Javassist. A JMangler

permite que estas bibliotecas usufruam das suas capacidades de intercepção do

carregamento de código.

1 Imagem retirada de [Kniesel2001]

Figura 3 – Resumo dos mecanismos de intercepção do carregamento de classes na JVM1

Page 38: Instrumentação de Código na Plataforma .NET

24 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

2.2.8. Twin Class Hierarchy Approach

Todas as bibliotecas descritas anteriormente têm um problema comum. Em JAVA existe

um conjunto de classes chamadas classes de sistema, que são utilizadas por todos os

programas escritos em JAVA. A classe base, no topo de qualquer hierarquia de classes, é a

classe java.lang.Object.

Estas classes são normalmente difíceis ou até mesmo impossíveis de instrumentar. Isto

porque, se modificadas, a utilização das suas versões instrumentadas pelo código que

realiza a instrumentação é quase inevitável quando se trata de instrumentação dinâmica.

Isto seria equivalente a “Instrumentar a Instrumentação”, conduzindo a comportamentos

erróneos ou excepcionais.

A Twin Class Hierarchy Approach (TCH) [Factor2004] propõe uma abordagem que evita este

problema e permite que as classes do sistema sejam, efectivamente, instrumentáveis. O

processo consiste em renomear as classes modificadas e em mudar as referências a classes

de sistemas por referências para as classes modificadas. Por exemplo, na Figura 4 é visível

a hierarquia original de classes de sistema em JAVA, e na Figura 5 a mesma hierarquia mas

baseada nas classes modificadas pela aplicação do TCH.

Nos diagramas, os elementos a cinzento representam as classes originais de sistema, e os

brancos as classes modificadas. É visível que as classes instrumentadas têm o seu namespace

modificado pela adição do prefixo “TCH”. Inicialmente, a classe java.lang.Object é

Figura 4 – Classes de sistema em JAVA

Page 39: Instrumentação de Código na Plataforma .NET

BIBLIOTECAS PARA A PLATAFORMA JAVA 25

modificada e é gerada a classe TCH.java.lang.Object a partir dessa modificação. Esta

nova classe vai assumir o papel de pai de toda a hierarquia de classes de sistema

modificadas, à excepção das classes descendentes de java.lang.Throwable. Isto

acontece porque o ambiente de execução da plataforma JAVA só permite que sejam

geradas excepções de tipos descendentes da classe java.lang.Throwable.

Este mecanismo resolve o problema do código de instrumentação sofrer o efeito das

modificações que provoca, mantendo as referências dentro de si para as classes originais

do sistema. No entanto, este mecanismo levanta outros problemas, como por exemplo,

todo o código de tratamento de excepções passa a lançar ou apanhar excepções dos tipos

modificados, ignorando as excepções dos tipos originais. Este problema é facilmente

resolvido, duplicando o tipo de excepções apanhadas. Assim, cada bloco de tratamento de

excepções deverá passar a lidar, não só com a classe modificada, mas simultaneamente

com a original.

O TCH permite a utilização de qualquer uma das bibliotecas apresentadas anteriormente

para a realização da instrumentação de código, herdando as características de intercepção

do carregamento de código dessas bibliotecas.

Figura 5 – Classes de sistema em JAVA após a transformação TCH

Page 40: Instrumentação de Código na Plataforma .NET

26 CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO

2.3. Instrumentação de Código em .NET A plataforma .NET fornece uma interface para a realização de introspecção sobre as

aplicações. Um programa que utilize o API disponibilizado é capaz de saber como é

constituído e qual a sua estrutura. No entanto, não é capaz de aceder ao seu código IL nem

de modificar o seu comportamento.

As bibliotecas desta plataforma também permitem gerar código e programas em tempo de

execução, usando o API System.Reflection.Emit [Microsoft2004b]. Esta biblioteca

disponibiliza mecanismos para gerar código IL, métodos, campos e classes, em tempo de

execução, e construir um programa ou uma biblioteca (DLL) em memória, podendo este ser

também guardado em disco. Em qualquer dos casos, é possível executar o novo programa

ou permitir o acesso aos seus componentes por outras aplicações, como se de um

programa comum se tratá-se (um programa gerado por compilação de código fonte).

Apesar destas capacidades de introspecção e geração de código em tempo de execução,

não podemos afirmar que a plataforma .NET seja uma plataforma totalmente reflexiva. Isto

porque a definição de reflexão obriga a que os programas, além de conhecerem a sua

estrutura, também devem ser capazes de conhecer o seu comportamento e realizar

modificações, tanto sobre a sua estrutura como sobre o seu comportamento. Em .NET,

como vimos, é possível conhecer a estrutura dos programas e gerar novos programas. No

entanto, não possível partir de um programa existente, modificar a sua

estrutura/comportamento e gerar este programa novamente. É desta lacuna que nasce a

necessidade de desenvolver bibliotecas de instrumentação de código para .NET.

2.3.1. API de Perfilagem de Programas

Não é inteiramente correcto que os programas.NET não são capazes de conhecer o seu

próprio comportamento. Apenas não são capazes de o fazer utilizando mecanismos

geridos pelo ambiente de execução (managed execution environment), como o API

disponibilizado pelas classes da plataforma (System.Reflection) [Gough2001], que só

permitem chegar até ao nível da assinatura dos métodos. Existe, no entanto, um API não

gerido, de perfilagem [Microsoft2005b], que permite a um programa saber como é

constituído até ao nível do código IL.

Este API de perfilagem dá a conhecer o código de um programa e consegue mesmo

manipular esse código, dentro de certos limites. No entanto, a sua natureza “insegura” que

Page 41: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE CÓDIGO EM .NET 27

escapa à alçada do ambiente de execução, não dá garantias suficientes ao obrigar misturar

código gerido com código não gerido dentro da mesma aplicação.

Page 42: Instrumentação de Código na Plataforma .NET
Page 43: Instrumentação de Código na Plataforma .NET

Instrumentação de Código em .NET

“O mais importante de uma linguagem de programação é o nome. Uma linguagem não terá sucesso sem um bom nome. Eu recentemente descobri um nome muito bom e agora ando à procura de uma linguagem adequada.”

— Donald Knuth

“Para se conseguir criar uma maçã do nada, tem de se criar o universo primeiro.”

— Carl Sagan

Neste capítulo será apresentada a biblioteca RAIL que permite realizar instrumentação de

código na plataforma .NET. O capítulo inicia-se com a descrição do formato dos

programas na plataforma .NET, os assemblies, e dos ficheiros que os constituem. É discutida

a forma como o ambiente de execução carrega/executa os programas e são também

enumerados os mecanismos mais importantes deste processo.

Em seguida, é discutida a arquitectura da biblioteca RAIL. A secção inicia-se como uma

descrição das principais camadas da mesma, sendo depois aprofundadas as

funcionalidades patentes em cada uma. A concluir a secção são enumeradas algumas das

funcionalidades de alto nível que a biblioteca oferece ao programador. O capítulo termina

com a descrição do trabalho relacionado.

Capítulo

3

Page 44: Instrumentação de Código na Plataforma .NET

30 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

3.1. Introdução Não existe para a plataforma .NET uma biblioteca de instrumentação de código tão

avançada como as bibliotecas de JAVA apresentadas na secção 2.2. Como poderá ser

observado na secção de trabalho relacionado existente no final deste capítulo, a maior

parte do trabalho existente centra-se na leitura ou na produção de assemblies, havendo

pouco trabalho foi feito em bibliotecas capazes de ler e escrever simultaneamente. Entre as

bibliotecas capazes de realizar escrita e simultaneamente leitura, nenhuma esconde a

complexidade das estruturas dos programas do programador, pelo que podem ser

classificadas como bibliotecas de baixo nível.

A falta de uma biblioteca de alto nível para instrumentação de código em .NET, a não

abordagem da questão da intercepção do carregamento de código por nenhum dos

projectos existentes e a falta de mecanismos de instrumentação de código na plataforma,

foram a principais razões que motivaram o desenvolvimento de uma nova biblioteca para

instrumentação de código na plataforma .NET. Esta biblioteca, fruto da presente

dissertação, foi baptizada de Runtime Assembly Instrumentation Library (RAIL)

[Cabral2005;DSG-CISUC2005].

3.2. Execução em .NET Um programa em .NET é vulgarmente denominado de assembly [ECMA2002]. Um

assembly, como o próprio nome indica, é o resultado da junção de diversos componentes e

é a unidade básica de carregamento na plataforma .NET. O assembly é composto por um

manifesto (informação sobre o próprio assembly que permite a sua execução e integração

com outros assemblies); por um ou mais módulos, a que correspondem diferentes ficheiros;

e por um conjunto opcional de recursos. Os recursos são as imagens e todas as informações

complementares ao programa. Uma das características dos assemblies é a possibilidade de

poderem existir simultaneamente, dentro do mesmo ambiente de execução, várias versões

de cada um.

Quando associado à programação COM o termo componente assumia dois sentidos: o de

uma classe COM e o de um módulo COM (DLL ou EXE). O conceito de assembly veio

eliminar esta bi-paridade. Em .NET um assembly é um componente de software, plug-and-

play, semelhante a um componente de hardware. O manifesto de um assembly contém a

informação que descreve o assembly, como por exemplo, a sua identidade, a lista de

Page 45: Instrumentação de Código na Plataforma .NET

EXECUÇÃO EM .NET 31

ficheiros que o compõem, as referências para assemblies externos, as classes exportadas, os

recursos exportados e a informação sobre as permissões de acesso e segurança.

Existem quatro tipos de assemblies em .NET:

• Assemblies Estáticos – São os ficheiros PE criados pela compilação de código fonte

utilizando um dos compiladores da plataforma.

• Assemblies Dinâmicos – São criados em tempo de execução utilizando a biblioteca

System.Reflection.Emit e só existem em memória.

• Assemblies Privados – São Assemblies Estáticos utilizados apenas por uma

determinada aplicação.

• Assemblies Públicos ou Partilhados – Possuem um nome único e podem ser

utilizados por qualquer aplicação.

De forma a funcionarem como componentes é importante poder identificar um assembly de

uma forma unívoca. Para isso foram incluídas na plataforma .NET certas regras de

segurança que garantem a unicidade do nome de cada assembly e até de cada método. Um

método possui uma identidade unívoca visto que possui uma assinatura única dentro de

uma classe. Esta classe, por seu lado, é designada por um nome e um namespace e pertence

a um assembly que é, como já foi referido, identificado univocamente. É obrigatório que

todos os assemblies partilhados estejam assinados por um par de chaves pública e privada.

Assim, sempre que se cria um assembly, devem ser referidas estas chaves de forma a incluir

no manifesto do assembly o valor de um hash. Este valor é depois verificado pelo CLR para

validar a identidade do assembly referenciado e verificar se o assembly pode ter acesso a

determinados recursos ou fazer/receber chamadas de outros assemblies. Para que o CLR

consiga recalcular o hash, a chave pública é também integrada no assembly. A identidade de

um assembly é obtida a partir da informação do seu nome, número de versão, culture

(código da língua) e chave pública.

A capacidade de se referenciar um assembly univocamente e de existirem simultaneamente

diferentes versões do mesmo assembly vem colocar um fim no chamado “Inferno das DLL”,

assim como, na utilização abusiva do registo do sistema operativo Windows. A plataforma

.NET permite que as diferentes versões de um assembly possam ser executadas

simultaneamente, no mesmo sistema e até no mesmo processo. O único senão é que estes

assemblies têm obrigatoriamente de ser assemblies partilhados e tem de estar registados no

Page 46: Instrumentação de Código na Plataforma .NET

32 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Global Assembly Cache (GAC), usando uma ferramenta como a .NET Global Assembly Cache

Utility para efectuar esse registo.

Os ficheiros Portable Executable (PE) são ficheiros executáveis dentro do ambiente

Windows, este formato de ficheiro foi também adoptado para os assemblies da plataforma

.NET, assim não foi necessário modificar o sistema operativo para executar programas

.NET. O PE é um formato derivado do Microsoft Common Object File Format (COFF), sendo a

especificação de ambos os formatos pública. As razões que levaram à adopção do formato

PE para guardar aplicações .NET foram a capacidade do sistema operativo Windows já

saber como ler e executar ficheiros DLL/EXE e o formato COFF ser modular, permitindo a

inclusão de novas secções.

Os ficheiros PE comuns estão divididos em duas secções: a primeira contém os cabeçalhos

PE/COFF que referenciam os conteúdos do ficheiro e permitem ao sistema operativo a

interpretação dos mesmos; a segunda contém um número finito de subsecções menores

chamadas de secções de imagem nativas (.data, .rdata, .rsrc e .text). É nesta

segunda zona que os compiladores da plataforma .NET guardam os dados necessários aos

seus executáveis.

Na Figura 6 é visível que a Microsoft adicionou ao formato clássico do PE o cabeçalho e as

secções de dados para o CLR. O cabeçalho CLR contém informação que indica que o

ficheiro é um executável da plataforma .NET e a secção de dados contém a metadata e o

código IL necessários para determinar o que o programa irá fazer.

Figura 6 – Formato de um ficheiro PE

Page 47: Instrumentação de Código na Plataforma .NET

EXECUÇÃO EM .NET 33

A primeira secção, logo após os cabeçalhos PE/COFF, está marcada com as flags Code e

Execute Read, que indicam ao carregador de aplicações do sistema que esta secção

contém código para ser executado. Nesta secção está o cabeçalho do CLR onde é

referenciada a função chamada _CorExeMain, implementada no assembly mscoree.dll,

que inicia uma nova fase da execução gerida pelo o ambiente virtual do CLR. Quando o

carregador do sistema operativo encontra esta chamada ao mscoree.dll e executa o

método _CorExeMain está na realidade a iniciar a máquina virtual, a partir deste

momento é o CLR que controla toda a execução e interpreta o ficheiro PE como sendo um

assembly. É este o “truque” que evitou a realização de modificações ao Windows que lhe

permitissem acomodar executáveis .NET.

No momento em que o CLR começa a executar o assembly, começa também a interpretar a

metadata existente no PE. A metadata é a informação interpretável por uma máquina sobre

um determinado recurso, sendo vulgarmente designada como “informação sobre a

informação”. A metadata inclui a definição de tipos/classes, métodos, campos,

propriedades, atributos, eventos, identificação de versões, referências para assemblies

externos e outras informações necessárias para a execução.

A secção da metadata no assembly é iniciada por um número mágico, informação sobre

versões e outros dados relevantes, seguida pelo número de streams, i.e. sequências

ordenadas de dados binários, existentes nesta secção e um conjunto de cabeçalhos que

identificam o offset para o inicio de cada uma das streams. Estas streams contêm os heaps que

são zonas especiais devidamente organizadas dentro das streams e caracterizadas pelo tipo

de dados que contêm, os heaps guardam as strings de utilizador, as strings de

identificadores, os dados no formato binário (e.g. as assinaturas dos métodos, campos e

classes), os identificadores universais (GUID) e todas as tabelas da metadata. É comum

existirem cinco streams:

• “#Strings” – heap onde são guardadas as strings contento identificadores.

• “#US” – heap onde são guardadas as strings inseridas pelo programador.

• “#Blob” – heap onde são guardadas as “bolhas” de informação como as

assinaturas de métodos, campos e classes.

• “#GUID” – heap onde são guardados os diversos GUID associados aos

programas.

Page 48: Instrumentação de Código na Plataforma .NET

34 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

• “#~” – heap onde se encontram as tabelas de metadata.

A metadata encontra-se organizada em tabelas, em que cada registo contém informação

organizada em colunas bem definidas e devidamente especificadas nos documentos da

ECMA. Muitas vezes estas tabelas referenciam nos seus registos outras tabelas. Estas

referências, normalmente codificadas, assumem o formato de tokens. Estes tokens são o

resultado da concatenação do código em hexadecimal associado à identificação de uma

tabela com o índice do registo referenciado nessa tabela (Figura 7). Dependendo do

número de tokens utilizado, estes podem estar ou não comprimidos num formato próprio

da plataforma de forma a para poupar espaço. Deve ter-se em conta que a plataforma .NET

é fortemente orientada para aplicações web, daí a dimensão dos componentes (assemblies)

ser controlada para diminuir os tempos de transmissão destes pela rede.

A plataforma .NET privilegia a arquitectura baseada em componentes (assemblies) e para

uma aplicação poder integrar um determinado componente precisa de saber exactamente o

que é que esse componente contém e como pode ser utilizado. É na metadata que as

aplicações, o CLR e outras ferramentas encontram a informação que necessitam para

realizar a integração desses componentes.

O CLR utiliza a metadata para assegurar a aplicação de regras de segurança, realizar a

serialização inter contextos, construir uma imagem do programa em memória e executar as

aplicações. O carregador de classes do CLR usa a metadata para descobrir e ler as classes

dos programas em .NET. É na metadata que o ambiente de execução encontra a informação

detalhada sobre uma determinada classe e sobre o assembly em que esta se encontra (pode

ser no mesmo ou num exterior). O compilador Just-in-Time (JIT)

[Aycock2003;McCarthy1960] usa a metadata para traduzir o código IL para código nativo.

Muitas instruções em IL referenciam elementos da metadata utilizando tokens (e.g. uma

instrução callvirt possuí como parâmetro um token para a tabela de referências a

métodos externos ou definições de métodos internos), pelo que a metadata é essencial desde

o momento do carregamento do código até à sua execução.

Figura 7 – Tabelas da metadata

Page 49: Instrumentação de Código na Plataforma .NET

EXECUÇÃO EM .NET 35

3.2.1. Execução

Na secção anterior, foi descrita a estrutura e formato dos programas em .NET. Nesta

secção será discutida a forma como é feito o carregamento dos assemblies no CLR e a sua

execução.

A Figura 8 ilustra o mecanismo de execução de aplicações no CLR e identifica todos os

intervenientes, excepto o carregador de ficheiros PE do sistema operativo, no processo de

carregamento e execução de um ficheiro PE. Como é visível nesta figura, os componentes

principais do ambiente de execução são o carregador de classes, o verificador de

tipos/classes, o compilador JIT, o Garbage Colector, o gestor de excepções, o gestor de debug

e o gestor de threading. O PE, antes de ser executado, tem de passar por todos estes

componentes: primeiro pelo carregador de classes, que coloca uma imagem do assembly

em memória; em seguida pelo verificador de classes/tipos, que testa se as classes e o seu

código são type-safe; depois pelo JIT, que faz a tradução de código IL para código máquina

e finalmente é executado.

Após o carregamento do PE pelo sistema operativo a execução passa para o controlo do

CLR quando é invocado o método _CorExeMain, como foi explicado na secção anterior.

O CLR verifica qual é o ponto (método) de entrada na aplicação (normalmente o método

Figura 8 – Carregamento de aplicações no CLR

Page 50: Instrumentação de Código na Plataforma .NET

36 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Main) e procura executá-lo. No entanto, para poder executar qualquer método, o CLR

precisa encontrar e carregar a classe que o implementa. Essa função é desempenhada pelo

carregador de classes do CLR. Este componente é utilizado sempre que uma classe é

referenciada pela primeira vez durante a execução.

O carregador de classes lê as classes para memória e prepara-as para a execução. As classes

são localizadas procurando o assembly que as implementa. A localização deste assembly é

obtida através da consulta, em primeiro lugar, do ficheiro de configuração da aplicação

(.config), na mesma directoria desta, e em seguida no GAC e na metadata existente no

ficheiro PE. Podem existir simultaneamente no sistema várias versões da mesma classe

pelo que o mecanismo de carregamento deve assegurar que está a carregar a classe

correcta.

Depois de localizar a classe, o carregador regista a informação necessária para não ter de a

localizar novamente. Simultaneamente, o carregador de classes calcula o espaço a reservar

em memória para guardar uma instância desta classe. É adicionado um pequeno bloco de

informação a cada método da classe que reportará qual o estado da compilação JIT do

método (se já ocorreu ou não) e que servirá também para guardar a informação necessária

para a transição entre código gerido e não gerido. Se a classe referenciar outras classes

ainda não carregadas, estas são localizadas. Caso contrário, é utilizada a metadata adequada

para inicializar as variáveis estáticas e instanciar um objecto da classe desejada.

Um aspecto chave da plataforma .NET é ser Type Safe, o que significa que o CLR possui

mecanismos que asseguram que uma determinada classe está a ser usada/referenciada da

forma correcta e que o código a ela associado vai ser executado sem problemas de

atribuição de valores ou chamadas inválidas a métodos. O mecanismo responsável por

assegurar que um programa é Type Safe é o verificador de código do CLR.

O verificador do CLR é responsável por validar a metadata no assembly carregado e a Type

Safeness do código, através da validação do uso correcto dos métodos (confirmação das

assinaturas). O verificador é chamado depois da classe ser carregada e antes da execução

do código IL. Isto faz com que o verificador seja parte integrante do compilador JIT, sendo

este accionado sempre que um método é invocado. No entanto, a verificação do código é

opcional e o código marcado como trusted é passado directamente para o JIT sem ser

validado.

Page 51: Instrumentação de Código na Plataforma .NET

37

O JIT, por seu lado, assume o papel de maior relevância dentro do CLR porque os

assemblies contêm metadata e código IL e não código nativo. Desta forma, para que os

componentes de suporte à execução possam efectivamente executar os assemblies, estes têm

de ser compilados para código nativo gerido. Por razões performance esta compilação só

ocorre da primeira vez em que o método é invocado, sendo o resultado guardado em cache

para posterior utilização. O código só é eliminado da cache quando o Garbage Colector o

entender ou quando o processo terminar. Duas vantagens óbvias dos compiladores JIT são:

optimizar o código para plataformas diferentes; e tornar as aplicações executáveis em

plataformas distintas.

O JIT, para além de gerar código nativo em tempo de execução, utiliza a metadata e o

código IL para gerar informação, que servirá para a máquina virtual da plataforma

controlar a execução dos programas, gerir as excepções e a stack do mesmo, realizar

verificações de segurança e executar o Garbage Colector.

O CLR possui vários mecanismos de suporte à execução, alguns, como o Garbage Colector,

os mecanismos de tratamento de excepções, o verificador de regras de segurança e o

suporte para debug, que já foram mencionados anteriormente. No entanto, o CLR ainda

fornece alguns outros serviços, sendo de sublinhar a Interoperabilidade do Código. Este

mecanismo permite realizar chamadas a métodos não geridos, como os disponibilizados

pelo API do Windows e pelos objectos COM, a partir de código gerido. A Interoperabilidade

do Código é implementada no CLR através dos packages COM Interop e Platform Invoke

(P/Invoke).

Page 52: Instrumentação de Código na Plataforma .NET

38 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

3.3. Arquitectura A metodologia seguida na RAIL (Figura 9) consiste em carregar uma imagem binária do

assembly em memória e extrair deste toda a informação contida na metadata e no código IL.

Com esta informação é construída uma estrutura de objectos em que cada objecto

representa um dos elementos (classe, método, campo, etc.) que compõem o assembly. O

programador realiza instrumentação de código manipulando a estrutura OO, seja pela

modificação de objectos ou referências ou pela adição ou remoção de objectos. Quando a

manipulação do assembly estiver concluída, o programador pode instruir a biblioteca para

que crie um novo assembly dinâmico, podendo este ser imediatamente executado ou

guardado em disco.

A especificação e planeamento da biblioteca foram desde muito cedo condicionados pela

complexidade inerente aos assemblies e mecanismos de carregamento de código no

ambiente de execução. O principal objectivo da biblioteca é fornecer mecanismos de alto

nível para a instrumentação de código, fáceis de utilizar pelo programador. No entanto, é

necessário descodificar os assemblies e construir uma representação facilmente reconhecida

e interpretável pelo mesmo. Esta estrutura tem de ser suficientemente robusta para

permitir a sua manipulação por métodos de alto nível, simplificando a tarefa do

Figura 9 – Metodologia de utilização da RAIL

Page 53: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 39

programador, ao mesmo tempo que esconde os pormenores inerentes ao código IL e à

metadata.

Para realizar os objectivos deste projecto, a biblioteca foi implementada numa arquitectura

de três camadas (Figura 10). A primeira camada ocupa-se da leitura e descodificação dos

ficheiros PE que constituem os assemblies. A segunda camada é responsável por fornecer as

classes para uma representação OO de todos os componentes dos assemblies. A terceira

camada fornece mecanismos de alto nível para instrumentação, constituindo a API pública

da biblioteca.

3.3.1. Leitura e Carregamento em Memória

A representação OO dos assemblies é construída com base na metadata e no código IL

existente nos ficheiros PE. Já foram descritos anteriormente os mecanismos

disponibilizados pela plataforma para a leitura dos assemblies, no entanto, nenhum possui

as funcionalidades necessárias para o desenvolvimento da RAIL na sua totalidade. Por um

lado, o System.Reflection não permite aceder ao código IL dos métodos, estando

limitado à estrutura do programa; por outro lado, o IMetaDataImport é um API não

gerido incapaz de resolver/interpretar o valor dos tokens existente no código e na metadata.

Figura 10 – Arquitectura RAIL

Page 54: Instrumentação de Código na Plataforma .NET

40 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Inicialmente, nenhuma das bibliotecas para leitura de assemblies conhecidas suportava a

totalidade das estruturas definidas nas especificações da ECMA para o CLR. Por exemplo,

tanto o CLIFileReader como o Managed Reflection IL Reader não reconheciam Custom

Attributes.

A inexistência de um API adequado para a leitura de assemblies e a necessidade de

compreender o complexo formato em que os programas .NET são guardados conduziram

ao desenvolvimento de uma primeira camada de software dentro da biblioteca em

detrimento da adopção de uma solução desenvolvida por terceiros.

Os requisitos para essa primeira camada eram:

• Ser totalmente desenvolvida em código gerido (i.e. não fazer chamadas COM ou

ao API Win32), de forma a não comprometer a portabilidade da biblioteca.

• Respeitar toda a especificação descrita no standard ECMA [ECMA2002] e suportar

todas as estruturas aí definidas.

• Efectuar a desassemblagem de assemblies de apenas um módulo. Visto não

existirem muitos assemblies multi-módulo, a complexidade exigida para o suporte

destes não ser, de um ponto de vista de investigação, suficientemente importante.

O desenvolvimento da primeira camada esteve sujeito a inúmeras dificuldades, de entre as

quais se destacam:

• A conversão de índices relativos, existentes na representação em disco dos

assemblies, para índices absolutos para zonas de memória. As diferenças existentes

nestes índices são causadas pela diferente paginação dos dados em memória e no

disco. Este problema foi solucionado através da chamada de um método

específico da plataforma (Win32) capaz de interpretar estas referências, solução

esta que contradiz um dos objectivos de desenvolvimento para esta camada de

software e acabou por conduzir à posterior substituição desta camada.

• A resolução de tokens ou referências entre metadata e código IL revelou-se uma

operação complicada devido a alguma incoerência e falta de informação na

documentação consultada, o que requereu a implementação de mecanismos para

a decomposição, interpretação e descompressão das mesmas.

Page 55: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 41

• A validação dos dados da metadata não é possível aquando da leitura do

assembly, sendo que, não existem quaisquer mecanismos para verificar a

coerência dos mesmos. Por exemplo, não existe um checksum para cada registo

lido de uma tabela da metadata. Esta dificuldade obrigou a validar visualmente os

dados lidos, utilizando ferramentas de desassemblagem como o ILDasm

[Microsoft2005d]. No entanto, muito erros de leitura só foram detectados durante

o desenvolvimento da terceira camada, que implementa a escrita dos assemblies

manipulados em disco, porque foi necessário reconstruir os assemblies utilizando o

System.Reflection.Emit e este gerava erros ao receber informação inválida.

• A descodificação e comparação de assinaturas de métodos, campos,

propriedades, tipos e variáveis locais, essenciais para a resolução de referências

dentro do código IL, revelou-se muito complicada devido à existência de uma

multiplicidade de combinações e formatos em que estas se podem encontrar.

• A extensão e volume de dados na metadata existente, mesmo em assemblies de

dimensões extremamente reduzidas, vieram extender mais do que o esperado a

dimensão da primeira camada e consequentemente o tempo gasto no seu

desenvolvimento.

• O suporte de todos os membros e estruturas existentes num programa .NET,

como os métodos, as referências a métodos no mesmo assembly ou em assemblies

diferentes, as chamadas a métodos por P/Invoke ou por COM Interop, a inclusão de

código nativo dentro de métodos geridos e o mecanismo de tratamento de

excepções, foram mais alguns dos problemas encontrados. A existência de um

número crescente de linguagens de programação, com compiladores para .NET,

veio aumentar ainda mais a complexidade. Linguagens como C++

[Stroustrup1997], Eiffel [Meyer1992] e Python [Eckel2001], ao serem compiladas

para .NET, moldam ligeiramente o formato dos PE, sem violar o formato

standard definido pela ECMA, mas o suficiente para acomodar toda a informação

que as suas aplicações necessitam. Estas subtis adaptações estiveram na origem

de alguns erros de leitura com que nos deparámos inicialmente.

Page 56: Instrumentação de Código na Plataforma .NET

42 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

A organização da primeira camada, visível na Figura 11, foi influenciada pela sua missão:

“ser uma fonte de informação para a criação de uma estrutura OO que represente um

assembly, assim como todos os seus membros”. O conteúdo do assembly foi classificado em

dois tipos: informação fornecida pela metadata e informação binária (código IL e outros

recursos do assembly). Para obter os dados guardados na metadata foram definidos mais de

50 tipos de dados para reproduzir os diferentes registos de cada tabela. Sempre que um

registo referencia informação binária, como os registos da tabela Method referenciam uma

stream de bytes que contém o corpo do método, é criado um array de bytes em memória

para guardar o conteúdo original da posição do ficheiro indicada. É também nesta camada

que se encontram todas as classes e métodos desenhados para realizar a resolução de

tokens, referências, assinaturas e índices.

À medida que o número de utilizadores da RAIL ia aumentando, o peso da manutenção

desta camada, sobre o tempo total de desenvolvimento, começou a aumentar. Por outro

lado, também já fora atingido um dos principais objectivos do desenvolvimento desta peça

de software: adquirir conhecimento sobre a composição interna dos assemblies. Um outro

objectivo, o de manter a biblioteca portável, ficou comprometido quando se adoptou um

método COM para mapear os ficheiros PE em memória. Nesta altura, foi necessário tomar

uma decisão que, inevitavelmente, conduziria à substituição da primeira camada por uma

biblioteca externa, a PEToolkit. A opção de adoptar a PEToolkit deveu-se ao facto de ser a

mais completa nesse momento e por esta ter passado a integrar o projecto Mono,

garantindo suporte técnico por parte da Novell.

Figura 11 – Organização da primeira camada da RAIL

Page 57: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 43

3.3.2. Representação Orientada aos Objectos de um Programa

Nesta secção será discutida a forma como a RAIL representa um assembly internamente.

Como já foi referido, a RAIL constrói uma estrutura de objectos em que cada objecto

representa um membro de um programa .NET. As classes utilizadas para construir esta

representação estão localizadas na segunda camada de software da biblioteca ilustrada na

Figura 12.

Esta segunda camada de software está organizada em grupos funcionais: o Grupo 1 reúne

todas as classes utilizadas na composição da estrutura que representa o assembly; o Grupo 2

contém as classes e algoritmos utilizados para representar e descodificar assinaturas

comprimidas de métodos, propriedades, campos e classes; o Grupo 3 desta camada de

software permite ao programador representar e manipular o código IL dos métodos;

finalmente, o Grupo 4 implementa os mecanismos para a resolução de referências externas

e internas aos assemblies.

Descrição do Grupo 1

O Grupo 1 tem por objectivo representar um assembly por meio de uma estrutura de

objectos. Como já foi referido e ilustrado na Figura 1 no primeiro capítulo desta

dissertação, um programa em .NET é composto por uma ou mais classes, sendo uma classe

composta por uma ou mais subclasses, métodos, campos, propriedades e eventos.

Figura 12 – Organização da segunda camada da RAIL

Page 58: Instrumentação de Código na Plataforma .NET

44 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Para se obter uma representação OO de um assembly é necessário existir pelo menos uma

classe que corresponda a cada um dos seus membros e que obedeça à posição hierárquica

original. A Figura 13 mostra o diagrama estático de classes criadas para representar um

assembly e os seus membros. A hierarquia de classes pode ser dividida em três níveis de

acordo com o propósito das classes que compõem cada nível:

• No primeiro nível está a classe CustomAttributeOwner que representa

qualquer membro do assembly ao qual possa ser associado um atributo (Custom

Attribute);

• No segundo nível estão as classes que representam os elementos base, i.e. o

próprio assembly e os seus módulos. Neste nível, também está localizada a classe

pai de todos os membros do assembly, a classe RMember, ponto de origem para o

terceiro nível da hierarquia. As classes RReturnType e RParameter

representam tipos de retorno e parâmetros de métodos;

• No terceiro nível estão localizadas as classes que descendem de RMember. Estas

classes são: a RField, que representa os campos; a REvent, que representa os

eventos; a RMethodBase, que representa os métodos e é pai de outras duas

classes, a RMethod e a RConstructor, que representam métodos simples ou

métodos construtores; a RProperty, que representa as propriedades; e a RType,

que representa as classes dentro do assembly.

Nas próximas subsecções estes níveis serão descritos com maior pormenor.

Figura 13 – Diagrama UML parcial da estrutura de classes implementada na RAIL para representar um assembly e os seus membros

Page 59: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 45

Primeiro Nível

No topo da estrutura da Figura 13 encontra-se a classe CustomAttributeOwner, o que

pode parecer estranho tendo em conta que o assembly é a unidade atómica de carregamento

na plataforma .NET e que todos os restantes componentes dos programas .NET são, de

alguma forma, sub componentes deste. No entanto, existe em .NET o conceito de Custom

Attributes, que são, como o nome indica, atributos personalizáveis que podem estar

associados a qualquer componente de um assembly, desde dos módulos, às classes,

métodos, campos, propriedades, eventos e até aos próprios assemblies. Por conseguinte, na

base de qualquer hierarquia está uma classe que permite representar e manipular estes

atributos.

Segundo Nível

O segundo nível hierárquico da estrutura apresentada na Figura 13 foi dividido em cinco

classes:

• A classe RAssembly é utilizada para representar os assemblies;

• A classe RMember é uma classe abstracta que serve para representar qualquer

membro de um assembly, desde das classes, até aos métodos e campos que as

compõem;

• A classe, RModule, representa os módulos de um assembly. Um módulo é um

componente que goza de um papel especial dentro do assembly, ao contrário das

classes, métodos, eventos, propriedades e campos, este não é declarado dentro de

uma classe pelo que não é elegível para descender de RMember. Todos os objectos

RMember pertencem a um módulo, assim como a classe que os declara. O

módulo, por seu lado, pertence a um assembly;

• A classe RParameter e a classe RReturnType, que representam parâmetros e

valores de retorno dos métodos, são tratadas de uma forma paralela à hierarquia

de membros de um assembly. Estas classes não são vistas como componentes do

assembly, pelo que não devem descender de RMember, e, no entanto, podem

possuir Custom Attributes, por conseguinte, descendem da classe

CustomAttributeOwner.

Page 60: Instrumentação de Código na Plataforma .NET

46 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Terceiro Nível

Todas as classes do terceiro nível da hierarquia da Figura 13 representam membros do

assembly pelo que, como já foi referido, existem classes para representar métodos

(RMethodBase), propriedades (RProperty), eventos (REvent), campos (RField) e

classes (RType). Um caso particular destas classes é a subdivisão do tipo de métodos a

representar em dois grupos: os métodos simples (RMethod) e os métodos construtores

(RConstructor).

Um aspecto importante que ainda não foi mencionado é o facto de que para além de

declarar as suas próprias classes, métodos, campos, propriedades e eventos, o assembly

também possui referências para estruturas externas. Isto é, o código IL dentro do programa

pode possuir referências para classes, métodos, campos, propriedades e eventos

declarados noutro assembly. Assim, para distinguir entre o que são representações de

estruturas internas ao assembly e referências externas foram criadas as classes “Def” e

“Ref”. As primeiras representam estruturas internas ao assembly enquanto que as

segundas representam referências externas. O tipo da classe é identificado pelo sufixo

“Def” ou “Ref” existente no nome da classe, e.g. RTypeDef, RTypeRef, RFieldDef e

RFieldRef.

A existência destes dois tipos de classes é visível ao longo de toda a hierarquia da Figura

13. Por exemplo, no segundo nível a classe RAssembly que representa um assembly é

especializada nas classes RAssemblyDef e RAssemblyRef, sendo que, a primeira

representa o assembly que vai ser alvo da instrumentação e a segunda quaisquer outros

assemblies referenciados dentro do primeiro. No terceiro nível da hierarquia esta técnica é

ainda mais evidente, todas as classes deste nível são especializadas desta forma.

A distinção entre classes “Def” e “Ref” é necessária, não só para fazer a separação entre o

que são referências internas e externas ao assembly, mas também para estabelecer uma das

principais regras de programação com a RAIL. Esta regra consiste em permitir que apenas

os objectos, que representam estruturas definidas dentro do assembly em instrumentação,

possam ser modificados, i.e. os objectos de classes “Def” podem ser modificados,

enquanto que, os objectos de classes “Ref” não. Se esta regra não existisse, a RAIL seria

obrigada a criar representações internas de todas as estruturas (e.g. classes, métodos e

campos) externas ao assembly em instrumentação que fossem referenciadas no código IL

deste. Esta situação iria gerar um grande overhead e prejudicar a performance da biblioteca.

Page 61: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 47

Para facilitar a compreensão do funcionamento das classes “Def” e “Ref” será analisado

com maior detalhe um caso particular da sua utilização na representação de eventos. No

diagrama da Figura 14 é visível que na classe base, REvent, estão definidos todos os

campos que compõem o estado de um objecto que representa um evento. Estão também

definidos os métodos públicos que permitem aceder aos valores dos campos da classe. Na

primeira classe filha, a REventDef, são adicionados os métodos que permitem “manipular

o evento” (pois é esta a classe que representa todos os eventos definidos no assembly alvo

da instrumentação, pelo que o seu estado pode ser modificado). A classe REventRef irá

representar as referências para os eventos definidos noutros assemblies. Esta classe adiciona

um novo campo de acesso privado que permite saber qual o assembly que possui a

referência para o evento em questão. É adicionado apenas um método na classe

REventRef em relação à sua irmã, um construtor, pois não deverá ser possível modificar

Figura 14 – Diagrama UML representando as classes REvent, REventDef e REventRef

Page 62: Instrumentação de Código na Plataforma .NET

48 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

o estado de um objecto REventRef depois de este ser criado. Este construtor é necessário

para criar representações de eventos, para os quais se obteve informação através do API

System.Reflection da plataforma .NET.

Identidade dos assemblies

Na secção 3.1 foi descrito que o mesmo assembly pode ter várias instâncias dentro do

mesmo sistema, sob a forma de diferentes versões. Muitas dessas versões são ainda

assinadas com o mecanismo de Strong Name, permitindo associar o valor resultante de uma

função de hash, após encriptação com a chave privada da entidade responsável pela sua

emissão, à identidade do assembly. O mecanismo de versões possui alguma complexidade

mas tem a vantagem de permitir identificar univocamente um assembly pelo seu nome,

pelo que, foi dada grande importância a este mecanismo dentro da biblioteca. As classes

apresentadas na Figura 15 são utilizadas para representar referências para assemblies

externos.

Figura 15 – Diagrama UML representando as classes RAssemblyName, RAssemblyNameDef, RAssemblyNameRef e RVersion

Page 63: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 49

Um assembly é identificado pelo nome e pelo código da versão (RVersion), que não é mais

que uma chave composta com os valores do número de versão (build number), número da

revisão (revision number), e o maior valor e menor valor da versão da plataforma em que o

assembly é suportado (major, minor). O nome do assembly também é composto pela

informação sobre a língua nativa do software (CultureInfo) e pelo token resultante do

hashing do assembly com a chave privada da entidade emissora. Existe ainda um campo

(flags) dedicado a guardar informação que caracteriza o tipo de atributos utilizado na

identificação do assembly, assim como para referir se o assembly é ou não Strong Named. A

classe RAssemblyNameDef é utilizada para representar o nome do assembly em

instrumentação e a RAssemblyNameRef para representar o nome de assemblies externos

nele referenciados.

Um dos mecanismos mencionados anteriormente (secção 3.2.1) é o P/Invoke, este

permite que métodos geridos dentro do CLR efectuem chamadas a métodos não geridos

do API Win32. As dificuldades inerentes a este mecanismo passam pela identificação do

assembly onde o método chamado se encontra declarado, já que este não obedece ao

sistema de identificação dos assemblies em .NET, e pela identificação do charset a utilizar na

transição, de forma a evitar problemas na conversão de parâmetros e resultados. Para

suportar este mecanismo foi necessário criar a classe ilustrada na Figura 16.

A classe RPInvokeInfo identifica o assembly, o nome do método e a restante informação

necessária à execução da chamada. Os parâmetros do método são definidos no objecto

RMethodDef que serve de invólucro ao método chamado por P/Invoke.

Figura 16 – Aspecto da classe RPInvokeInfo

Page 64: Instrumentação de Código na Plataforma .NET

50 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Figura 17 – Suporte para assinaturas compactadas

Page 65: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 51

Descrição do Grupo 2

O Grupo 2 da segunda camada de software da biblioteca (Figura 12) contém as classes e

algoritmos utilizados para representar e descodificar assinaturas comprimidas de métodos,

propriedades, campos e classes.

Muitas instruções IL fazem referência a classes, métodos, campos e propriedades definidas

noutros assemblies. O mecanismo de baixo nível utilizado nos assemblies para representar

estas referências implica a compressão dos dados. No entanto, o programador ao utilizar a

RAIL, não tem de conhecer esses mecanismos de compressão. A biblioteca sabe

descodificar estas referências e disponibiliza ao programador a uma representação OO do

código IL. O Grupo 2 da segunda camada de software da biblioteca, ilustrado na Figura 12,

foi desenvolvido para implementar esta funcionalidade. Na Figura 17 são esquematizadas

as principais classes, sendo visível o suporte de cinco tipos de assinaturas, que utilizam

diferentes codificações. A classe MethodDefSig representa as assinaturas de métodos

definidos no próprio assembly, incluindo os wrappers para chamadas por P/Invoke; a

MethodRefSig representa assinaturas de métodos externos ao assembly; a PropertySig,

referências a propriedades; a TypeSpecSig, referências a tipos; e a FieldSig, referências

a campos.

public static int ReadCompressedInt(byte [] stream, out int pos, int posValue) { pos = posValue; int returnVal = -1; if (stream[pos] == 0xFF) pos += 1; else if ((stream[pos] & 0x80) == 0) { returnVal = stream[pos]; pos += 1; else if ((stream[pos] & 0x40) == 0) { returnVal = (stream[pos] & ~0x80) << 8 | stream[pos + 1]; pos += 2; } else { returnVal = (stream[pos] & ~0xC0) << 24 | stream[pos + 1] << 16 | stream[pos + 2] << 8 | stream[pos + 3]; pos += 4; } return returnVal; }

Listagem 3.1 – Código C# do método ReadCompressedInt

Page 66: Instrumentação de Código na Plataforma .NET

52 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

A classe MetadataSignatures, no topo da hierarquia, não representa nenhuma

assinatura em particular, esta classe reúne as características comuns a todos os tipos de

assinaturas existentes num assembly. Um dos principais métodos da classe

MetadataSignatures é o método ReadCompressedInt() que permite ler valores

inteiros das assinaturas quando estes se encontram num formato comprimido. O método é

visível na Listagem 3.1 onde uma máscara inicial de bits define o tipo de compressão

utilizada. Este método verifica qual é essa máscara e reproduz o valor correcto.

As classes Param e RetType descrevem, respectivamente, as assinaturas dos parâmetros e

valores de retorno dos métodos representados por MethodRefSig e MethodDefSig.

Finalmente, a classe CustomMod é utilizada apenas por determinados compiladores, como

os de C++ [Stroustrup1997], para descrever características próprias da linguagem. Na

biblioteca RAIL não existe nenhum suporte para CustomMod [ECMA2002] para além da

leitura dos valores contidos nas assinaturas.

Em qualquer um dos tipos de assinaturas, os tipos dos parâmetros, valores de retorno,

campos e propriedades não são codificados directamente, i.e. não são codificados na forma

índices para tabelas da metadata, são antes agrupados em seis categorias representadas

pelas classes da Figura 18. Esses seis grupos representam ponteiros para funções (FNPTR),

Figura 18 – Classes para representa os tipos dentro das assinaturas

Page 67: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 53

ponteiros (PTR), value types (ValueType), arrays de uma dimensão (SZARRAY), arrays

multi-dimensão (ARRAY) e por fim classes (CLASS).

Descrição do Grupo 3

O Grupo 3 da segunda camada de software da biblioteca (Figura 12) é responsável pela

representação do código IL, mais em particular da sequência de instruções IL que

compõem o corpo dos métodos e das próprias instruções.

Existem mais de 200 tipos de instruções IL, por conseguinte, utilizar uma classe para

representar cada tipo de instrução faria com que a biblioteca fosse muito grande e,

provavelmente, difícil de utilizar. Para manter a biblioteca dentro de dimensões aceitáveis,

optou-se por dividir as instruções em grupos, de acordo com o tipo de operador de cada

uma.

Na Figura 19 é ilustrado o diagrama das classes utilizadas na representação de instruções

IL. Existe uma classe base chamada Instruction e uma série de classes filhas que se

distinguem entre si pelo seu tipo de operador. A título de exemplo, a classe ILMethod é

utilizada para representar as instruções call, callvirt, jmp, ldftn, ldvirtftn e

newobj. O denominador comum entre estas instruções está no operador ser uma

referência para um método. Um outro exemplo é a classe ILBranch, utilizada para

representar o conjunto das instruções que apontam para outra instrução (e.g. beq,

beq.s, bge, bge.s, bge.un, bge.un.s, bgt, bgt.s, bgt.un e leave.s).

O mesmo tipo de representação é utilizado para as instruções que referenciam campos,

assinaturas, números inteiros de 2, 4 e 8 bytes, tipos e até mesmo uma classe que representa

instruções sem operador (ILNone).

Para representar saltos, as instruções referenciam outras instruções que servem de destino

ao salto, impossibilitando a habitual representação do offset entre a instrução de salto e a

instrução de destino. Em .NET é necessário marcar as instruções que servem de alvo aos

saltos com labels, Estes labels permitem ao ambiente de execução saber que instruções,

destino de saltos, existem antes de serem emitidas. Desta forma, o problema da

impossibilidade de gerar uma instrução que referencia outra que ainda não foi emitida,

não se coloca.

Page 68: Instrumentação de Código na Plataforma .NET

54 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Figura 19 – Classes utilizadas na representação das instruções IL

Page 69: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 55

De forma a ser possível emitir labels, cada instrução tem de saber se é referenciada ou não

por outras instruções (i.e. se é alvo ou não de saltos). A classe base da hierarquia visível na

Figura 19 implementa as estruturas e os métodos necessários para que cada classe conheça

o número de instruções que a referencia. Um caso especial desta hierarquia é a classe

ILVirtualException, que não representando uma instrução verdadeira, é apenas

utilizada pelo RAIL para marcar os locais de início e fim de blocos do código de tratamento

de excepções.

A representação do código IL de um método consiste na organização de objectos das

classes já referidas dentro de uma tabela. Esta tabela pertence a um objecto da classe Code

que implementa os métodos (API de baixo nível) que permitem manipular a sequência de

instruções através da sua adição e remoção.

Um objecto do tipo RMethod que contenha código IL, um método com corpo, possui

obrigatoriamente uma instância da classe MethodBody que implementa todos os

mecanismos necessários para manipular as variáveis locais ao método. Cada objecto

MethodBody instancia também um objecto do tipo Code para representar o código IL do

método.

A RAIL pode ser utilizada para representar e modificar métodos que já existem (i.e.

implementados em assemblies em disco ou memória) ou então para criar novos métodos a

partir do zero. Para criar e modificar métodos são necessários mecanismos que permitam

emitir novas instruções de uma forma simples e rápida. Para realizar esta função foi

implementado um padrão de software Factory [Gamma1995]. A ILFactory (Figura 20) é

uma classe que permite gerar novos objectos, representando os diferentes tipos de

instruções, através de chamadas aos diferentes métodos que a compõem.

Page 70: Instrumentação de Código na Plataforma .NET

56 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Cada método desta classe devolve um novo objecto que representa uma instrução e aceita

como parâmetros o conjunto de operadores associado à mesma. As classes ILFactory,

Code e MethodBody são ilustradas na Figura 20.

Associada a cada método está a informação dos mecanismos de tratamento de excepções

ou seja tudo o que permite representar os blocos try-catch e os seus handlers. Ao nível dos

assemblies esta informação é registada sob a forma de uma tabela localizada imediatamente

a seguir à última instrução do método. A tabela contém a informação sobre o índice da

instrução de início e fim do bloco, do número e tipo de handlers associados a esse bloco (i.e.

o índice das instruções de inicio e fim do handler). Na RAIL seguiu-se uma abordagem

semelhante. A cada método é associada uma tabela, correspondendo cada item desta

tabela a um bloco try-catch, sendo cada um desses blocos associados a um ou mais handlers.

Estes handlers podem ser do tipo Catch (o mais comum), do tipo Finally, do tipo Filter ou do

tipo Fault. Embora só os dois primeiros existam em C#, o CLI já prevê a utilização dos

outros por outras linguagens de programação. Os handlers Catch permitem tratar excepções

Figura 20 – Classes ILFactory (parcial), MethodBody e Code

Page 71: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 57

de um determinado tipo e terminar normalmente; o Finally é executado sempre e termina

normalmente; o Filter é executado sempre que a expressão associada a este for verdadeira e

o Fault é semelhante ao Finally, não terminando, no entanto, normalmente.

A enumeração ExceptionHandlerType é responsável por classificar cada um dos

diferentes tipos de handler apresentados. Esta enumeração é utilizada como atributo para

os objectos da classe ExceptionHandler que representam os handlers através do registo

do bloco a que se destinam e das instruções que executam. Os objectos

ExceptionHandler são agrupados dentro de um objecto ExceptionBlock que

representa o bloco de código protegido dentro do método. Cada método (MethodBody)

pode reunir várias instâncias destes objectos dentro de uma ExceptionTable, como é

visível na Figura 21.

No final da Secção 3.3.2 foi descrita a utilidade da classe TypeResolver que para cada

objecto da biblioteca que represente uma classe, campo, método, propriedade, entre outros,

obtém a sua representação correspondente no CLR (e.g. a partir de um objecto RType

obtém-se o correspondente System.Type ou

System.Reflection.Emit.TypeBuilder). O inverso deste comportamento é

assegurado pela classe MetadataResolver que a partir de informação disponível na

Figura 21 – Classes envolvidas na representação dos mecanismos de tratamento de excepções

Page 72: Instrumentação de Código na Plataforma .NET

58 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

metadata e no ambiente de execução obtém as representações da RAIL para assemblies e

seus componentes (e.g. a partir de um objecto System.Type obtém um RTypeRef).

Como foi descrito no início deste capítulo, a metadata está organizada em tabelas, contidas

numa das várias streams existentes num assembly. Informação como as strings (com o nome

de identificadores de tipos, métodos, campos, entre outros), os dados binários (assinaturas,

etc.) e GUIDs estão noutras streams. A classe MetadataResolver implementa os métodos

necessários para efectuar leituras destas streams, como por exemplo os métodos

ResolveString(), USStream(), entre outros. A interface desta classe é visível na

Figura 22.

Descrição do Grupo 4

O Grupo 4 de classes da segunda camada (Figura 12), que implementa os mecanismos para

a resolução de referências externas e internas aos assemblies, é composto por uma classe, a

TypeResolver. Esta classe permite converter os objectos da RAIL, que representam

classes, métodos, campos, propriedades, eventos e assemblies, em objectos do ambiente de

execução da plataforma.

A Figura 23 ilustra a classe TypeResolver. Um exemplo da utilização desta classe

poderia seria: ao gerar código IL, durante a fase de emissão de um novo assembly, é

encontrada uma chamada a um método representado por um objecto RMethodDef (i.e.

um método definido no assembly em instrumentação), é necessário utilizar a classe

TypeResolver para obter uma referência para o método “real” (neste caso um objecto

MethodBuilder) dentro do ambiente de execução, de forma a ser possível emitir

correctamente o código. O método ResolveRMethod da classe TypeResolver é

responsável por resolver estas referências. Este método tem o seguinte comportamento:

Figura 22 – Classe MetadataResolver

Page 73: Instrumentação de Código na Plataforma .NET

ARQUITECTURA 59

• Em primeiro lugar verifica se o objecto corresponde a um componente interno ou

externo ao assembly. Se for uma referência externa, o método verifica se o assembly

referenciado se encontra carregado no ambiente de execução, se não estiver

realiza essa operação e, através do API System.Reflection, obtém uma

referência para o método desejado.

• Tratando-se de um método definido no próprio assembly, o ResolveRMethod

verifica se o objecto dinâmico (MethodBuilder) que implementa o método já foi

emitido. Caso tal tenha acontecido, devolve uma referência para esse objecto, caso

contrário, emite uma nova instância utilizando o tipo dinâmico (TypeBuilder)

que implementa o método em questão.

Este comportamento é comum a todos os outros métodos “Resolve” da classe

TypeResolver. Primeiro é verificado se o componente é interno ou externo, sendo de

seguida criada, localizada ou produzida a referência para o componente no ambiente de

execução.

Figura 23 – Classe TypeResolver

Page 74: Instrumentação de Código na Plataforma .NET

60 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

3.4. Instrumentação de Alto Nível A terceira camada de software da biblioteca, ilustrada na Figura 10, fornece ao programador

mecanismos de alto nível para instrumentação de código. O principal objectivo da RAIL é

dar ao programador um API de alto nível para manipular aplicações .NET que esconda

todos os pormenores inerentes ao formato dos assemblies, incluindo a organização e

estruturação da metadata e do código IL. O vocabulário associado à instrumentação de

código deve ser comum ao utilizado nas linguagens de programação de alto nível em que

se incluem termos como classe, método, campo e propriedade.

Foram projectadas diversas funcionalidades com o objectivo de permitir ao programador

realizar instrumentações complexas, sem ter de manipular código IL ou metadata.

Exemplos dessas funcionalidades são a substituição automática de referências para classes,

o redireccionamento de chamadas a métodos, a campos e propriedades e a cópia de tipos

entre assemblies. Estes mecanismos fazem uso da implementação de um padrão de software

Visitor [Gamma1995] que permite propagar as modificações a todos os componentes de

um assembly. Estes mecanismos, enumerados na seguinte lista tópicos, são discutidos nesta

secção:

• Redireccionar referências para classes

• Adicionar epílogos e prólogos a métodos

• Redireccionar o acesso e chamadas a métodos

• Redireccionar o acesso a campos e propriedades

• Redireccionar o acesso a campos para propriedades e vice-versa

• Redireccionar os acessos de leitura e escrita em campos para métodos

3.4.1. O padrão de software Visitor

O objectivo do padrão de software Visitor é executar uma operação em diversos elementos

de uma estrutura de objectos. A operação é encapsulada dentro da classe que implementa

o Visitor, não sendo necessário alterar qualquer uma das classes dos objectos que compõem

a estrutura a manipular. Este padrão de software permite separar as classes que compõem a

estrutura, dos algoritmos a aplicar sobre esta.

Page 75: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE ALTO NÍVEL 61

No sistema RAIL, cada nó da estrutura de objectos que representa um assembly implementa

um método Accept(), que “aceita” um Visitor. Se este Visitor implementar alguma

operação sobre o tipo de nó que o recebeu, essa operação é executada. O processo é

denominado Double Dispatching [Gamma1995], visto que o nó que recebe o Visitor também

faz uma chamada a um método deste passando-se a si próprio como parâmetro. O

diagrama UML que representa o padrão de software Visitor é visível na Figura 24. O

mecanismo de Double Dispatching é ilustrado nas notas na parte inferior desta imagem e

consiste na chamada ao método VisitConcreteElementX(this) do Visitor v pelo

método Accept(Visitor v) do elemento alvo da operação a realizar.

A classe base do Visitor na RAIL declara os seguintes métodos:

• VisitAssembly(), que permite aplicar operações a todo um assembly

• VisitModule(), que permite aplicar operações a um módulo e todos os seus

membros (e.g. classes, métodos, atributos)

Figura 24 –O padrão de software Visitor

Page 76: Instrumentação de Código na Plataforma .NET

62 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

• VisitType(), que permite aplicar operações a uma classe e todos os seus

membros (e.g. métodos, campos, propriedades)

• VisitMethod(), que permite aplicar operações a um método

• VisitConstructor(), que permite aplicar operações a um método construtor

• VisitField(), que permite aplicar operações a um campo

• VisitProperty(),que permite aplicar operações a uma propriedade e aos

seus métodos acessores

• VisitEvent(),que permite aplicar operações a um evento

Ao padrão de software Visitor foi associado um padrão de software Walker [Gamma1995]

que permite caminhar ao longo da estrutura e definir a forma como o Visitor actua, i.e. se a

função do Visitor actua sobre o elemento antes ou depois de visitar todos os elementos

abaixo deste na estrutura. O Walker define duas formas de “caminhar” na estrutura,

PreOrder e PostOrder, uma actua antes de passar aos elementos seguintes e a outra só

no retorno.

3.4.2. Substituição de Referências

No primeiro capítulo desta dissertação foi apresentado um exemplo de um utilizador que

realiza o download de uma aplicação da Internet mas não pode afirmar com segurança que

essa aplicação não irá roubar informação confidencial dos seus ficheiros em disco. Também

foi descrita a forma de implementar um mecanismo de proxy que permite monitorar ou até

mesmo controlar os acessos a disco da aplicação através da substituição das referências

para as classes responsáveis pelos acessos a disco por outras recorrendo à instrumentação

de código. A RAIL permite realizar este tipo de manipulações de uma forma automática e

permite também que essas modificações se propaguem ao longo de todo o assembly.

Page 77: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE ALTO NÍVEL 63

O código na Listagem 3.2 ilustra a utilização do Visitor ReferenceReplacer na

substituição de todas as referências à classe Foo para a classe Bar dentro do assembly

Fooish.exe.

O construtor da classe ReferenceReplacer tem como parâmetros referências para o tipo

original e para o tipo que o vai substituir (ambos representados por objectos RType). O

passo seguinte é aplicar o Visitor à estrutura OO que representa o assembly. No caso da

Listagem 3.2, este Visitor é recebido pelo objecto na raiz da estrutura, o RAssembly que

representa o assembly a instrumentar obrigando a que a modificação se aplique a todo o

programa.

Na sequência de instruções da Listagem 3.2 é inicialmente chamado o método estático

LoadAssembly da classe RAssemblyDef que carrega uma imagem do assembly em

memória e constrói a estrutura que o representa e aos seus componentes. Em seguida são

obtidas referências para os dois tipos a trocar, o original e o que o irá substituir, sob a

forma de objectos RType e é criada uma instância do Visitor ReferenceReplacer que os

irá utilizar. Finalmente, é aplicada a transformação a todo o assembly, sendo o tipo original

eliminado e o novo assembly gerado.

Os métodos implementados na classe ReferenceReplacer têm de procurar referências

para o tipo original em diferentes locais. Por exemplo, o método VisitField verifica se o

campo que está a visitar é do tipo original e, se assim for, muda o tipo do campo. O

método VisitProperty faz as mesmas operações sobre o tipo da propriedade e sobre os

métodos Get e Set desta. O método VisitMethod verifica se o tipo dos parâmetros ou

tipo do valor de retorno do método são passíveis de ser modificados, assim como se os

tipos dos operadores das instruções o são. Sendo a arquitectura da RAIL baseada numa

representação OO do assembly e do código IL dos seus métodos, as modificações descritas

correspondem a simples troca de referências nesta estrutura onde todos as referências para

RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Fooish.exe"); RType oldType = rAssembly.RModuleDef.GetType("Foo"); RType newType = rAssembly.RModuleDef.GetType("Bar"); ReferenceReplacer rr = new ReferenceReplacer(oldType,newType); rAssembly.Accept(rr); rAssembly.RModuleDef.RemoveType("Foo"); rAssembly.SaveAssembly("Fooish.exe");

Listagem 3.2 – Código utilizado para trocar todas as referências do tipo Foo para o tipo Bar dentro do assembly Fooish.exe

Page 78: Instrumentação de Código na Plataforma .NET

64 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

o objecto RType que representa o tipo original são substituídas por referências para o

objecto RType que representa o novo tipo.

3.4.3. Cópia de Classes e Métodos

Grande parte das aplicações dos nossos dias tem de obedecer a restrições no seu tamanho

ou dimensão, visto que muitas serão copiadas ou enviadas pela Internet. Nestes casos é

essencial manter uma dimensão reduzida, seja para baixar os custos com o aluguer de

largura de banda nas empresas, seja para diminuir o tempo de transmissão. As aplicações

em .NET são baseadas em componentes, sendo habitual os programadores utilizarem

bibliotecas definidas em assemblies externos. A maioria destas bibliotecas é muito grande e,

na maioria das vezes, o programador só necessita de utilizar uma funcionalidade mínima

entre tudo o que lhe é disponibilizado. No entanto, geralmente, vê-se obrigado a juntar à

sua aplicação ficheiros de alguns megabytes, enquanto que, quatro ou cinco classes

poderiam resolver o seu problema.

A RAIL disponibiliza funcionalidades para copiar classes entre assemblies, assim como para

copiar métodos, campos, propriedades e eventos entre classes. No caso da cópia de classes

entre assemblies é criado um novo objecto RTypeDef no assembly de destino como o mesmo

nome e conteúdo da classe original. Isto significa que todos os métodos, campos e

propriedades também serão duplicados no objecto de destino.

A implementação deste mecanismo não é tão trivial como possa parecer à primeira vista.

Por exemplo, se quisermos copiar o tipo Foo.A entre dois assemblies e este referenciar o

tipo Foo.B dentro do mesmo PE (basta possuir um campo, uma propriedade ou chamar

um método da classe Foo.B), não é suficiente copiar a classe em questão, pois as

referências para Foo.B deixam de ser válidas. A RAIL fornece duas abordagens para

solucionar este problema: a primeira passa pela cópia recursiva para o assembly de destino

de todas as referências que a classe original possui (i.e. vão sendo copiadas todas as classes

e suas referências internas até já não existirem mais referências deste tipo nas classes

copiadas); a segunda consiste em transformar essas referências internas em referências

externas para o assembly original. Não é possível afirmar qual das duas abordagens será a

“melhor”, pois ambas são viáveis em cenários de utilização distintos.

Page 79: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE ALTO NÍVEL 65

A Listagem 3.3 mostra o código necessário para copiar todas as classes de Bar.dll,

referenciadas em Foo.exe, para dentro de Foo.exe. Em primeiro lugar recorre-se ao

método LoadAssembly para carregar as imagens dos assemblies Foo.exe e Bar.dll em

memória. Em seguida percorre-se a lista de referências externas do assembly Foo.exe e,

para cada referência resolvida dentro do assembly Bar.dll, que não exista no assembly

Foo.exe, é feita uma cópia da mesma e criado um objecto ReferenceReplacer para

substituir todas a referências existentes.

3.4.4. Redireccionamento de Chamadas a Métodos

Ambas as funcionalidades descritas nas secções anteriores utilizam o mecanismo de

redireccionamento de chamadas a métodos que está acessível através do API de alto nível.

Este mecanismo permite que sejam substituídas as chamadas a um determinado método

por chamadas a outro com a mesma assinatura.

A grande restrição deste mecanismo é a obrigatoriedade da igualdade da assinatura dos

métodos a redireccionar. No caso da igualdade não se verificar, teriam de se adicionar ou

remover instruções antes e depois das chamadas aos métodos de forma a assegurar a

integridade da stack. O código que antecede a instrução IL de chamada a um método

RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RAssemblyDef rAssemblyLib = RAssemblyDef.LoadAssembly("Bar.dll"); RTypeRef [] rTypeRefs = rAssembly.GetTypes(); if (rTypeRefs!=null) for (int i=0; i < rTypeRefs.Length; i++) { RTypeDef rtD = (RTypeDef)rAssemblyLib.RModuleDef.GetType( rTypeRefs[i].Name); if (rtD!=null && rAssembly.RModuleDef.GetType(rtD.Name)==null) { rtD = rAssembly.RModuleDef.CopyType(rtD,rtD.Name); ReferenceReplacer rr = new ReferenceReplacer(rTypeRefs[i],rtD); rAssembly.Accept(rr); } } rAssembly.SaveAssembly("Foo.exe");

Listagem 3.3 – Código utilizado para copiar do assembly Bar.dll para o assembly Foo.exe todas as classes referenciadas pelo assembly Foo.exe

Page 80: Instrumentação de Código na Plataforma .NET

66 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

assegura que a instância do objecto em que o método é invocado é colocada na stack1 e que

são carregados na stack todos os objectos que serão passados como parâmetros ao método.

Concluiu-se que qualquer tentativa para automatizar o processo de troca de referências

entre métodos com diferentes assinaturas, de uma forma genérica, seria de uma grande

complexidade e será alvo de investigação no futuro.

Na Listagem 3.4 é listado o código necessário para trocar todas as chamadas ao método

Write por chamadas ao método MyWrite dentro do assembly Foo.exe. A primeira

instrução, como já foi referido, constrói a representação OO do assembly e a segunda obtém

a classe que implementa os métodos que vão ser trocados. De seguida, é construído o array

que representa o tipo dos parâmetros de ambos os métodos, obtém-se as referências para

os objectos RMethodDef que representam os métodos original e o seu substituto e,

finalmente, é construída uma instância do Visitor CodeTransformer para trocar os

métodos, sendo aplicada a modificação a todo o assembly.

3.4.5. Redireccionamento do Acesso a Campos e Propriedades

O redireccionamento das operações de leitura e escrita em campos e propriedades pode ser

utilizado no âmbito das funcionalidades da troca de referências ou cópia de classes. Pode,

no entanto, ter muitas outras utilidades se for, por exemplo, permitido trocar os acessos a

campos por acessos a propriedades ou vice-versa, trocar as escritas e leituras em campos

1 Este passo não acontece se tratar de um método estático.

RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RType rtd = rAssembly.RModuleDef.GetType("Bar"); RParameter [] paramsz = new RParameter[1]; paramsz[0] = new RParameter(0,rAssembly.GetType("System.String")); RMethodDef rmdOriginal = (RMethodDef)rtd.GetMethod( "Write",rAssembly.GetType("System.Void"),paramsz); RMethodDef rmdReplacer = (RMethodDef)rtd.GetMethod( "MyWrite",rAssembly.GetType("System.Void"),paramsz); CodeTransformer cc = new CodeTransformer(); cc.add(new MethodReplacer(rmdOriginal,rmdReplacer)); rAssembly.Accept(cc); rAssembly.SaveAssembly("Foo.exe");

Listagem 3.4 – Código utilizado para redireccionar as chamadas ao método Write por chamadas ao método MyWrite no assembly Foo.exe

Page 81: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE ALTO NÍVEL 67

ou propriedades por chamadas a métodos, ou trocar chamadas a métodos por acessos a

propriedades. A RAIL disponibiliza os métodos para automatizar estes processos.

Coloca-se, no entanto, a mesma restrição descrita na secção anterior: as substituições a

efectuar não podem requerer qualquer modificação do conteúdo da stack, pois essa

modificação teria de ser realizada manualmente pelo programador.

Na Listagem 3.5 é apresentado o código de um programa que redirecciona os acessos de

escrita e leitura ao campo Val1 da classe FooBar por chamadas aos métodos SetVal1 e

GetVal1 da mesma classe.

Após obter as referências para o assembly, para a classe que implementa os métodos e,

neste caso, para o campo Val1, a aplicação obtém as referências para os métodos GetVal1

e SetVal1. De seguida, o programa cria um objecto CodeTransformer para reunir os

dois Visitor necessários para realizar a transformação. O primeiro Visitor é um objecto

do tipo ReplaceFieldReadAccess. Este recebe como parâmetros referências para o

campo e para o método GetVal1 e vai substituir todos os acessos de leitura ao campo

Val1 por chamadas ao método GetVal1. O segundo Visitor é o

ReplaceFieldWriteAccess e faz o mesmo tipo de substituições nos acessos de escrita.

3.4.6. Adicionar Epílogos e Prólogos a Métodos

Uma das funcionalidades com maior sucesso na RAIL é a adição de epílogos e prólogos a

métodos, motivada por uma crescente vaga de desenvolvimento de aplicações para AOP

RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RTypeDef aType = (RTypeDef)rAssembly.RModuleDef.GetType("FooBar"); RType fieldType = rAssembly.RModuleDef.GetType(“SomeClass"); RField aField = aType.GetField("Val1"); RMethod methodGet = aType.GetMethod("GetVal1",fieldType, new RParameter[0]); RParameter [] rparams = new RParameter[1]; rparams[0]=new RParameter(0,fieldType); RMethod methodSet = aType.GetMethod("SetVal1",rAssembly.GetType("System.Void"), rparams); CodeTransformer cc = new CodeTransformer(); cc.add(new ReplaceFieldReadAccess(aField,methodGet)); cc.add(new ReplaceFieldWriteAccess(aField,methodSet)); rAssembly.Accept(cc); rAssembly.SaveAssembly("Foo.exe");

Listagem 3.5 – Código utilizado para redireccionar os acessos ao campo Val1 dentro do assembly Foo.exe pelos métodos SetVal1 e GetVal1

Page 82: Instrumentação de Código na Plataforma .NET

68 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

em .NET. O paradigma AOP é cada vez mais conhecido e o número de ferramentas que

permitem este tipo de programação está a aumentar de dia para dia.

O API do RAIL permite adicionar código IL de um método ao inicio e/ou ao fim de outro

método. Os métodos a adicionar ou manipular podem já existir dentro do assembly ou

podem ser criados em tempo de execução pelo programador.

Para auxiliar a compreensão deste mecanismo é apresentado um exemplo de código na

Listagem 3.6. Neste programa, o corpo dos métodos WriteToScreenBefore e

WriteToScreenAfter é adicionado ao início e ao fim, respectivamente, do corpo do

método MyMethod. Os métodos são exibidos em código IL.

Adicionar novas instruções (mesmo que copiadas a partir de um terceiro método) ao início

do corpo de um método é linear: basta colocar novas instruções a partir da posição zero da

tabela com as instruções, não copiar a instrução final de retorno e fazer um refrescamento

dos índices das instruções. Neste processo é necessário ter-se o cuidado de verificar que as

instruções de acesso a variáveis locais no método inserido e no método alvo são

equivalentes entre si. Por exemplo, se uma instrução se refere ao parâmetro dois do

método no código inserido, essa referência deve corresponder a um objecto do mesmo

tipo, com a mesma função (identidade) e com o mesmo índice na lista de parâmetros no

método final. No entanto, não existem mecanismos que permitam assegurar a

RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RType aType = rAssembly.RModuleDef.GetType("FooBar"); RParameter [] paramsz = new RParameter[1]; paramsz[0] = new RParameter(0,rAssembly.GetType("System.String")); RMethodDef prologueMethod = (RMethodDef)aType.GetMethod("WriteToScreenBefore", rAssembly.GetType("System.Void"),paramsz); RMethodDef epilogueMethod = (RMethodDef)aType.GetMethod("WriteToScreenAfter", rAssembly.GetType("System.Void"),paramsz); paramsz = new RParameter[1]; paramsz[0] = new RParameter(0,rAssembly.GetType("System.String")); RMethodDef methodToModify = (RMethodDef)aType.GetMethod("MyMethod", rAssembly.GetType("System.Void"),paramsz); CodeTransformer cc = new CodeTransformer(); cc.add(new MethodPrologueAdder(prologueMethod, true)); cc.add(new MethodEpilogueAdder(epilogueMethod, true)); methodToModify.Accept(cc); rAssembly.SaveAssembly("Foo.exe");

Listagem 3.6 – Programa que utiliza o API RAIL para adicionar um epílogo e um prólogo a um método

Page 83: Instrumentação de Código na Plataforma .NET

INSTRUMENTAÇÃO DE ALTO NÍVEL 69

correspondência entre as variáveis locais no código a inserir e as variáveis locais no código

alvo, pelo que terá de ser o programador a ter tais cuidados.

Adicionar novas instruções ao final de um método requer uma operação extra (para além

das realizadas para adicionar instruções ao inicio de um método): a de apagar a instrução

de retorno (ret) do método antes de inserir novo código.

Os corpos dos métodos envolvidos no programa da Listagem 3.6 são transcritos na integra

na Tabela 3.1. O código IL foi obtido com recurso à ferramenta ILDasm disponível na

plataforma .NET, que permite fazer a desassemblagem de programas .NET. Nas primeiras

duas linhas da tabela são apresentados os corpos dos métodos WriteToScreenBefore e

WriteToScreenAfter que irão ser adicionados ao início e fim do método MyMethod. Na

terceira linha é apresentada a estrutura original do código IL dentro do método

MyMethod. Finalmente, a última linha exibe o código após a instrumentação. As zonas a

sombreado identificam o código modificado.

Page 84: Instrumentação de Código na Plataforma .NET

70 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

Métodos Corpo do método (código IL)

WriteToScreenBefore

.method public hidebysig instance void WriteToScreenBefore(string test2) cil managed { .maxstack 2 IL_0000: ldstr "Before: " IL_0005: ldarg.1 IL_0006: call string [mscorlib]System.String::Concat(string,string) IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: ret }

WriteToScreenAfter .method public hidebysig instance void WriteToScreenAfter(string test2) cil managed { .maxstack 2 IL_0000: ldstr "After: " IL_0005: ldarg.1 IL_0006: call string [mscorlib]System.String::Concat(string,string) IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: ret }

MyMethod (original) .method public hidebysig instance void MyMethod(string test) cil managed { .maxstack 2 IL_0000: ldstr "MyMethod : " IL_0005: ldarg.1 IL_0006: call string [mscorlib]System.String::Concat(string,string) IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: ret }

MyMethod (modificado) .method public hidebysig instance void MyMethod(string test) cil managed { .maxstack 2 IL_0000: ldstr "Before: " IL_0005: ldarg.1 IL_0006: call string [mscorlib]System.String::Concat(string,string) IL_000b: call void [mscorlib]System.Console::WriteLine(string) IL_0010: ldstr "MyMethod : " IL_0015: ldarg.1 IL_0016: call string [mscorlib]System.String::Concat(string,string) IL_001b: call void [mscorlib]System.Console::WriteLine(string) IL_0020: nop IL_0021: ldstr "After: " IL_0026: ldarg.1 IL_0027: call string [mscorlib]System.String::Concat(string,string) IL_002c: call void [mscorlib]System.Console::WriteLine(string) IL_0031: ret }

Tabela 3.1 – Métodos utilizados e método modificado no exemplo da Listagem 3.6

Page 85: Instrumentação de Código na Plataforma .NET

TRABALHO RELACIONADO 71

3.5. Trabalho Relacionado Nesta secção é descrito o trabalho existente na plataforma .NET relacionado com

instrumentação de código.

3.5.1. AbstractIL

O AbstractIL [Syme2001], da autoria de Don Syme, Microsoft Research em Cambridge, foi a

primeira biblioteca para manipulação de programas em .NET. Foi totalmente desenvolvida

em OCaml [Remy1998] e compilada para .NET, o que permite a sua utilização em

linguagens como C# [ECMA2003]. No entanto, é muito mais vocacionada para a utilização

a partir de F# [Microsoft2004a], uma linguagem funcional desenhada para esta plataforma.

Esta biblioteca baseia-se num princípio simples: o programa é carregado no ambiente de

execução e é construída uma representação deste sobre a forma de uma Abstract Sintax Tree

(AST). Em seguida esta árvore pode ser manipulada de acordo com as necessidades do

programador e o resultado guardado sob a forma de um novo assembly.

A AbstractIL possui no entanto um problema: todos os detalhes dos programas de .NET e

do formato dos ficheiros que os compõem, descritos na secção 3.1, são visíveis para o

programador. Por conseguinte, a utilização da AbstractIL levanta inúmeras dificuldades,

desde o cálculo dos índices das instruções e dos alvos dos saltos, até à gestão de

referências, atributos, métodos, eventos e mecanismos de tratamento de excepções. O

programador ao modificar um programa através da manipulação directa de uma AST

enfrenta um mar de dificuldades.

3.5.2. PEAPI e PERWAPI

Quando se iniciou este trabalho, a PEAPI [PLAS2005] era uma biblioteca que permitia a

emissão de programas .NET. A principal característica desta biblioteca, quando comparada

com o API System.Reflection.Emit é ser muito mais rápida.

Actualmente a PEAPI transformou-se na PERWAPI [PLAS2005], permitindo agora fazer a

leitura das aplicações e a sua manipulação. É possível utilizar esta biblioteca para ler,

modificar e escrever programas para o Common Language Runtime (CLR).

Esta biblioteca tem o mesmo problema que associámos ao AbstractIL: ainda que a sua

complexidade de utilização seja menor, continuam a ser visíveis para o programador todos

os pormenores da estrutura das aplicações .NET.

Page 86: Instrumentação de Código na Plataforma .NET

72 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

3.5.3. MONO PEToolkit

Juntamente com a implementação do CLR pela Microsoft e fruto da estandardização das

especificações da plataforma, surgiram duas novas implementações desta. A Shared Source

Common Language Infrastructure (SSCLI) [Stutz2003] ou, como é conhecida pelo seu nome

de código, ROTOR, é uma implementação ao estilo de código fonte livre, sendo da

responsabilidade da Microsoft Research. A Microsoft não tem uma política de divulgar o

código fonte dos seus produtos. No entanto, no caso do ROTOR a opção de o fazer para

divulgar a sua tecnologia no meio académico foi óbvia: a melhor forma de conhecer um

standard é navegar e modificar o código fonte de uma implementação deste, o acesso ao

código fonte também permite aos investigadores modificar a plataforma e ajustá-la para as

suas experiências. A segunda implementação do standard, da autoria da Novell, é apelidada

de MONO [Novell2005a]. A MONO, também no regime de código livre, resulta da junção

de diversos projectos menores entre os quais o PEToolkit. Este toolkit permite a leitura de

aplicações .NET e disponibiliza um interface para aceder à estrutura e ao código das

aplicações, ainda que num formato muito similar ao encontrado dentro dos ficheiros PE

(descrito nas especificações da ECMA). Por conseguinte, a PEToolkit é uma biblioteca de

baixo nível e não esconde a complexidade inerente às aplicações .NET.

A PEToolkit também não permite a geração de código IL e a criação de novos assemblies,

sendo apenas possível ler código para memória.

Actualmente o desenvolvimento e suporte do MONO PEToolkit foi cancelado e a sua

substituição dentro do projecto MONO está prevista para muito breve. O PERWAPI

deverá ser a biblioteca utilizada enquanto a CECIL [Novell2005b], uma biblioteca

desenvolvida pelo próprio projecto, não for concluída.

3.5.4. Reflector

Lutz Roeder é o autor do Reflector [Roeder2004] que é uma aplicação que permite ler e

desassemblar programas .NET. Esta aplicação é, na realidade, uma evolução do Managed

Reflection ILReader do mesmo autor. O ILReader já existia quando se iniciaram os trabalhos

desta dissertação, mas apresentava algumas desvantagens que não eram visíveis nas

bibliotecas descritas anteriormente, particularmente por não suportar Custom Attributes

(mecanismo que permite associar atributos a quaisquer componentes de um programa

como os seus métodos, campos, tipos, eventos e propriedades). O Reflector, no entanto, já

não possui estas limitações.

Page 87: Instrumentação de Código na Plataforma .NET

TRABALHO RELACIONADO 73

3.5.5. CLIFileReader

À semelhança da biblioteca anterior, a CLIFileReader [Cisternino2003] apenas permite a

leitura de programas. Não é conhecida qualquer documentação desta biblioteca e, no

entanto, é das mais utilizadas pela comunidade científica.

3.5.6. Common Language Aspect Weaver

Em meados nos anos 90 surgiu nos laboratórios da Xerox em Palo Alto um novo paradigma

de programação apelidado de Programação Orientada aos Aspectos (AOP). Este

paradigma centra-se na ideia de que existem determinados comportamentos ou

características que são comuns a diversas partes de um programa e que podem ser

aplicados de uma forma transversal ao mesmo. Um exemplo da utilização do AOP seria

identificar todos os locais no código onde pode ocorrer uma excepção na ligação a uma

base de dados como sendo um aspecto associado a esse programa. A AOP permitiria

introduzir transversalmente a toda a estrutura da aplicação o código de tratamento dessa

excepção. Informalmente esta operação poderia ser descrita como: “sempre que ocorrer

uma excepção na criação de uma ligação à BD, em qualquer ponto do programa, deve ser

executado o seguinte código”.

No parágrafo anterior foi apresentada uma visão simplificada do AOP. Porém, se

imaginarmos que a adição de novo código a uma aplicação pode acontecer depois desta

ser compilada, fica ilustrada a necessidade de ferramentas de instrumentação de código

para a implementação de AOP. John Lam ao implementar a biblioteca Common Language

Aspect Weaver (CLAW) [Lam2002] para realizar AOP em .NET, acabou por desenvolver a

primeira aplicação de instrumentação de código nesta plataforma, ainda que, não seja uma

biblioteca de uso generalista.

A realização de AOP ao nível do código intermédio tem em .NET é vantajosa, visto que, é

permitido utilizar este paradigma de programação para modificar aplicações escritas em

qualquer linguagem que compile para o CLR (O CLR é uma ambiente multilingue). Por

outro lado, o paradigma AOP não é utilizável apenas ao nível do código intermédio, é

possível programar AOP ao nível do código fonte. Em JAVA, por exemplo, existe a AspectJ

[Eclipse2005]. Trata-se de uma extensão à linguagem JAVA que permite a programação

orientada aos aspectos ao nível do código fonte. No entanto, esta ferramenta requer a

utilização de compiladores próprios. Em .NET, começam já aparecer projectos com a

mesma filosofia do AspectJ para linguagens como o C#.

Page 88: Instrumentação de Código na Plataforma .NET

74 CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET

3.5.7. WEAVE.NET

A WEAVE.NET [Lafferty2003] é uma biblioteca para AOP em .NET. Esta biblioteca utiliza

o CLIFileReader para construir uma representação dos programas em memória, permitindo

a sua manipulação através da utilização de mecanismos ao estilo AOP e a consequente

emissão dos programas modificados utilizando o System.Reflection.Emit.

Page 89: Instrumentação de Código na Plataforma .NET

Domínios de Aplicação

“A verdadeira viagem de descoberta não consiste em ver novas paisagens, mas em descobrir um novo olhar.”

— Marcel Proust

Neste capítulo são discutidas algumas aplicações da instrumentação de código que foram

testadas no decurso do trabalho da dissertação. A principal motivação para a realização

destas experiências foi testar e avaliar as capacidades da RAIL em diferentes cenários de

aplicação.

O capítulo inicia-se com a descrição de um mecanismo que permite integrar em programas

já compilados a capacidade de serem modificados em tempo de execução. É importante

recordar que a plataforma não possui ferramentas de reflexão a este nível, sendo este

mecanismo o primeiro a permiti-lo.

Seguidamente é proposta uma forma de usar a reflexão da RAIL para avaliar a utilização

dos mecanismos de tratamento de excepções no código das aplicações. Os resultados desta

análise permitem tirar conclusões sobre a forma como os programadores utilizam os

mecanismos de tratamento de excepções que lhes são disponibilizados pela plataforma. A

concluir o capítulo é discutida a forma como a instrumentação de código pode ser utilizada

no desenvolvimento de aplicações com recurso a metodologias AOP e são apresentados

alguns projectos de terceiros que utilizam ou já utilizaram a RAIL.

Capítulo

4

Page 90: Instrumentação de Código na Plataforma .NET

76 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

4.1. Alternativa aos Proxies Dinâmicos Na plataforma JAVA, os proxies dinâmicos apareceram com a versão 1.3 do J2SE

[Blosser2000;Sun1999], permitindo que se intercepte em tempo de execução a chamada a

métodos, de forma a adicionar novos comportamentos entre a chamada de um método e a

sua execução. Nos proxies dinâmicos, ao contrário dos proxies normais, as classes proxy são

definidas em tempo de execução e todas as chamadas passam por um único método,

denominado Invoke.

Da perspectiva do código que faz a invocação, a interface da classe proxy é exactamente

igual ao da classe que encapsula. Assim, este tipo de padrão de software permite que em

tempo de execução se dêem novas funcionalidades aos programas. Por exemplo, não faz

muito sentido gastar tempo e energia a desenvolver e manter código de debugging e logging

dentro do código principal da aplicação quando se podem centralizar estas

funcionalidades em proxies. Utilizando proxies dinâmicos é possível fazer com que seja

executado código antes e depois das chamadas aos métodos de qualquer classe. Isto

possibilita que o código de debug ou log seja escrito dentro da classe que implementa o

proxy dinâmico e executado sempre que ocorram chamadas aos métodos das classes

encapsuladas. Os proxies dinâmicos são generalistas, visto que não são implementados

para encapsular uma classe em particular, mas qualquer classe que se deseje

independentemente do seu interface.

O padrão de software tradicional de proxies obriga a desenvolver uma classe proxy por cada

classe que se pretende encapsular e reescrever todos os métodos que essa classe

implementa no proxy. Quando se trata de proxies dinâmicos, já não existe a necessidade de

escrever um proxy por cada classe a encapsular. Como as classes proxies só são geradas em

tempo de execução, o que o programador tem de escrever é uma classe genérica que deve

implementar apenas um método. Este método será executado aquando da intercepção da

chamada a qualquer método das classes encapsuladas. Em tempo de execução, o

mecanismo de proxies dinâmicos encarrega-se de criar as classes proxy com uma interface

semelhante à das classes encapsuladas e incluir o código do método previamente definido

em todos os métodos do proxy.

Page 91: Instrumentação de Código na Plataforma .NET

ALTERNATIVA AOS PROXIES DINÂMICOS 77

Em .NET, o mecanismo que permite implementar este padrão de software é chamado

RealProxy [Microsoft2002] e recorre às capacidades de reflexão e de invocação remota da

plataforma. Na Figura 25 é sintetizado o processo de criação dinâmica de proxies no CLR.

Inicialmente, o programador deve definir uma classe filha da classe RealProxy que faça o

override do método Invoke para incluir as funcionalidades que deseja (na figura essa

classe é chamada de GenericProxy). Em tempo de execução, é à classe GenericProxy

que é pedido para criar uma proxy transparente para o objecto a encapsular. A esta nova

classe é dado o nome de Transparent Proxy. O método Invoke em GenericProxy recorre

aos mecanismos de invocação remota e de reflexão da plataforma para saber como invocar

os métodos no objecto encapsulado. É por esta razão que em .NET todas as classes dos

objectos a encapsular tem de descender de ContextBoundObject ou de

MarshalByRefObject. Na Listagem 4.1 é apresentado o código de um programa que

cria um proxy dinâmico para a classe Foo utilizando os mecanismos de proxies dinâmicos

da plataforma para invocar o método DoBar().

Os AppDomain são ambientes isolados dentro do espaço de execução do CLR onde cada

aplicação é executada de uma forma independente. Podem existir, dentro da mesma

máquina virtual, diversos AppDomain activos simultaneamente. Podem também existir

aplicações em execução, em cada um deles, independentemente umas das outras. Os

objectos de aplicações dentro do mesmo AppDomain comunicam directamente entre si,

enquanto que, os objectos de aplicações em execução em diferentes AppDomain

Figura 25 – Proxies dinâmicos em .NET

Page 92: Instrumentação de Código na Plataforma .NET

78 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

comunicam através do envio de cópias de objectos entre diferentes AppDomain ou através

de proxies para os objectos dentro dos AppDomain externos. Os mecanismos de invocação

remota foram desenvolvidos para permitir a comunicação entre aplicações em execução

em diferentes AppDomain. Estes AppDomain podem pertencer ou não à mesma máquina

virtual. A utilização do mecanismo de invocação remota entre objectos existentes dentro

do mesmo AppDomain adiciona um considerável overhead à aplicação. Assim, é possível

concluir que a versatilidade oferecida pelo mecanismo de proxies dinâmicos tem o seu

custo. Este custo está no overhead associado à utilização conjunta do mecanismo de reflexão

e do mecanismo de invocação remota dentro de aplicações do mesmo AppDomain.

Um outro factor que não favorece a utilização deste mecanismo e se mostra incomodativo

para muito programadores é a obrigatoriedade de fazer com que as suas classes derivem

de MarshalByRefObject ou de ContextBoundObject. Esta herança forçada pode ser

um problema visto que, a plataforma .NET só implemente herança simples. Isto impede o

programador de utilizar qualquer outra herança sobre as classes envolvidas.

public sealed class Foo : MarshalByRefObject { public Foo() {} public void DoBar() { Console.WriteLine("This is Bar"); } } public class GenericProxy : RealProxy { Object subject; public GenericProxy (Object subject) : base(subject.GetType()){ subject = subject; } public override IMessage Invoke(IMessage msg){ IMessage ReturnMsg = RemotingServices.ExecuteMessage(subject, msg); return ReturnMsg; } } public class MyApp { static void Main(String[] args){ Foo pb = new GenericProxy(new Foo()).GetTransparentProxy(); pd.DoBar(); } }

Listagem 4.1 – Exemplo da utilização de proxies dinâmicos em .NET

Page 93: Instrumentação de Código na Plataforma .NET

ALTERNATIVA AOS PROXIES DINÂMICOS 79

Com o recurso à instrumentação de código e à biblioteca RAIL foi possível desenhar uma

alternativa que mantendo ou até mesmo aumentando a versatilidade do processo permite

obter uma performance muito superior à que se obtém utilizando as ferramentas da

plataforma. A estratégia consiste em identificar na aplicação quais as classes a encapsular e

qual o código a adicionar e executar aquando das chamadas aos métodos dessas classes.

Figura 26 – a) Estrutura original de uma aplicação em que a classe Cliente chama métodos nas classes A, B, C; b) Após um transformação em que as classes A, B, e C passam a ter os nomes _A, _B e _C e todas as referências para estas classes passam a ter como proxies novas classes com os nomes das originais; c) Adição à estrutura anterior da classe TypeResolver que vai permitir mudar a implementação dos objectos encapsulados por outra em tempo de execução.

Page 94: Instrumentação de Código na Plataforma .NET

80 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Utilizando a RAIL, as classes originais são renomeadas, são criadas novas classes com o

nome das originais e com a mesma assinatura que irão servir de proxies para as classes

originais. A cada método destas será adicionado o código que o programador definiu no

início do processo, sendo em seguida substituídas as referências das classes originais no

programa por referências para os seus proxies. Esta implementação corresponde ao

esquema b) na Figura 26.

A vantagem desta abordagem, quando compara com os proxies dinâmicos, é de não ser

necessário recorrer aos mecanismos de invocação remota e de reflexão, por conseguinte, a

aplicação fica com o desempenho semelhante ao de um programa igual codificado

manualmente. Ao realizar a instrumentação descrita em tempo de execução, é

acrescentado ao tempo total de execução o tempo gasto a modificar a aplicação. Em

aplicações que utilizem um número pequeno de classes e façam poucas chamadas a

métodos em classes proxy, a abordagem sugerida está em desvantagem. No entanto, a

maioria dos programas actuais utilizam centenas ou até milhares de classes e métodos, o

que resulta num elevado número de chamadas. Os programas nestas condições beneficiam

obviamente da abordagem RAIL, sendo o custo de introdução de proxies pago apenas no

momento em que se faz a instrumentação e não em todas as chamadas a métodos. É de

salientar que até mesmo o tempo da instrumentação pode ser eliminado da execução do

programa se a manipulação do assembly for realizada de modo estático.

É ainda possível melhorar esta técnica e oferecer um novo atractivo, permitindo modificar

a implementação de um programa em tempo de execução. Isto é fazer instrumentação

dinâmica de código. Partindo do mecanismo de proxies aqui descrito, substitui-se as

referências das classes encapsuladas dentro das classes proxy por referências para

interfaces com a mesma assinatura. As classes a encapsular devem implementar estas

interfaces. Seguidamente é adicionada uma nova classe à aplicação em instrumentação, a

classe TypeResolver. Esta classe vai ser responsável por instanciar os objectos que os

proxy vão encapsular e simultaneamente dizer qual a classe associada a cada nova

instância encapsulada. Este mecanismo é ilustrado no esquema c) da Figura 26.

Page 95: Instrumentação de Código na Plataforma .NET

ALTERNATIVA AOS PROXIES DINÂMICOS 81

Nesta nova versão do mecanismo de proxies, as classes proxy deixam de referenciar a classe

a encapsular directamente e passam a referenciar um interface. Durante a execução,

sempre que uma classe proxy tem de instanciar o objecto que encapsula, recorre a um

objecto da classe TypeResolver para saber qual será a classe do objecto a criar. A forma

como a TypeResolver identifica a classe a instanciar consiste na interpretação de um

ficheiro XML (Listagem 4.2), onde através de correspondência directa com o tipo da

interface referenciada no proxy encontra o tipo da classe para o novo objecto.

De forma a permitir a instrumentação em tempo de execução, o tipo da classe encapsulada

não pode ser obtido apenas uma vez (no início da execução do proxy) mas sempre que o

programador ou utilizador o requisitar. Assim, para alcançar este objectivo é adicionado

um novo método à classe proxy que controla uma variável de estado. Se esta variável

estiver a um, da próxima vez que existir uma chamada a um método desse proxy, antes de

satisfazer esse pedido, o proxy recorre ao objecto TypeResolver para criar uma nova

instância do objecto encapsulado, depois passa a usar essa instância como destino das

chamadas que recebe; Se a variável estiver a zero, o proxy segue o seu percurso de

execução normal.

Existe, no entanto, um problema nesta abordagem, depois da aplicação estar em execução

há algum tempo, é quase certo que o estado interno dos objectos encapsulados se

modificou desde a sua criação. É provável que os parâmetros dos construtores de cada

classe não sejam suficientes para criar um novo objecto que reflicta o estado interno do

objecto que o antecedeu. Uma abordagem possível para solucionar este problema seria

criar um novo método que permita reconstruir o estado interno do objecto encapsulado

<bindings xmlns="binding.xsd"> <bind> <original-assembly> Interfaces, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null </original-assembly> <original-type> Interfaces._IproxyBench </original-type> <proxy-assembly> RAILAppMod, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null </proxy-assembly> <proxy-type> RAILAppMod.ProxyBenchOriginal </proxy-type> </bind> </bindings>

Listagem 4.2 – Exemplo do conteúdo do ficheiro de configuração em XML

Page 96: Instrumentação de Código na Plataforma .NET

82 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

dentro de uma nova classe. O método a implementar pode, por exemplo, aceitar como

parâmetro um array de objectos, que se relacionam directamente com todos os campos e

propriedades existentes na classe. Este método faz corresponder cada um dos objectos

desse array a um dos campos ou propriedades que a classe que o implementa possui. A

solução implica, no entanto, que as classes utilizadas originalmente possuam métodos que

permitam construir o array de parâmetros recolhendo o estado interno de cada instância se

este não for se acesso público. Em caso de desfasamento em número ou tipo entre os

objectos no array e os campos ou propriedades na nova classe, o método utilizado para

reconstruir o estado interno deve lançar um erro ou estar preparado para lidar com essas

situações.

Em JAVA, também seria possível resolver este problema com recurso à serialização e

descerialização de objectos. Isto é, um objecto do tipo A depois de serializado, poderia ser

descerializado como um objecto do tipo B, desde que as propriedades de ambas as classes

fossem idênticas e as classes tivessem o mesmo identificador. Mesmo que estas duas

condições não se verificassem, ainda seria possível utilizar este processo através da

adaptação dos métodos envolvidos na leitura dos dados do objecto a partir das streams

para onde foram serializados. Em .NET não é possível utilizar os mecanismos de

serialização automáticos como em JAVA, pois para serializar um objecto de um tipo e

descerializa-lo como sendo de outro tipo, a classe inicial tem de conhecer a classe que a irá

substituir. Esta condição não pode ser satisfeita se a classe de substituição for criada após a

compilação e início de execução da aplicação.

4.1.1. Avaliação de Desempenho

Quando se pretende avaliar o desempenho de uma aplicação que realize instrumentação

de código é necessário ter em conta o momento em que ocorre a instrumentação. Quando

se trata de instrumentação estática não interessa saber quanto tempo é gasto a aplicar

determinada transformação mas sim qual o impacto da instrumentação na aplicação alvo.

Por outro lado, no caso da instrumentação dinâmica (aquela que ocorre enquanto a

aplicação está em execução) o tempo gasto na manipulação da aplicação já pode

influenciar o desempenho final da mesma.

Nesta secção é apresentado o resultado de alguns testes de performance para aplicações

modificadas com instrumentação de código estática, uma vez que é aquela para a qual a

RAIL possui suporte directo. O caso aqui discutido pretende comparar a performance de

três versões de uma aplicação, uma não instrumentada, uma versão modificada por

Page 97: Instrumentação de Código na Plataforma .NET

ALTERNATIVA AOS PROXIES DINÂMICOS 83

instrumentação de código e outra que utiliza ferramentas da plataforma .NET para

conseguir os mesmos resultados.

Para a realização dos testes comparativos de performance foi imaginado um cenário em

que se adicionam novas funcionalidades a um programa depois de toda lógica de negócio

estar implementada, utilizando para isso classes proxy. A classe a encapsular chama-se

ProxyBench e implementa a interface visível na Listagem 4.3.

Os testes consistem em fazer 10 milhões de chamadas a cada um dos métodos definidos na

interface através do objecto proxy contabilizando o tempo gasto para realizar essas

chamadas em cada método e o tempo total usado na execução. Os métodos implementados

na classe ProxyBench têm funções muito simples e operam quase todos sobre um campo

do tipo inteiro de nome Scale existente na mesma classe:

• void ping() – Duplica o valor do campo Scale existente na classe

ProxyBench.

• int ping(int a) – Multiplica o valor do campo Scale da classe ProxyBench

por o valor de a e retorna o resultado da operação.

• int add(int a, int b) – Adiciona a e b e multiplica o resultado por Scale,

retornando o valor final.

• int add(ref int a, ref int b, ref int c) – Adiciona os valores em

a e b e multiplica o resultado por Scale, sendo o resultado colocado em c e

retornado.

• Object ping(Object a) – Retorna o objecto a.

• Object pingref(ref Object a) – Retorna o objecto a.

public interface IProxyBench { void ping(); int ping(int a); int add(int a, int b); int add(ref int a, ref int b, ref int c); Object ping(Object a); Object pingref(ref Object a); }

Listagem 4.3 –Interface IProxyBench.

Page 98: Instrumentação de Código na Plataforma .NET

84 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Este conjunto de operações foi seleccionado por duas razões:

• Combinar diferentes tipos de parâmetros e tipos valores de retorno de modo a

verificar qual a sua influência no tempo gasto na invocação dos métodos com e

sem proxy.

• Estas operações são muito simples para não colocarem grande peso

computacional nas chamadas aos métodos e se poder observar mais facilmente o

overhead existente na inclusão dos proxies.

O programa original executa cada um dos métodos referidos 10 milhões de vezes e

contabiliza o tempo gasto. As novas versões do programa realizam exactamente as

mesmas operações mas sobre um objecto proxy que encapsula o objecto original:

• Versão #1 – O padrão de software proxy é implementado directamente em código

fonte e compilado utilizando o compilador de C# da plataforma.

• Versão #2 – Nesta versão é utilizado um proxy dinâmico implementado através

de ferramentas disponíveis na plataforma, tal como é descrito na Secção 4.1. É a

performance do mecanismo RealProxy que se pretende comparar com o

desempenho de uma proxy estática (Versão #1) e com a alternativa RAIL (Versão

#3).

• Versão #3 – Esta versão corresponde ao assembly produzido pela técnica descrita

na Secção 4.1, à qual é acrescentado um mecanismo que permite à classe proxy

modificar, em tempo de execução, o tipo do objecto que encapsula, sendo esta

proxy adicionada ao assembly por instrumentação de código com a RAIL.

Cada uma destas versões foi executada dez vezes na mesma máquina (Intel® Pentium® M

1700MHz, 512 MB de RAM, Sistema Operativo Windows XP SP2, plataforma .NET 1.1),

sendo os valores médios dos tempos de execução sumariados na Tabela 4.1. O formato dos

tempos exibidos na tabela é “HH:MM:SS.xxxxxxx”, sendo que, HH=Horas, MM=Minutos,

SS.xxxxxxx=Segundos e décimos de segundo. O tempo de execução total da versão

original (sem proxies) é aproximadamente 69 centésimos de segundo. Quando se

implementam classes proxy, manualmente codificadas, o tempo total de execução passa

para 1 segundo e 27 centésimos. Este acréscimo deve-se ao facto de ser necessário

instanciar o dobro dos objectos e executar mais instruções para chamar os métodos

originais.

Page 99: Instrumentação de Código na Plataforma .NET

ALTERNATIVA AOS PROXIES DINÂMICOS 85

Ver

são

#3

00:0

0:00

.680

9792

00:0

0:00

.701

0080

00:0

0:00

.801

1520

00:0

0:00

.771

1088

00:0

0:00

.721

0368

00:0

0:00

.670

9648

00:0

0:04

.446

3936

Ver

são

#2

00:0

1:16

.439

9152

00:0

1:32

.763

3872

00:0

1:41

.846

4480

00:0

2:29

.154

4736

00:0

1:31

.321

3136

00:0

1:47

.184

1232

00:1

0:18

.729

6896

Ver

são

#1

00:0

0:00

.170

2448

00:0

0:00

.210

3024

00:0

0:00

.230

3312

00:0

0:00

.290

4176

00:0

0:00

.180

2592

00:0

0:00

.190

2736

00:0

0:01

.271

8288

Ori

gin

al (s

em p

roxi

es)

00:0

0:00

.090

1296

00:0

0:00

.110

1584

00:0

0:00

.130

1872

00:0

0:00

.170

2448

00:0

0:00

.090

1296

00:0

0:00

.100

1440

00:0

0:00

.690

9936

ping()

ping(int)

add(int,int)

add(ref int,ref int,ref int)

ping(Object)

ping(ref Object)

Tem

po

de

Exe

cuçã

o T

otal

Page 100: Instrumentação de Código na Plataforma .NET

86 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Na segunda versão foram instanciadas proxies dinâmicas, nativas da plataforma .NET,

sendo as chamadas aos métodos efectuadas através destas. Neste caso o tempo de

execução aumenta para uns longos 10 minutos e 18 segundos. Apesar de ser um

mecanismo muito poderoso, um proxy dinâmico tem um overhead de invocação que pode

ter graves consequências na performance de um programa. Este facto pode ser explicado

não só pelo tempo gasto em obter uma assinatura da classe a encapsular (com recurso à

reflexão da plataforma) para criar um novo proxy, mas especialmente pelo mecanismo que

permite centrar a chamadas a qualquer método da classe encapsulada num único método

da classe proxy, recorrendo à API de invocação remota do .NET.

A alternativa proposta na Secção 4.1 que utiliza a RAIL, reúne o mesmo tipo de

funcionalidades que se obtêm com proxies dinâmicos e, embora continue a ter algum peso

na performance do programa, o tempo total de execução é muito inferior aos 10 minutos

anteriormente referidos. Neste caso, são gastos 4 segundos e 45 centésimos, continuando a

ser superior ao tempo de execução obtido com a implementação directa de proxies em C#.

O acréscimo de tempo deve-se ao facto do programa necessitar de ler informação a partir

de um ficheiro XML e de executar um conjunto de instruções extra que lhe permitam

modificar-se em tempo de execução. Removendo esta última capacidade ao programa

(renovar-se em tempo de execução), os tempos de execução obtidos passam a competir

directamente com os da versão #1.

4.2. Avaliação dos Mecanismos de Tratamento de Excepções

O bloco try-catch é um mecanismo para detecção e tratamento de comportamentos

excepcionais na execução dos programas existente nas linguagens de programação OO

modernas. Este mecanismo consiste em delimitar um bloco de código, detectar e tratar

qualquer excepção que ocorra dentro desse bloco, sendo o tratamento da mesma feito pelo

código presente nos handlers que sucedem a cláusula catch. Muitas vezes é comum

encontrar também handlers do tipo finally que ao contrário dos do tipo catch são

sempre executados e não apenas quando ocorre uma excepção dentro do bloco de código

protegido. Embora sejam referidos em todos os manuais de programação como uma

ferramenta imprescindível para construir aplicações robustas, muito programadores, por

má formação ou apenas por inércia fazem um uso desadequado destas ferramentas

[Müller 2002].

Page 101: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 87

No caso particular da linguagem JAVA existe também a clausula throws, que serve para

declarar os tipos de excepções que um método pode lançar. A sua utilização é obrigatória

no caso das excepções não serem tratadas dentro do próprio método, sendo também

obrigatório definir um bloco try-catch quando se realizam chamadas a métodos que

utilizem throws. Na base desta obrigatoriedade estão as excepções verificadas (checked)

que obrigam o compilador a procurar em todos os locais ao longo da pilha de invocação do

método que lança a excepção, o código de detecção e tratamento da mesma ou uma

cláusula throws semelhante. Se isto não ocorrer o compilador termina com uma mensagem

de erro. Em JAVA também existem excepções não verificadas (not checked) mas, na maioria

dos casos, referem-se a erros internos da máquina virtual.

A obrigatoriedade de declarar e apanhar excepções verificadas é muitas vezes prejudicial

para a robustez de uma aplicação. Isto porque, muitas vezes, os programadores silenciam

o mecanismo de excepções a fim de poderem compilar os seus programas concentrando-se

no código principal. Esta técnica de “silenciar as excepções” consiste em criar blocos try-

catch vazios, enganando o compilador.

Sendo este um problema do conhecimento comum [Müller 2002], a Microsoft, ao criar a

plataforma .NET, optou por uma política de não obrigatoriedade na declaração das

excepções (e.g. não existe um throws em C#). A declaração das excepções passou a fazer

parte das boas práticas de programação sendo utilizados comentários. Ou seja, o

programador possui um conjunto de comandos que utiliza para comentar o seu código e

produzir documentação que permite identificar o tipo de excepções que um método pode

lançar. A Microsoft evita assim o recurso a handlers vazios por parte dos programadores

mas, por outro lado, corre-se o risco de nunca se tratarem as excepções e de nunca se

comentar o código correctamente tornando quase impossível desenvolver programas

robustos. No entanto, uma excepção “calada” pode causar mais danos num programa do

que uma excepção que provoque a terminação do mesmo. Fazendo os testes adequados,

esta última situação é facilmente detectável e corrigível; a primeira não.

O estudo dos mecanismos de tratamento de excepções já tem muitos anos dentro dos

grupos de investigação de programação orientada aos objectos

[Bordiga1985;Dony1990;Meyer1988], sendo o seu intuito, o de aperfeiçoar os processos

envolvidos. Existem alguns trabalhos relativos a estas práticas de programação

[Lippert2000;Lopes2000] que relacionam a possibilidade de encarar os mecanismos de

tratamento de excepções como uma área de aplicação da AOP. Para justificar este cenário,

Page 102: Instrumentação de Código na Plataforma .NET

88 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Martin Lippert e Cristina Lopes analisaram o código de tratamento de excepções

implementado na JWAM [Breitling2000] e reescreveram por completo a aplicação

utilizando o paradigma AOP. Tal trabalho foi realizado partindo do código fonte da

JWAM e utilizando uma ferramenta de AOP, denominada AspectJ [Eclipse2005] que

estende a linguagem JAVA. Nesta secção serão analisadas várias aplicações .NET e, com

base nos dados recolhidos, é discutida a utilidade da abordagem AOP para implementação

dos mecanismos de tratamento de excepções nesta plataforma, à semelhança do que já foi

feito em JAVA. Para suportar tal processo, é utilizada a biblioteca RAIL.

Selecção das Aplicações em Estudo

Para realizar o estudo foram seleccionadas três aplicações alvo: Fotovision [Vertigo2005],

FxCop [Microsoft2005a] e o assembly System.Web.dll, parte dos ficheiros base da plataforma.

A escolha destas aplicações teve como principal razão serem desenvolvidas por empresas

de software, sendo produtos comerciais que representam, até certo ponto, o tipo e a

qualidade da programação para .NET na industria actual.

A Fotovision é uma aplicação de manipulação e partilha de imagens para a plataforma

.NET. Foi desenvolvida pela Vertigo Software, Inc, em parceria com a Microsoft, para ilustrar

as capacidades da plataforma .NET junto do grande público. O objectivo principal desta

aplicação é demonstrar as funcionalidades combinadas do Windows Forms, do ASP.NET,

dos XML Web Services e da .NET Compact Framework. Na realidade, a Fotovision é quase um

produto comercial pois é uma aplicação muito funcional, leve e prática. Foi escrita em

VB.NET e é constituída por um módulo para o computador pessoal (PC), outro para a

Internet e ainda outro para o Pocket PC. Neste estudo foi somente analisado o módulo para

o PC.

A FxCop é uma aplicação desenvolvida pela Microsoft que funciona como um verificador

de regras de boas práticas de codificação, design e implementação de bibliotecas de

software para .NET. Trata-se de uma ferramenta de análise de código que verifica os

assemblies .NET no que respeita à conformidade com as Microsoft .NET Framework Design

Guidelines [Microsoft2005c]. A FxCop utiliza os mecanismos de reflexão da plataforma,

parsing de código IL e construção de grafos de chamadas de funções para inspeccionar os

assemblies enquanto procura defeitos ao nível do design, segurança e performance. A FxCop

inclui uma Interface Gráfica com o Utilizador (GUI) e uma versão em linha de comandos,

assim como um Kit de Desenvolvimento de Software (SDK) para criar novas regras.

Page 103: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 89

A assembly System.Web.dll é parte integrante da plataforma .NET e inclui packages

importantes para o desenvolvimento de aplicações Web, nomeadamente:

• System.Web

• System.Web.Hosting

• System.Web.Mail

• System.Web.Security

• System.Web.UI

• Entre outros

Pela importância das classes implementadas no assembly System.Web.dll o tipo de atenção

dada aos mecanismos de tratamento de excepções neste assembly deve ser representativa

das práticas adoptadas ao longo do código de toda a plataforma. O System.Web.dll é

utilizado, por exemplo, no desenvolvimento de aplicações para servidores Web. A

execução destas aplicações não pode sofrer interrupções, pelo que, o código de detecção e

o tratamento de excepções tem de ser o mais correcto possível. Podemos inferir que, no

desenvolvimento do System.Web.dll, que vai ser parte integrante destas aplicações Web, a

Microsoft teve as mesmas preocupações.

Metodologia

A RAIL foi utilizada para realizar a análise dos mecanismos de tratamento de excepções

nestas três aplicações. Como a biblioteca cria uma representação OO dos assemblies,

também os blocos try-catch, finally e os blocos de tratamento de excepções são

representados por objectos dentro da biblioteca. Através do API da RAIL, é possível saber

que instruções estão dentro dos blocos de código protegidos, quantos handlers (ou blocos

de tratamento de excepções) um bloco de código protegido tem, que tipos de blocos de

tratamento de excepções, que tipos de excepções estão a ser apanhados, onde se iniciam e

acabam os blocos de tratamento de excepções e quais são as instruções IL que os compõem.

Esta informação foi utilizada por uma aplicação e reunida num ficheiro XML após a

análise das aplicações. Ao contrário dos estudos em Java [Lippert2000], devido à utilização

da RAIL para ler e modificar os ficheiros binários das aplicações, não é necessário aceder

ao código fonte das aplicações, mas apenas aos ficheiros executáveis.

Page 104: Instrumentação de Código na Plataforma .NET

90 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

A Listagem 4.4 descreve o conteúdo parcial do ficheiro XML gerado a partir da informação

recolhida na aplicação FxCop. No mesmo é possível observar as seguintes etiquetas:

<ExceptionReport>, que é a etiqueta principal do relatório; <ExceptionsTable>, que

representa cada instrução catch no código da aplicação; <Type>, que identifica o tipo de

excepções a ser tratada; <CodeSize>, a que corresponde o número de instruções

existentes no bloco de tratamento de excepções; e <Code>, que representa a lista de opcodes

(nome das instruções IL) das instruções do bloco de tratamento de excepções.

Na análise de toda a informação recolhida, os blocos finally foram ignorados por não

identificarem o tipo de excepção. No entanto, teve-se em conta a informação sobre o

número de blocos de tratamento de excepções existentes na aplicação, o tipo de excepções

tratadas e o tipo de instruções que compõem esses blocos de tratamento de excepções.

<ExceptionReport> <ExceptionsTable> <Type> Microsoft.Tools.FxCop.Common. ProjectVersionMismatchException </Type> <CodeSize> 12 </CodeSize> <Code> pop ldarg.0 ldstr ldstr call ldstr ldc.i4.0 ldc.i4.s ldc.i4.0 call pop leave.s </Code> </ExceptionsTable> <ExceptionsTable> <Type> System.ArgumentException </Type> <CodeSize> 5 </CodeSize> <Code> stloc.s ldloc.s ldloc.0 call leave.s </Code> </ExceptionsTable> … </ExceptionReport>

Listagem 4.4 – Exemplo do conteúdo do ficheiro XML gerado para a aplicação FxCop

Page 105: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 91

Recolha e Análise de Dados

A Tabela 4.2 reúne a informação recolhida a partir da aplicação Fotovision. Nesta tabela, a

primeira coluna identifica o tipo de excepção; a segunda o número total de handlers (blocos

catch ou blocos de tratamento de excepções) existentes na aplicação para cada tipo de

excepção; e a terceira, o número de blocos de tratamento de excepções com diferentes

corpos (i.e. compostos por sequências de instruções diferentes).

Da análise dos dados da tabela depreende-se que existem 50 blocos de tratamento de

excepções para o tipo de excepção System.Exception sendo apenas 12 deles diferentes.

Pode concluir-se que o programa contém muito código duplicado e escrever

repetidamente o mesmo código é uma tarefa ingrata para o programador, sendo mais

susceptível de gerar erros. O melhor caso possível, independentemente da tecnologia,

paradigma ou técnica utilizada, seria: escrever cada blocos de tratamento de excepções

apenas uma vez, reduzindo o número total de blocos de tratamento de excepções escritos

de um total de 53 para apenas 14, representando assim uma redução em 73.5% menos

blocos de tratamento de excepções do que no código original.

Considerando que o número médio de instruções em todos os blocos de tratamento de

excepções da aplicação é de 8.23, o número total de instruções diminuiria em 321

instruções aproximadamente se cada bloco de tratamento de excepções diferente fosse

escrito apenas uma vez. Não é possível no entanto inferir qual seria no número total de

linhas de código fonte poupadas, mas todas as instruções try-catch desapareceriam do

código fonte e todo o tratamento de excepções poderia estar concentrado num único lugar.

Tipos de excepções

Num

ero

de

bloc

os catch

Dif

eren

tes

hand

lers

System.Exception 50 12

System.Net.WebException 1 1

System.Threading.ThreadAbortException 2 1

Total 53 14

Tabela 4.2 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos handlers na Fotovision

Page 106: Instrumentação de Código na Plataforma .NET

92 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Isto iria simplificar a tarefa dos programadores e dos projectistas pois permitiria separar a

especificação do código de tratamento de excepções, do código da lógica de negócio.

Na Tabela 4.3 são reunidos os mesmos tipos de resultados mas para a aplicação FxCop, em

particular, para o assembly FxCop.exe. Existem neste assembly 46 blocos de tratamento de

excepções para excepções do tipo System.Exception, sendo apenas 17 diferentes entre

si. Para System.FormatException existem 5 blocos catch iguais. Por outro lado, para

os restantes nove tipos de excepções, todos os blocos de tratamento de excepções são

diferentes. Esta informação mostra que seria possível obter um decréscimo no número de

blocos de tratamento de excepções escritos, na ordem dos 52.3%, o que representaria

Tipos de excepções

Num

ero

de

bloc

os

catch

Dif

eren

tes

hand

lers

System.Exception 46 17

System.FormatException 5 1

System.IO.FileNotFoundException 2 2

System.ArgumentException 2 2

System.Runtime.InteropServices.COMException 2 2

System.IO.PathTooLongException 1 1

Microsoft.Tools.FxCop.Common.ProjectVersionMismatchException 1 1

System.NotSupportedException 1 1

System.Object 1 1

System.OverflowException 1 1

System.Xml.XmlException 1 1

Total 63 30

Tabela 4.3 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos handlers na FxCop

Page 107: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 93

menos 363 instruções IL, sendo que, o tamanho médio dos blocos de tratamento de

excepções é de 11 instruções.

No caso do assembly System.Web.dll, os dados recolhidos estão ilustrados na Tabela 4.4. Os

casos mais visíveis de repetição de código de blocos de tratamento de excepções ocorrem

para os tipos de excepções System.Exception e System.Object. É de notar que nesta

tabela surge uma excepção chamada System.Object. Isto acontece porque em .NET é

possível utilizar instruções catch sem qualquer parâmetro e, nesse caso, o CLR aceita que

seja lançado como excepção qualquer tipo de objecto. Os dados da tabela permitem

concluir que, no caso de se escrever cada tipo diferente de blocos de tratamento de

excepções apenas uma vez, o número de blocos de tratamento de excepções escritos

diminuiria em 53,2% o que representaria um total de menos 2094 instruções IL.

A análise realizada até ao momento incide sobre o número de blocos de tratamento de

excepções diferentes para cada tipo de excepção que pode ocorrer dentro das aplicações.

No entanto, nada impede que para diferentes tipos de excepções sejam escritos blocos de

tratamento idênticos. Isto significa que, se os blocos de tratamento de excepções diferentes

forem escritos apenas uma vez independentemente do tipo de excepção tratada, a

diminuição na quantidade de código escrito poderia ser superior à verificada na

abordagem anterior.

À luz dos dados recolhidos sob esta nova perspectiva conclui-se que na aplicação

Fotovision nada se alteraria. O número de blocos de tratamento de excepções diferentes

continuaria a ser 14, independentemente da comparação inter-tipos-de-excepções. No

entanto, na aplicação FxCop, o número total de blocos de tratamento de excepções

diferentes decairia de 30 para 24, o que representaria uma redução de 61.9% no total de

handlers escritos em relação ao original. Quanto à biblioteca System.Web.dll passariam a

existir apenas 104 tipos diferentes de blocos de tratamento de excepções, uma redução de

16.13% em relação aos 124 contabilizados originalmente. Isto iria reduzir o número total de

blocos de tratamento de excepções escritos para 40% do valor inicial, o que, considerando o

tamanho do assembly, representaria muito menos código.

Page 108: Instrumentação de Código na Plataforma .NET

94 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Tipos de excepções

Num

ero

de

bloc

os

catch

Dif

eren

tes

hand

lers

System.Exception 125 72

System.Object 106 26

System.ArgumentException 5 3

System.Configuration.ConfigurationException 4 2

System.Threading.ThreadAbortException 4 3

System.FormatException 4 1

System.Xml.XmlException 2 2

System.Threading.ThreadInterruptedException 2 2

System.IO.IOException 2 2

System.Data.ConstraintException 1 1

System.IndexOutOfRangeException 1 1

System.InvalidCastException 1 1

System.IO.DirectoryNotFoundException 1 1

System.IO.FileNotFoundException 1 1

System.IO.PathTooLongException 1 1

System.Reflection.TargetInvocationException 1 1

System.Runtime.Serialization.SerializationException 1 1

System.Security.SecurityException 1 1

System.Threading.ThreadInterruptedException 1 1

System.Web.HttpException 1 1

Total 265 124

Tabela 4.4 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos handlers em System.Web.dll

Page 109: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 95

Outro resultado interessante deste estudo está relacionado com o tamanho dos blocos de

tratamento de excepções. Foi contabilizado o número de blocos de tratamento de

excepções existentes consoante o número de instruções IL que os compõem. Os resultados

são apresentados na Figura 27, onde é claramente visível um padrão comum às três

aplicações: A maioria dos blocos de tratamento de excepções, aproximadamente 75%, tem

menos de 9 instruções.

Este é um detalhe interessante que permite concluir que todo o código de tratamento de

excepções é normalmente muito simples e, muitas vezes, inexistente. Esta afirmação é

justificada pelo facto de estar presente um número muito significativo de blocos de

tratamento de excepções com apenas 2 instruções (pop e leave), geradas pelos

compiladores .NET quando encontram um bloco de tratamento de excepções vazio. Este

tipo de bloco é muito comum na System.Web.dll, como é visível pela análise da Figura 27.

Figura 27 – Distribuição do tipo de handlers caracterizados pelo número de instruções nas três aplicações

Page 110: Instrumentação de Código na Plataforma .NET

96 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Um estudo mais aprofundado do código dos blocos de tratamento de excepções revelou

que a grande maioria destes são usados para tarefas de logging de erros e/ou notificação

do tipo de erros ocorridos ao utilizador.

Também foi reunida informação sobre excepções que podem ocorrer e que não são

tratadas de qualquer forma dentro da aplicação, i.e. contabilizou-se o número total de

instruções que podem gerar alguma excepção e não se encontram dentro de um bloco de

código protegido. Para se obterem estes dados, para além de se utilizar a RAIL, também é

necessário recorrer à documentação, tanto dos assemblies da plataforma como das próprias

aplicações, a fim de saber que tipo de excepções cada método pode lançar. É importante

relembrar que em .NET não é obrigatório declarar e apanhar as excepções, podendo-se

apenas documentar a possibilidade da sua ocorrência.

Assim, é gerado um novo ficheiro XML para guardar a informação recolhida, sendo este

criado usando um programa especialmente desenvolvido para o efeito. O aspecto final

desse ficheiro é visível na Listagem 4.5. A etiqueta <exception_report> identifica o

assembly alvo; <summary> inicia o segmento de resumo da informação recolhida;

<exception> identifica o tipo de excepção, assim como o número de vezes que a mesma

<exception_report assembly_path="FotoVision.exe" assembly_name="FotoVision, Version=1.0.1034.32473, Culture=neutral, PublicKeyToken=6250413531e7a2c0" type=""> ... <summary ...> ... <exception name="System.MissingMethodException" amount="7149" percentage="22,62986%"> </exception> <exception name="System.NullReferenceException" amount="9426" percentage="29,83761%"> </exception> ... <exception name="System.OverflowException" amount="287" percentage="0,9084866%"> </exception> <exception name="System.Security.SecurityException" amount="7898" percentage="25,00079%"> </exception> ... </summary> </exception_report>

Listagem 4.5 – Exemplo do conteúdo do ficheiro XML gerado para a aplicação FotoVision

Page 111: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES 97

foi encontrada sem estar a ser tratada e o seu peso no total dos tipos de excepções

encontradas nesta situação.

Tipo de Excepção

mer

o d

e in

stru

ções

af

ecta

das

%

System.NullReferenceException 9426 29,8

System.Security.SecurityException 7898 25

System.MissingMethodException 7149 22,6

System.MissingFieldException 2724 8,62

System.Security.VerificationException 1950 6,17

System.OutOfMemoryException 968 3,06

… … …

Foto

visi

on

System.NullReferenceException 14725 29,6

System.Security.SecurityException 10828 21,8

System.MissingMethodException 10002 20,2

System.MissingFieldException 6047 12,2

System.Security.VerificationException 3815 7,68

System.OutOfMemoryException 2053 4,13

… … …

FxC

op

System.NullReferenceException 25479 26

System.Security.SecurityException 22529 23

System.MissingMethodException 15664 16

System.Security.VerificationException 15601 15,9

System.MissingFieldException 9449 9,66

System.OutOfMemoryException 2887 2,95

… … …

Sys

tem

.Web

.dll

Tabela 4.5 – Tipos de excepções e número de instruções passíveis de as gerar

Page 112: Instrumentação de Código na Plataforma .NET

98 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

A informação extraída das três aplicações encontra-se reunida na Tabela 4.5. É interessante

reparar, por exemplo, que o “top três” das instruções não protegidas é comum nas três

aplicações. A saber:

• System.NullReferenceException

• System.Security.SecurityException

• System.MissingMethodException

No entanto, a razão principal que motivou este último estudo foi descobrir se alguns dos

tipos de excepções para os quais existem handlers ainda podem ser geradas por instruções

fora de um bloco de código protegido, ou dentro de um destes mas para um tipo de

excepção diferente.

De forma a não aumentar muito o seu tamanho, a Tabela 4.5 não mostra todos os tipos de

excepções que podem ocorrer foram dos blocos de código protegido, apenas os mais

frequentes. No entanto, o estudo aprofundado dos resultados obtidos mostra claramente

que no caso da aplicação Fotovision nenhum dos tipos de excepções que já são tratados

pode acontecer fora dos blocos de protecção. Na FxCop isso acontece apenas com o tipo

System.OverflowException. Sendo isto visível em 110 localizações diferentes. Dentro

do assembly System.Web.dll, essa situação repete-se em 4 tipos de excepções:

System.Security.Exception, System.InvalidCastException,

System.IndexOutOfRangeException e System.ArgumentException.

Não se pode afirmar que não existem outros tipos de mecanismos a impedir estas

instruções de gerar excepções, apenas é possível confirmar que as instruções em questão

não se encontram dentro de um bloco de código protegido. Este dado pode contribuir para

aumentar a margem de erro da análise. No entanto, após uma inspecção visual do código

IL, numa amostra representativa dos locais identificados, não foi encontrado qualquer

mecanismo que impedisse o lançamento do tipo de excepção esperado.

Para finalizar esta secção são enumeradas algumas conclusões quantitativas e qualitativas:

• Análise Quantitativa – Dois handlers são iguais se tiverem o mesmo

comportamento, i.e. se forem classificados de acordo com a sequência de

instruções que executam. Um dos resultados mais interessantes deste estudo é ser

possível reduzir o número total de handlers escritos por tipo de instrução para um

Page 113: Instrumentação de Código na Plataforma .NET

PROGRAMAÇÃO ORIENTADA AOS ASPECTOS 99

valor próximo de 59.67% se cada tipo diferente de handler for implementado uma

única vez. A diminuição do número de handlers seria ainda maior, cerca de

65.13%, se cada handler fosse escrito uma única vez independentemente do tipo de

excepção associada.

• Análise Qualitativa – A separação entre o código da lógica de negócio e o código

de tratamento de excepções evita possíveis confusões e erros fruto de alguma

interdependência entre ambos [Dunn1991]. O código das aplicações torna-se mais

legível e as tarefas de debug mais simples com uma separação distinta entre os

dois tipos de código. O programa torna-se muito mais tolerante a modificações

bruscas na sua arquitectura e design, à reimplementação da lógica de negócio que

pode, em casos extremos, nem afectar o código de tratamento de excepções.

Simultaneamente, se o programador se concentrar apenas na implementação da

lógica de negócio, é provável que cometa menos erros. O mesmo acontecerá

quando tiver de se preocupar apenas com o código de tratamento de erros, pelo

que a qualidade deste aumentará, e a existência de handlers vazios será eliminada

por completo. Ao centralizar o código de tratamento de excepções num só lugar

evita-se o copy-paste e a reprodução de handlers ao longo do código da aplicação.

Por outro lado, também se pode afirmar que quando o código de tratamento de

excepções tem de recorrer a muitos objectos pertencentes ao domínio da lógica de

negócio a codificação em separado pode aumentar a complexidade do código

produzido.

4.3. Programação Orientada aos Aspectos A Programação Orientada aos Aspectos (AOP) teve a sua origem em meados dos anos 90

nos laboratórios do Xerox Park. Este paradigma baseia-se na ideia que existem

determinados problemas que são comuns a várias partes (classes, métodos, propriedades,

recurso) de uma aplicação que não estão directamente relacionados com a lógica de

negócio a implementar. A esses problemas ou, mais concretamente, às suas soluções, é

dado o nome de aspectos. Alguns exemplos de aspectos são: mecanismos de logging,

segurança, qualidade de comunicação, disponibilidade de recursos, entre muitos outros.

À localização dos aspectos ou ao momento da execução do programa onde estes aspectos

se manifestam é dado o nome de pointcut. Um pointcut é, na realidade, um conjunto de join

points (i.e. posições no fio de execução de um programa). Os aspectos, depois de

Page 114: Instrumentação de Código na Plataforma .NET

100 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

identificados, são associados aos pointcuts (i.e. de uma forma transversal a todas as classes

e métodos do programa) sob a forma de advices ou, mais simplesmente, métodos.

O encadeamento do código dos advices com o código principal da aplicação é feito numa

fase posterior e não é mais que uma colagem de novas instruções dentro dos métodos a

executar. Esta fase é vulgarmente chamada de Weaving.

Desde os primeiros tempos que o tratamento de excepções foi identificado como uma área

de aplicação do paradigma AOP (i.e. foi identificado como um aspecto). O estudo

efectuado na secção anterior mostra claramente que pode existir uma diminuição

substancial na quantidade de código fonte de um programa, até mesmo um aumento da

sua qualidade, se os diferentes blocos de tratamento de excepções forem escritos um só

vez. O AOP permite aplicar os mecanismos de tratamento de excepções de forma separada

e transversalmente a toda a aplicação, isto permite que o código dos blocos de tratamento

de excepções seja escrito uma só vez e depois aplicado a diversos locais do programa.

Nesta secção é discutida a forma de o fazer em .NET utilizando instrumentação de código.

Na plataforma .NET existem formas diferentes de implementar o tratamento de excepções,

quando comparadas com a abordagem tradicional. Por exemplo, o Exception Management

Application Block (EMAB) [Jone2002] permite de uma forma simples e flexível fazer o

registo da ocorrência de excepções usando apenas uma linha de código, sendo possível

fazer o log da informação sobre uma excepção directamente no Event Log do Windows;

estender esta funcionalidade para fazer esse registo em bases de dados ou mesmo notificar

o utilizador sem afectar o código da lógica de negócio. Numa arquitectura de

componentes, como é a das aplicações .NET, o EMAB será apenas mais um bloco.

Existe, no entanto, um senão nesta abordagem do EMAB: é necessário escrever pelo menos

uma linha de código por cada handler, pelo que esta linha acaba por ser repetida inúmeras

vezes ao longo da aplicação, não sendo uma solução elegante. No entanto, esta linha de

código pode ser identificada como um aspecto, conduzindo a uma abordagem AOP do

problema.

Neste trabalho as excepções são encaradas como aspectos e a metodologia consiste em

procurar todos os métodos susceptíveis de gerar um determinado tipo de excepção para

posteriormente colocar o seu código dentro de um bloco de protecção com um código de

tratamento predeterminado. Este objectivo é alcançado através de duas técnicas,

ligeiramente diferentes, descritas nas duas próximas subsecções. A primeira envolve

Page 115: Instrumentação de Código na Plataforma .NET

PROGRAMAÇÃO ORIENTADA AOS ASPECTOS 101

alguma intrusão no código da lógica de negócio enquanto que a segunda é completamente

autónoma.

4.3.1. Utilização de Custom Attributes

A primeira técnica recorre a Custom Attributes [ECMA2002] para identificar o tipo de

excepções que um método pode lançar. Os Custom Attributes são utilizados como

marcadores, marcadores especiais é certo, cuja única função é servir de “comentários” (i.e.

anotações) capazes de sobreviver à compilação e indicar que tipos de excepções um

método pode lançar.

A abordagem consiste em utilizar a RAIL para percorrer o código IL de cada método,

verificando que excepções podem ocorrer, sendo lançadas do mesmo. De seguida, os

métodos em questão são marcados com uma anotação1 especial (ExceptionAttribute),

definido dentro da RAIL, que irá permitir mais tarde fazer o tratamento dessas excepções

como aspectos.

Na Listagem 4.6-a), encontra-se o código utilizado para definir a classe

ExceptionAttribute. Em b), na mesma listagem, é visível um exemplo de um método

marcado com este atributo para os tipos de excepção System.IO.IOException e

System.OverflowException. Finalmente, o último bloco de código da listagem, mostra

o código de instrumentação que pode ser escrito utilizando a RAIL para que os métodos

que possuem atributos ExceptionAttribute do tipo de excepção

System.IO.IOException sejam protegidos por blocos try-catch com um handler que

permite fazer o registo dessa excepção.

Ao ser executado o código de instrumentação, este percorre toda a estrutura OO que

representa a aplicação à procura de métodos marcados com o atributo

ExceptionAttribute para a instância System.IO.IOException. Sempre que um

desses métodos é encontrado, é criada uma nova tabela para conter a informação sobre os

blocos try-catch existentes, sendo introduzida uma nova entrada nesta tabela, definindo

um bloco de protecção que se inicia com a primeira instrução do método e terminando na

última instrução antes do return do método.

1 Ao longo desta secção, quando nos referirmos a “anotação” deve entender-se que se está a falar de

um “Custom Attribute”.

Page 116: Instrumentação de Código na Plataforma .NET

102 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

No exemplo da listagem anterior apenas se definiu um tipo de handler para o bloco try-

catch, cuja funcionalidade se resume a apanhar e fazer log da excepção. No entanto,

podem ser adicionados novos tipos de handler, tendo sempre o cuidado de os registar na

enumeração HandlerCode que serve de argumento ao método que realiza a

instrumentação.

A escrita do código que vai produzir os handlers é a tarefa mais complexa associada a esta

abordagem, especialmente se o código a gerar possuir referências aos argumentos do

método onde vai ser inserido ou às suas variáveis locais. No entanto, tal não é uma tarefa

impossível e pode ter soluções muito simples dependendo da proficiência do

a) [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=false)] public class ExceptionAttribute : System.Attribute { private Type exceptionType; public ExceptionAttribute(Type exceptionType) { this.exceptionType = exceptionType; } public Type ExceptionType { get { return this.exceptionType; } } } ----------------------------------------------------------------- b) [Exception(typeof(System.IO.IOException))] [Exception(typeof(System.OverflowException))] public static void Main(string [] args) { //Corpo do método } ----------------------------------------------------------------- c) RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RType rt = rAssembly.GetRTypeRef( "System.IO.IOException"); AddExceptionHandling aeh = new AddExceptionHandling( rt,HandlerCode.CatchAndLog,true); rAssembly.Accept(aeh); rAssembly.SaveAssembly("Foo.exe");

Listagem 4.6 – a) A classe que define o Custom Attribute utilizado como marcador nos métodos; b) Exemplo de um método com dois atributos/marcadores; c) O código de instrumentação capaz de adicionar um bloco de protecção e um handler em todos os métodos que podem lançar uma System.IO.IOException.

Page 117: Instrumentação de Código na Plataforma .NET

PROGRAMAÇÃO ORIENTADA AOS ASPECTOS 103

programador. Este código de handling é adicionado ao método alvo da instrumentação e o

tipo de excepção a apanhar é registado na tabela de excepções que foi adicionada ao

objecto que representa o método. Finalmente, é adicionado um novo ponto de retorno ao

método. Ao terminar a instrumentação, os Custom Attributes, que já cumpriram o seu

papel, são eliminados do assembly.

Para o leitor atento, existe uma desvantagem que é imediatamente visível em relação ao

método tradicional: a granularidade dos blocos protegidos está ao nível do método e não

ao nível de um grupo de instruções como normalmente acontece. Conseguir adicionar

blocos protegidos com uma granularidade menor, desta forma, seria uma tarefa bastante

complicada pois quando um bloco se inicia tem de ser garantido que a stack no CLR se

encontra vazia, assim como tem de ser completamente limpa antes da execução abandonar

um destes blocos. Assegurar o cumprimento destas regras conduziria, inevitavelmente, a

complexas manipulação da pilha de execução do CLR com o único objectivo de validar a

posição de início e de fim de um bloco de código protegido. Uma solução de recurso para

conseguir delimitar o início e o fim dos blocos foi fornecer ao programador novas

ferramentas, sob a forma de dois métodos estáticos, que não executam qualquer instrução,

para identificar estas posições no código. Aquando da instrumentação, as chamadas a estes

métodos são removidas.

Page 118: Instrumentação de Código na Plataforma .NET

104 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

4.3.2. Tratamento de Excepções Automático

A segunda técnica testada não necessita de recorrer a Custom Attributes ou a qualquer tipo

de marcador no código da aplicação original, logo não há qualquer tipo de mistura entre o

código da lógica de negócio e o código de tratamento de excepções.

A técnica consiste em fazer uso dos relatórios XML referidos na Secção 4.2, em particular o

documento que identifica as instruções passíveis de lançar excepções e que não se

encontram dentro de nenhum bloco de código protegido. Na Listagem 4.5 apenas foi

exibida a secção final do ficheiro XML produzido, ou seja, o resumo. No entanto, este

ficheiro possui muito mais informação como, por exemplo, a identificação de cada

instrução passível de gerar erro, a identificação do método onde esta se encontra, qual o

seu índice e qual o tipo de excepção que pode lançar. Uma amostra do conteúdo do

ficheiro é apresentada na Listagem 4.7-a).

a) <exception_report assembly_path="FxCop.exe" assembly_name="FxCop, Version=1.30.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type=""> <method name="Microsoft.Tools.FxCop.UI. DictionaryView.OnVisibleChanged(System.EventArgs)"> <exception type="code" id="Exception1"> <name> System.ArithmeticException </name> <code_line> 8 </code_line> </exception> <exception type="code" id="Exception2"> <name> System.DivideByZeroException </name> <code_line> 8 </code_line> </exception> <exception type="code" id="Exception3"> ... ----------------------------------------------------------------- b) RAssemblyDef rAssembly = RAssemblyDef.LoadAssembly("Foo.exe"); RType rt = rAssembly.GetRTypeRef( "System.IO.IOException"); AddExceptionHandling aeh1 = new AddExceptionHandling( rt, HandlerCode.CatchAndLog, "exception_report.xml"); rAssembly.Accept(aeh1); rAssembly.SaveAssembly("Foo.exe");

Listagem 4.7 – a) Exemplo do conteúdo do ficheiro XML gerado para a aplicação FotoVision; b) Exemplo do código de instrumentação.

Page 119: Instrumentação de Código na Plataforma .NET

PROJECTOS DE TERCEIROS 105

Utilizando o API da RAIL, é possível utilizar esta informação e cruzá-la com a estrutura

OO da aplicação a instrumentar. Para cada tipo de excepção que se pretende tratar na

aplicação é produzida uma tabela que identifica os métodos em que esse tipo de excepção

pode ocorrer. Esta tabela é construída com base na informação do referido ficheiro XML. O

passo seguinte é semelhante ao que foi descrito na abordagem anterior, ou seja, criar uma

tabela de excepções por cada método visitado (se esta já não existir), marcar um novo bloco

de código protegido, adicionar o código do handler ao método, actualizar a tabela de

excepções e adicionar uma nova instrução de retorno ao método.

Na Listagem 4.7-b) é visível o código C# utilizado para realizar a instrumentação descrita

para o tipo de excepção System.IO.IOException, criando blocos de código protegido

e handlers em todos os métodos do assembly passíveis de lançar a excepção em questão.

Não existem alterações visíveis de desempenho entre uma aplicação em que os

mecanismos de tratamento de excepções foram escritos da forma tradicional e uma

aplicação modificado por meio das técnicas propostas. Isto pode ser justificado pelo facto

de que o código intermédio gerado é semelhante, ou igual, em qualquer um dos cenários.

Por outro lado, a abordagem com instrumentação de código não possui as deficiências que

outras abordagens AOP possuem, onde se incluem a complexidade acrescida relacionada

com as mudanças de contexto para executar código extra ou a criação de novos objectos de

suporte ao mecanismo.

As técnicas descritas, apesar de rudimentares, já demonstram uma grande utilidade e

podem facilmente evoluir e suprir alguns dos problemas apontados ao longo desta

discussão como, por exemplo, a complexidade existente na geração do código para alguns

handlers. Na verdade um grande conjunto de utilizadores da RAIL utiliza-a para criar

ferramentas de AOP que permitem fazer exactamente este tipo de modificações.

4.4. Projectos de Terceiros Desde Outubro de 2003 que o código fonte da RAIL está disponível para download no site

do projecto em http://rail.dei.uc.pt. Desde então, já foram realizado mais de 600 downloads

do código e mais de 200 dos ficheiros binários, sendo todas as semanas recebidos por e-

mail relatórios de erros, sugestões de novas funcionalidades e declarações de utilizadores a

demonstrarem o seu interesse na biblioteca.

Page 120: Instrumentação de Código na Plataforma .NET

106 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO

Nesta secção são apresentados alguns dos projectos, a decorrer um pouco por todo o

mundo, que utilizam ou já utilizaram a RAIL de alguma forma. Não é possível fazer uma

lista completa ou uma descrição exaustiva de cada um mas os projectos aqui enumerados

são suficientes para se compreender algumas das aplicações de uma biblioteca de

instrumentação na plataforma .NET. Os dados apresentados foram recolhidos de diversos

sites na Internet assim como de inúmeros e-mails enviados pelos utilizadores1.

Uma das aplicações da instrumentação de código com maior destaque é o

desenvolvimento de ferramentas de AOP. O NetAOP é uma biblioteca de código aberto

distribuída sob licença LGPL que começou por ser uma transferência para plataforma

.NET do JBOSS AOP. Esta biblioteca utiliza a RAIL para ler, modificar e produzir

assemblies, permitindo a definição de pointcuts no acesso a campos, propriedades e

métodos. A RAIL é também utilizada para adicionar interfaces e novas classes aos

assemblies a fim de permitir a intercepção do código. A DotNetGuru, uma organização de

programadores Francesa, está também a desenvolver a Aspect Weaver for .Net e utilizou a

RAIL, para fazer a “colagem” dos advices nos programas. O mesmo foi feito pelos

argentinos da SetPoint.

Um grupo de investigadores suíços do Swiss Federal Institute of Technology utiliza a RAIL

para adicionar aos programas desenvolvidos na linguagem de programação Eiffel

[Meyer1992] as capacidades de invocação remota (remoting) da plataforma .NET,

inexistentes naquela linguagem.

Na Internet foi encontrada a referência para a implementação de um programa para

correcção de outros programas (patcher) que também utiliza a RAIL. Trata-se de uma

ferramenta que permite corrigir os erros de um programa sem ser necessário o acesso ao

seu código fonte.

Ao nível da confiabilidade de software, a RAIL foi utilizada, por exemplo, para realizar

testes por mutação. Este tipo de testes consiste em provocar falhas e verificar como é que o

programa reage, as falhas são geradas pela mutação do código.

A RAIL também foi utilizada para transformar, dentro do código de uma aplicação, algo

abstracto (e.g. chamadas a métodos) em algo concreto (e.g. um objecto). A este processo

dá-se o nome de “message reify”. A RAIL foi utilizada para analisar o código IL e substituir

1 Note-se que como uma grande parte destes trabalhos ainda não estão publicamente disponíveis,

nem publicados, não é possível fazer uma referência formal para alguns deles.

Page 121: Instrumentação de Código na Plataforma .NET

PROJECTOS DE TERCEIROS 107

todas as chamadas a instruções call, callvirt e calli, por novas instruções que

passam o controlo para o objecto introduzido.

A NDepend é uma aplicação que permite determinar a qualidade do design e

implementação do código de uma aplicação .NET. Esta ferramenta gera um relatório cuja

análise permite avaliar uma aplicação em termos de expansibilidade, reutilização e

facilidade de manutenção, permitindo ainda controlar as dependências entre os assemblies

das aplicações. Actualmente, a NDepend utiliza o ILReader para ler os assemblies, visto que é

uma ferramenta mais leve do que o RAIL, necessitando a NDepend apenas de ler os

assemblies e o código IL (praticamente todo o API do RAIL não seria usado).

As ferramentas de testes unitários permitem projectar testes para todos os métodos, de

todas as classes de uma aplicação. A execução dos testes é automática e sistemática, de

forma gerar um relatório que expõe a qualidade e robustez do código. A este nível, a RAIL

foi utilizado pela equipa de desenvolvimento do MbUnit [Stopford2005] para ler e

interpretar os assemblies, particularmente componentes como os Custom Attributes

utilizados para identificar os testes e os seus alvos dentro de cada aplicação.

Finalmente, e entre outros projectos, a RAIL está a ser utilizada para implementar

lightweight threads muito semelhantes ao que é feito em Java com as Picothreads [Begel1997].

É adicionado código a cada método (da aplicação a instrumentar), permitindo guardar e

serializar o estado da thread em execução, de forma a possibilitar a paragem e novo

arranque desta.

Page 122: Instrumentação de Código na Plataforma .NET
Page 123: Instrumentação de Código na Plataforma .NET

Conclusão

“Por vezes um conceito pode ser confuso não por ter um significado profundo mas porque é errado.”

— Edward O. Wilson

“Não se pode ensinar nada a um homem; apenas esperar que ele o aprenda por si.”

— Galileu Galilei

Neste capítulo são discutidas as conclusões da dissertação, sendo elaborado o balanço do

que foi o projecto e realizada uma reflexão sobre futuros desenvolvimentos.

Capítulo

5

Page 124: Instrumentação de Código na Plataforma .NET

110 CAPÍTULO 5 — CONCLUSÃO

5.1. Avaliação do Projecto RAIL e Trabalho Futuro Esta dissertação teve como foco central o desenvolvimento de uma biblioteca de

instrumentação de código para a plataforma .NET, o projecto RAIL. Este projecto foi

beneficiário de duas bolsas Microsoft Research SSCLI-ROTOR Grants consecutivas, tendo

sido na última atribuição o único projecto nacional agraciado.

Desde Outubro de 2003, altura em que foi disponibilizada no site do projecto o código

fonte da biblioteca, que o número de utilizadores tem vindo a aumentar. Embora não seja

possível apontar um número exacto, são crescentes as referências em sites de programação,

de engenharia de software, em blogs e sites de outros projectos para a RAIL. A partir desta

informação é possível concluir que o projecto teve um impacto muito positivo na

comunidade de investigação e até no meio empresarial.

Em pouco mais de dois anos de vida deste projecto foram ultrapassadas inúmeras

dificuldades inerentes à complexidade dos formatos binários das aplicações e à

implementação dos padrões de software de alto nível. Foram realizados muitos testes em

várias áreas de aplicação da instrumentação de código, com o objectivo de assegurar a sua

viabilidade. Um resultado secundário deste trabalho consistiu na produção de algumas

pequenas aplicações com fins muito específicos e na recolha de dados que permitiram

avaliar a qualidade dos processos e mecanismo utilizados.

O projecto em si está a entrar numa fase de maturação em que se pretende aumentar a

robustez do software, melhorar a API de acordo com as sugestões dos utilizadores,

acrescentar novas funcionalidades e aumentar a performance.

Existem actualmente alguns utilizadores externos, que devido à sua utilização da RAIL, já

contribuem para o melhoramente e aumento do código da biblioteca. Todo o código está

livremente disponível e acessível via um servidor CVS. Esta decisão de permitir a

contribuição de programadores exteriores ao grupo de investigação vem assegurar a

continuidade do projecto e a actividade dentro da pequena comunidade que se começa a

formar.

Têm sido dados passos importantes na divulgação da biblioteca através da publicação de

artigos em revistas e conferências internacionais, na participação em mailing-lists e na

realização de palestras em território nacional e internacional. De referir, além da

publicação numa conferência internacional com referee (ACM SAC’2005), a quando do

Page 125: Instrumentação de Código na Plataforma .NET

AVALIAÇÃO DO PROJECTO RAIL E TRABALHO FUTURO 111

lançamento da biblioteca, o grupo foi convidado a escrever um artigo convidado em

revista.

Associado ao desenvolvimento futuro da biblioteca está o estudo e desenvolvimento de

novos padrões de software para a realização de instrumentação de código a alto nível. Esta

actividade consistirá na implementação e avaliação da utilidade, performance e robustez

destes novos padrões. A RAIL veio facilitar e divulgar ainda mais a instrumentação de

código numa das plataformas virtuais com maior relevância na actualidade, a plataforma

.NET. A RAIL veio também fornecer novas ferramentas que permitirão ao programador e

à comunidade de investigadores que desenvolve o seu trabalho na plataforma .NET

realizar complicadas manipulações que não lhes seriam acessíveis sem um conhecimento

profundo da arquitectura e do funcionamento da plataforma.

Page 126: Instrumentação de Código na Plataforma .NET
Page 127: Instrumentação de Código na Plataforma .NET

Bibliografia

[Apache2003] Apache, "BCEL Project," Apache Software Foundation, 2003.

Disponível em: http://jakarta.apache.org/bcel/projects.html.

[Aycock2003] J. Aycock, "A Brief History of Just-In-Time," ACM Computing

Surveys, vol. 35, pp. 97-113, 2003.

[Begel1997] A. Begel, J. MacDonald, e M. Shilman, "PicoThreads: Lightweight

Threads in Java," Rel. Tec. Nº CS262 Class Project, UC Berkeley, 1997.

[Binder2001] W. Binder, J. Hulaas, A. Villazón, e R. Vidal, "Portable Resource

Control in Java: The J-SEAL2 Approach," em Proceedings of the ACM

Conference on Object-Oriented Programming, Systems, Languages,

and Applications (OOPSLA-2001), Florida, USA, ACM Press, 2001.

[Blosser2000] J. Blosser, "Explore the Dynamic Proxy API," 2000. Disponível em:

http://www.javaworld.com/javaworld/jw-11-2000/jw-1110-

proxy.html.

[Bordiga1985] A. Bordiga, "Language Features for Flexible Handling of Exceptions in

Information Systems," ACM Transactions on Database Systems, vol.

10(4), pp. 565-603, 1985.

[Breitling2000] H. Breitling, C. Lilienthal, M. Lippert, e H. Züllighoven, "The JWAM

Framework: Inspired By Research, Reality-Tested By Commercial

Utilization," em Proceedings of OOPSLA 2000 Workshop: Methods

and Tools for Object-Oriented Framework Development and

Specialization, 2000.

[Bruneton2002] E. Bruneton, R. Lenglet, e T. Coupaye, "ASM: A Code Manipulation

Tool to Implement Adaptable Systems," em Adaptable and extensible

component systems, Grenoble, França, 2002.

[Cabral2005] B. Cabral, P. Marques, e L. Silva, "RAIL: Code Instrumentation

for.NET," em Proceedings of the 2005 ACM Symposium On Applied

Computing (SAC'05), Santa Fé, New Mexico, U.S.A., ACM Press, 2005.

Page 128: Instrumentação de Código na Plataforma .NET

114 BIBLIOGRAFIA

[Chander1999] A. Chander, J. Mitchell, e I. Shin, "Mobile Code Security through Java

Byte Code modification," 1999. Disponível em:

http://theory.stanford.edu/~vganesh/project.html.

[Chiba2000] S. Chiba, "Load-Time Structural Reflection in Java," em Proceedings of

ECOOP 2000 - Object-Oriented Programming: 14th European

Conference, Lecture Notes in Computer Science, Sophia Antipolis and

Cannes, França, Springer-Verlag, 2000.

[Cisternino2003] A. Cisternino, "CLIFileReader Library," Universita de Pisa, Italia, 2003.

Disponível em:

http://dotnet.di.unipi.it/MultipleContentView.aspx?code=103.

[Cohen1998] G. Cohen, J. Chase, e D. Kaminsky, "Automatic Program

Transformation with JOIE," em USENIX Annual Technical

Symposium, New Orleans, Louisiana, USA, 1998.

[Dahm1999] M. Dahm, "Byte Code Engineering," em JIT '99 - Java-Informations-

Tage (Tagungsband), Dusseldorf, Germany, Springer-Verlag, 1999,

pp. 267-277.

[Dmitriev2004] M. Dmitriev, "Profiling Java Applications Using Code Hotswapping

and Dynamic Call Graph Revelation," em Proceedings of the Fourth

International Workshop on Software and Performance, Redwood

Shores, California, ACM Press, 2004.

[Dony1990] C. Dony, "Exception Handling and Object-Oriented Programming:

towards a synthesis," em Proceedings of OOPSLA/ECOOP ’90, 1990,

vol. 25(10), SIGPLAN Notices, ACM Press, Outubro 1990.

[DSG-CISUC2005] DSG-CISUC, "RAIL Project Web Site," DSG-CISUC, 2005. Disponível

em: http://rail.dei.uc.pt.

[Duncan1999] A. Duncan e U. Hölzle, "Load-Time Adaptation: Efficient and Non-

Intrusive Language Extension for Virtual Machines," Rel. Tec. Nº

TRCS99-09, Department of Computer Science University of California,

Santa Barbara, California, U.S.A., Abril 1999.

Page 129: Instrumentação de Código na Plataforma .NET

115

[Dunn1991] M. F. Dunn e J. C. Knight, "Software Reuse in an Industrial Setting: A

Case Study," em Proceedings of the 13th International Conference on

Software Engineering, Austin, Texas, U.S.A, IEEE Computer Society

Press, 1991.

[Eckel2001] B. Eckel, Thinking in Python, Mindview, Inc, 2001.

[Eclipse2005] Eclipse, "AspectJ," Eclipse Foundation, 2005. Disponível em:

http://eclipse.org/aspectj/.

[ECMA2002] ECMA, "Standard ECMA-335 Common Language Infrastructure

(CLI)," ECMA International, 2002. Disponível em: http://www.ecma-

international.org/publications/standards/ecma-335.htm.

[ECMA2003] ECMA, "Standard ECMA 334 C# Language Specification," ECMA

International, 2003. Disponível em: http://www.ecma-

international.org/publications/standards/Ecma-334.htm.

[Factor2004] M. Factor, A. Schuster, e K. Shagin, "Instrumentation of Standard

Libraries in Object-Oriented Languages: The Twin Class Hierarchy

Approach," em Proceedings of the 19th annual ACM SIGPLAN

Conference on Object-oriented programming, systems, languages, and

applications, Vancouver, BC, Canada, ACM Press, 2004.

[Ferber1989] J. Ferber, "Computational Reflection in Class Based Object-Oriented

Languages," em Proceedings on Object-oriented programming

systems, languages and applications, New Orleans, Louisiana, U.S.A.

ACM Press, 1989.

[Fu2004] C. Fu, B. G. Ryder, A. Milanova, e D. Wonnacott, "Testing of Java Web

Services for Robustness," em Proceedings of the 2004 ACM SIGSOFT

International Symposium on Software Testing and Analysis, Boston,

Massachusetts, U.S.A., ACM Press, 2004.

[Gamma1995] E. Gamma, J. Vlissides, J. Johnson, e R. Helm, Design patterns:

Elements of Reusable Object-Oriented Software, Reading, Addison-

Wesley, 1995.

Page 130: Instrumentação de Código na Plataforma .NET

116 BIBLIOGRAFIA

[Gosling2000] J. Gosling, B. Joy, G. Steele, e G. Bracha, The Java Language

Specification, Mountain View, California, U.S.A., Sun Microsystems,

Inc, 2000.

[Gough2001] J. Gough, Compiling for the.NET Common Language Runtime (CLR),

New Jersey, Prentice Hall, 2001.

[Jone2002] K. Jone, G. Malcolm, A. Mackman, e E. Jezierski, "Exception

Management Application Block for.NET. In Microsoft Patterns and

Practices for Application Architecture and Design," Microsoft

Corporation, 2002. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/dnbda/html/emab-rm.asp.

[Keller1998] R. Keller e U. Hölzle, "Binary Component Adaptation," em

Proceedings of European Conference on Object-Oriented

Programming (ECOOP’98), Lecture Notes in Computer Science, vol.

1445, Springer-Verlag, 1998, pp. 307.

[Kiczales1997] G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. Lopes, J.-M.

Loingtier, e J. Irwin, "Aspect-oriented programming," em Proceedings

of ECOOP'97, 11th European Conference, vol. 1241, Lecture Notes in

Computer Science, Finland, Springer-Verlag, 1997, pp. 220-242.

[Kiczales2001] G. Kiczales, E. Hilsdale, J. Hugunin, M. Kersten, J. Palm, e W. G.

Griswold, "An Overview of AspectJ," Lecture Notes in Computer

Science, vol. 2072, pp. 327-355, 2001.

[Kniesel2001] G. Kniesel, P. Costanza, e M. Austermann, "JMangler - A Framework

for Load-Time Transformation of Java Class Files," em IEEE Workshop

on Source Code Analysis and Manipulation (SCAM), 2001.

[Lafferty2003] D. Lafferty e V. Cahill, "Language-Independent Aspect-Oriented

Programming," em Proceedings of the 18th ACM SIGPLAN

Conference on Object-Oriented Programming (OOPSLA 2003),

Anaheim, California, U.S.A., 2003.

Page 131: Instrumentação de Código na Plataforma .NET

117

[Lam2002] J. Lam, "Cross-Language Load-Time Aspect Weaving on Microsoft's

Common Language Runtime," em demonstração na 1st International

Conference on Aspect-Oriented Software Development (AOSD2002),

University of Twente, Enschede, The Netherlands, 2002.

[Larus1995] J. R. Larus e E. Schnarr, "EEL: Machine-Independent Executable

Editing," em Proceedings of the ACM SIGPLAN 1995 Conference on

Programming Language Design and Implementation, La Jolla,

California, U.S.A., ACM Press, 1995.

[Lian1998] S. Lian e G. Bracha, "Dynamic Class Loading in the Java Virtual

Machine," em Object-Oriented Programming Systems Languages and

Applications (OOPSLA’98), Vancouver, Canada, ACM Press, 1998.

[Lindholm1999] T. Lindholm e F. Yellin, The Java Virtual Machine Specification, 2ed,

Addison-Wesley Professional, 1999.

[Lippert2000] M. Lippert e C. V. Lopes, "A Study on Exception Detection and

Handling Using Aspect-Oriented Programming," em Proceedings of

the 22nd International Conference on Software Engineering (ICSE

2000), New York, NY, U.S.A., ACM Press, 2000.

[Lopes2000] C. Lopes, J. Hugunin, M. Kersten, M. Lippert, E. Hilsdale, e G.

Kiczales, "Using AspectJ For Programming The Detection and

Handling of Exceptions," em Object-Oriented Technology: ECOOP

2000 Workshop Reader, Sophia Antipolis and Cannes, France, 2000,

vol. 1964, Lecture Notes in Computer Science, Springer-Verlag.

[Malenfant1992] J. Malenfant, C. Dony, e P. Cointe, "Behavioral Reflection in a

Prototype-Based Language," em Workshop on Reflection and Meta-

Level Architectures (IMSA'92), Tokyo, 1992.

[McCarthy1960] J. McCarthy, "Recursive Functions of Symbolic Expressions and Their

Computation by Machine, Part I," Commun, ACM, vol. 3, pp. 184-195,

1960.

[Meyer1988] B. Meyer, Object-oriented software construction, New York, Prentice-

Hall, 1988.

Page 132: Instrumentação de Código na Plataforma .NET

118 BIBLIOGRAFIA

[Meyer1992] B. Meyer, Eiffel The Language, Prentice-Hall, 1992.

[Microsoft2002] Microsoft, "RealProxy class.NET Framework Class Library," Microsoft

Corporation, 2002. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cpref/html/frlrfsystemruntimeremotingproxiesrealproxyclasstopi

c.asp.

[Microsoft2004a] Microsoft, "F# Programming Language," Microsoft Research,

Cambridge, UK, 2004. Disponível em:

http://research.microsoft.com/projects/ilx/fsharp.aspx.

[Microsoft2004b] Microsoft, "System.Reflection.Emit," em .NET Framework Class

Library, Microsoft Corporation, 2004. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cpref/html/frlrfsystemreflectionemit.asp.

[Microsoft2004c] Microsoft, "ContextBoundObject Class, " em .NET Framework Class

Library, Microsoft Corporation, 2004. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cpref/html/frlrfsystemcontextboundobjectclasstopic.asp.

[Microsoft2004d] Microsoft, "Remotable Objects, " em.NET Framework Class Library,

Microsoft Corporation, 2004. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cpguide/html/cpconRemotableObjects.asp.

[Microsoft2005a] Microsoft, "Metadata Unmanaged API," em .NET Tool Developers

Guide," Microsoft Corporation, 2005.

[Microsoft2005b] Microsoft, "FxCop web site," Microsoft Corporation, 2005. Disponível

em: http://www.gotdotnet.com/team/fxcop/.

[Microsoft2005c] Microsoft, "Design Guidelines for Class Library Developers, em.NET

Framework General Reference," Microsoft Corporation, 2005.

Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cpgenref/html/cpconnetframeworkdesignguidelines.asp.

Page 133: Instrumentação de Código na Plataforma .NET

119

[Microsoft2005d] Microsoft, "MSIL Disassemble (ILDasm.exe),” em .NET Framework

Tools, Microsoft Corporation, 2005. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/cptools/html/cpconmsildisassemblerildasmexe.asp.

[Müller 2002] A. Müller e G. Simmons, "Exception Handling: Common Problems

and Best Practice with Java 1.4," Sun Microsystems, 2002. Disponível

em:

http://www.old.netobjectdays.org/pdf/02/papers/industry/1430.p

df.

[Novell2005a] Novell, "MONO Project," Novell, 2005. Disponível em:

http://www.mono-project.com.

[Novell2005b] Novell, "CECIL - MONO Project," Novell, 2005. Disponível em:

http://www.mono-project.com/Cecil.

[PLAS2005] PLAS, "PEAPI and PERWAPI," Programming Languages and Systems

Research Group - Queensland University, Brisbane, Australia, 2005.

Disponível em:

http://www.plas.fit.qut.edu.au/perwapi/Default.aspx.

[Remy1998] D. Remy e J. Vouillon, "Objective ML: An Effective Object-Oriented

Extension to ML," Theory and Practice of Object Systems, vol. 4(1), pp.

27-50, 1998.

[Roeder2004] L. Roeder, "ILReader Library," 2004. Disponível em:

http://www.aisto.com/roeder/dotnet/.

[Stroustrup1997] B. Stroustrup, The C++ Programming Language, 3 ed, Addison-

Wesley Pub Co, 1997.

[Stutz2002] D. Stutz, "The Microsoft Shared Source CLI Implementation,"

Microsoft Corporation, 2002. Disponível em:

http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/Dndotnet/html/mssharsourcecli.asp.

[Stutz2003] D. Stutz, T. Neward, e G. Shilling, Shared Source CLI Essentials,

U.S.A., O’Reilly, 2003.

Page 134: Instrumentação de Código na Plataforma .NET

120 BIBLIOGRAFIA

[Stopford2005] A. Stopford, "MbUnit," 2005. Disponível em:

http://mbunit.tigris.org/.

[Sun1999] Sun, "Dynamic Proxy Classes," Sun Microsystems, Inc, 1999.

Disponível em:

http://java.sun.com/j2se/1.3/docs/guide/reflection/proxy.html.

[Sun2001] Sun, "HotSwap," Sun Microsystems, Inc, 2001. Disponível em:

http://java.sun.com/j2se/1.4.2/docs/guide/jpda/enhancements.htm

l#hotswap.

[Syme2001] D. Syme, "ILX: Extending the .NET Common IL for Functional

Language Interoperability," MS Research, 2001. Disponível em:

http://research.microsoft.com/projects/ilx.

[Tanter2002] E. Tanter, M. Ségura-Devillechaise, J. Noyé, e J. Piquer, "Altering Java

Semantics via Bytecode Manipulation," em Proceedings of Generative

Programming and Component Engineering (GPCE 2002), 2002, vol.

2487, Lecture Notes in Computer Science, Springer-Verlag, 2002, pp.

283-298.

[Truyen2000] E. Truyen, B. Robben, B. Vanhaute, T. Coninx, W. Joosen, e P.

Verbaeten, "Portable Support for Transparent Thread Migration in

Java," em Proceedings of the Joint Symposium on Agent Systems and

Applications/Mobile Agents (ASA/MA’2000), Zurique, Suiça, 2000,

vol., Lecture Notes in Computer Science, Springer-Verlag.

[Vall1999] R. Vall, R. e, P. Co, E. Gagnon, L. Hendren, P. Lam, e V. Sundaresan,

"Soot - a Java Bytecode Optimization Framework," em Proceedings of

the 1999 conference of the Centre for Advanced Studies on

Collaborative Research, Mississauga, Ontario, Canada, IBM Press,

1999.

[Vertigo2005] Vertigo, "Fotovision web site," Vertigo Software, Inc, 2005. Disponível

em: http://msdn.microsoft.com/library/default.asp?url=/library/en-

us/dnnetcomp/html/FotoVisionDesktop.asp.

Page 135: Instrumentação de Código na Plataforma .NET

121

[Welch1999] I. Welch e R. Stroud, "From Dalang to Kava — The Evolution of a

Reflective Java Extension," em Proceedings of Reflection ’99, 1999, vol.,

Lecture Notes in Computer Science, Springer-Verlag, 1999, pp. 2–21.

[Welch2000] I. S. Welch e R. J. Stroud, "Kava - a Powerful and Portable Reflective

Java (poster session)," em Addendum to the 2000 Proceedings of the

Conference on Object-Oriented Programming, Systems, Languages,

and Applications (Addendum), Minneapolis, Minnesota, U.S.A., ACM

Press, 2000.

[White2002] A. A. White, "SERP: Overview," 2002. Disponível em:

http://serp.sourceforge.net.

Page 136: Instrumentação de Código na Plataforma .NET

Lista de Publicações

A lista de publicações que se segue é o resultado do trabalho efectuado no decurso desta

dissertação.

Artigos em Revistas

B. Cabral, P. Marques, L. Silva, "IL Code Instrumentation with RAIL", in .NET

Developers Journal, Vol. 2(1), pp. 34-35, SYS-CON Media Publishers, Janeiro 2004.

Artigos em Conferências Internacionais

B. Cabral, P. Marques, L. Silva, "RAIL: Code Instrumentation for .NET", in Proc. of

the 2005 ACM Symposium On Applied Computing (SAC'05), ACM Press, Santa

Fé, New Mexico, USA, Março 2005.

B. Cabral, P. Marques, L. Silva, "RAIL: Code Instrumentation for .NET"(extended

abstract), in Proc. of the ACM OOPSLA'04 Conference Companion, ACM Press,

Vancouver, Canada, Outubro 2004.

Relatórios Técnicos

B. Cabral, “Exceptions as an Aspect: Feasibility of Using AOP to Enforce Exception

Handling in .NET” , Technical Report, CISUC, Julho 2004.

Palestras Convidadas

Maio 2005 – “Code Instrumentation in the CLR”, no Microsoft Research Academic

Summit - SSCLI Teaching and Research Workshop, São Paulo, Brazil, a convite da

Microsoft Research.

Abril 2005 – “Instrumentação de Código em .NET”, integrada no Curso de Pos-

Gradução em Engenharia da Aplicações Empresarias do Instituto Superior de

Engenharia do Porto a convite do Departamento de Engenharia Informática do

mesmo Instituto.

Page 137: Instrumentação de Código na Plataforma .NET

123

Maio 2004 - “Instrumentation in the .NET Platform”, no evento Microsoft Research

Academic Days Portugal, Vilamoura, a convite da Microsoft.

Janeiro 2004 – “Reflection, Code Generation and Instrumentation in the .NET

Platform”, no evento Aspectos da Plataforma .NET realizado no Instituto Superior

de Engenharia de Lisboa, Lisboa, a convite do Departamento de Engenharia

Electrotécnica e Telecomunicações e de Computadores do mesmo Instituto.