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
Departamento de Engenharia Informática
Faculdade de Ciências e Tecnologia
Universidade de Coimbra
ISBN 972-95988-2-7
Coimbra – Portugal, Junho 2005
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.
⎯ para a avó Lurdes ⎯
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
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.
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.
Índice
RESUMO........................................................................................................................................ IX
AGRADECIMENTOS ................................................................................................................. XI
ÍNDICE........................................................................................................................................ XIII
1. INTRODUÇÃO........................................................................................................................... 1
1.1.
1.2.
1.3.
1.4.
Motivação.................................................................................................................... 2
Objectivos de Investigação...................................................................................... 4
Contribuição............................................................................................................... 6
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
XIV ÍNDICE
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
Capítulo
1
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.
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
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
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:
OBJECTIVOS DE INVESTIGAÇÃO
5
Figura 1 – Exemplo da estrutura de um Assembly
•
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].
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.
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.
Capítulo
2
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.
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).
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
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
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
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.
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. À
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
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
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
disponibilizado no site web da ASM:
abordagem é apresentado
o seguinte
exemplo,
BIBLIOTECAS PARA A PLATAFORMA JAVA
19
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
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,
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
BIBLIOTECAS PARA A PLATAFORMA JAVA
21
Figura 2 – Panorâmica do sistema BCA1
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]
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.
BIBLIOTECAS PARA A PLATAFORMA JAVA
23
Figura 3 – Resumo dos mecanismos de intercepção do carregamento de
classes na JVM1
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]
24
CAPÍTULO 2 — INSTRUMENTAÇÃO DE CÓDIGO
Figura 4 – Classes de sistema em JAVA
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 é
BIBLIOTECAS PARA A PLATAFORMA JAVA
25
Figura 5 – Classes de sistema em JAVA após a transformação TCH
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.
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
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.
Capítulo
3
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.
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-andplay, 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
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
32
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 6 – Formato de um ficheiro PE
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.
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.
34
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 7 – Tabelas da metadata
•
“# ~” – 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.
EXECUÇÃO EM .NET
35
Figura 8 – Carregamento de aplicações no CLR
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
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.
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).
38
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 9 – Metodologia de utilização da RAIL
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
ARQUITECTURA
39
Figura 10 – Arquitectura RAIL
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.
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.
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.
42
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 11 – Organização da primeira camada da RAIL
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.
ARQUITECTURA
43
Figura 12 – Organização da segunda camada da RAIL
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.
44
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 13 – Diagrama UML parcial da estrutura de classes implementada na RAIL para representar
um assembly e os seus membros
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.
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,
CustomAttributeOwner.
por
conseguinte,
descendem
da
classe
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.
ARQUITECTURA
47
Figura 14 – Diagrama UML representando as classes REvent, REventDef e REventRef
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
48
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 15 – Diagrama UML representando as classes RAssemblyName, RAssemblyNameDef,
RAssemblyNameRef e RVersion
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.
ARQUITECTURA
49
Figura 16 – Aspecto da classe RPInvokeInfo
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.
50
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 17 – Suporte para assinaturas compactadas
ARQUITECTURA
51
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
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.
52
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 18 – Classes para representa os tipos dentro das assinaturas
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),
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.
54
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 19 – Classes utilizadas na representação das instruções IL
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.
56
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 20 – Classes ILFactory (parcial), MethodBody e Code
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
ARQUITECTURA
57
Figura 21 – Classes envolvidas na representação dos mecanismos de tratamento de
excepções
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.Reflection.Emit.TypeBuilder).
System.Type
O
inverso
deste
comportamento
ou
é
assegurado pela classe MetadataResolver que a partir de informação disponível na
58
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
Figura 22 – Classe MetadataResolver
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:
ARQUITECTURA
59
Figura 23 – Classe TypeResolver
•
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.
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.
INSTRUMENTAÇÃO DE ALTO NÍVEL
61
Figura 24 –O padrão de software Visitor
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)
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.
INSTRUMENTAÇÃO DE ALTO NÍVEL
63
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
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
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.
INSTRUMENTAÇÃO DE ALTO NÍVEL
65
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
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
66
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
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
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.
67
INSTRUMENTAÇÃO DE ALTO NÍVEL
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
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
68
CAPÍTULO 3 —INSTRUMENTAÇÃO DE CÓDIGO EM .NET
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
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
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.
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
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.
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.
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#.
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.
Capítulo
4
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.
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.
ALTERNATIVA AOS PROXIES DINÂMICOS
77
Figura 25 – Proxies dinâmicos em .NET
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
78
CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
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
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.
ALTERNATIVA AOS PROXIES DINÂMICOS
79
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.
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.
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.
ALTERNATIVA AOS PROXIES DINÂMICOS
81
<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
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
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
ALTERNATIVA AOS PROXIES DINÂMICOS
83
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.
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.
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.
00:00:00.2103024 00:01:32.7633872 00:00:00.7010080
00:00:00.2303312 00:01:41.8464480 00:00:00.8011520
00:00:00.2904176 00:02:29.1544736 00:00:00.7711088
00:00:00.1802592 00:01:31.3213136 00:00:00.7210368
00:00:00.1902736 00:01:47.1841232 00:00:00.6709648
00:00:01.2718288 00:10:18.7296896 00:00:04.4463936
00:00:00.1101584
00:00:00.1301872
00:00:00.1702448
00:00:00.0901296
00:00:00.1001440
00:00:00.6909936
ping(int)
add(int,int)
add(ref int,ref int,ref int)
ping(Object)
ping(ref Object)
Tempo de Execução Total
Versão # 3
00:00:00.1702448 00:01:16.4399152 00:00:00.6809792
Versão # 2
00:00:00.0901296
Versão # 1
ping()
Original (sem proxies)
ALTERNATIVA AOS PROXIES DINÂMICOS
85
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].
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 trycatch 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,
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.
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.
90
CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
<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
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.
Numero de
blocos catch
Diferentes
handlers
AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES
System.Exception
50
12
System.Net.WebException
1
1
System.Threading.ThreadAbortException
2
1
53
14
Tipos de excepções
Total
91
Tabela 4.2 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos
handlers na Fotovision
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.
Diferentes
handlers
CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
Numero
de blocos
catch
92
System.Exception
46
17
System.FormatException
5
1
System.IO.FileNotFoundException
2
2
System.ArgumentException
2
2
System.Runtime.InteropServices.COMExce
ption
2
2
System.IO.PathTooLongException
1
1
Microsoft.Tools.FxCop.Common.ProjectVe
rsionMismatchException
1
1
System.NotSupportedException
1
1
System.Object
1
1
System.OverflowException
1
1
System.Xml.XmlException
1
1
63
30
Tipos de excepções
Total
Tabela 4.3 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos
handlers na FxCop
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
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.
Diferentes
handlers
CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
Numero
de blocos
catch
94
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.SerializationExc
eption
1
1
System.Security.SecurityException
1
1
System.Threading.ThreadInterruptedException
1
1
System.Web.HttpException
1
1
265
124
Tipos de excepções
Total
Tabela 4.4 – Tipos de excepções, numero de blocos catch e diferentes conteúdos do corpo dos
handlers em System.Web.dll
AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES
95
Figura 27 – Distribuição do tipo de handlers caracterizados pelo número de instruções nas três
aplicações
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.
96
CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
<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
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
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
…
…
…
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
…
…
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
…
…
…
…
Fotovision
System.NullReferenceException
FxCop
%
Tipo de Excepção
97
System.Web.dll
Número de
instruções
afectadas
AVALIAÇÃO DOS MECANISMOS DE TRATAMENTO DE EXCEPÇÕES
Tabela 4.5 – Tipos de excepções e número de instruções passíveis de as gerar
foi encontrada sem estar a ser tratada e o seu peso no total dos tipos de excepções
encontradas nesta situação.
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
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
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
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”.
102 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
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.
No exemplo da listagem anterior apenas se definiu um tipo de handler para o bloco trycatch, 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
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.
104 CAPÍTULO 4 — DOMINIOS DE APLICAÇÃO
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.
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).
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 email relatórios de erros, sugestões de novas funcionalidades e declarações de utilizadores a
demonstrarem o seu interesse na biblioteca.
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.
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.
Capítulo
5
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.
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
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.
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-1110proxy.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.
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.
Transformation
Chase,
with
e
D.
JOIE,"
Kaminsky,
em
"Automatic
USENIX
Annual
Program
Technical
Symposium, New Orleans, Louisiana, USA, 1998.
[Dahm1999]
M. Dahm, "Byte Code Engineering," em JIT '99 - Java-InformationsTage (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 NonIntrusive 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.
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.ecmainternational.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, AddisonWesley, 1995.
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/enus/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.
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 MetaLevel 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, PrenticeHall, 1988.
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/enus/cpref/html/frlrfsystemruntimeremotingproxiesrealproxyclasstopi
c.asp.
[Microsoft2004a]
Microsoft,
Cambridge,
"F#
Programming
UK,
Language,"
2004.
Microsoft
Research,
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/enus/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/enus/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/enus/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
http://msdn.microsoft.com/library/default.asp?url=/library/enus/cpgenref/html/cpconnetframeworkdesignguidelines.asp.
em:
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/enus/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, AddisonWesley 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/enus/Dndotnet/html/mssharsourcecli.asp.
[Stutz2003]
D. Stutz, T. Neward, e G. Shilling, Shared Source CLI Essentials,
U.S.A., O’Reilly, 2003.
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/enus/dnnetcomp/html/FotoVisionDesktop.asp.
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:
http://serp.sourceforge.net.
Overview,"
2002.
Disponível
em:
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 PosGraduçã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.
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.
Download

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