Processo de execução de um código no .NET Framework

Quando estamos desenvolvendo, o único momento em que pensamos na compilação do código é quando algum erro ocorre. Fora isso, essa é só mais uma etapa do processo de execução (que costuma passar despercebida).

Só que um bom programador precisa entender como este processo funciona, para criar códigos melhores, entender mais a linguagem que se está utilizando e compreender melhor os erros gerados.

No geral as linguagens possuem um processo de compilação parecido, mas aqui veremos o processo de compilação de uma aplicação desenvolvida para o .NET Framework.

Nele, separamos o processo de compilação em quatro etapas:

  • Escolhendo um compilador;
  • Compilando o código para a linguagem intermediária;
  • Compilando o código da linguagem intermediário para a nativa;
  • Executando o código.

Essas etapas são ilustradas na imagem abaixo:

Escolhendo um compilador

O .NET Framework possui um importante recurso que permite a interoperabilidade das linguagens suportadas por ele, que é o Common Language Runtime
(CLR).

Seguindo as especificações definidas no padrão ECMA-335 (Obs: Esse link leva a um arquivo PDF) – que define as especificações do CLR – qualquer linguagem pode definir um compilador suportado pelo .NET. Com isso, é possível desenvolver aplicações para o .NET utilizando C#, F#, Perl, Cobol, Ruby etc.

Assim, na compilação, a primeira coisa que o .NET faz é selecionar o compilador definido para a linguagem do código que será compilado.

É função do compilador verificar o código que está sendo analisado. Ele verificará se algum token está sendo utilizado de forma errônea e se todas as regras da linguagem estão sendo obedecidas. Basicamente ele verificará se há algum erro de digitação no código.

Compilando o código para a linguagem intermediária

Agora que o .NET começa a fazer a sua “mágica”. Ao final do processo de compilação, o compilador converte o código-fonte para uma linguagem intermediária, que no .NET, é chamada de Microsoft Intermediate Language (MSIL), ou apenas Intermediate Language (IL).

O IL é um conjunto de instruções independente da CPU, que pode ser convertido de forma mais eficiente para código nativo.

Nele temos instruções para carregar, armazenar, iniciar e chamar métodos em objetos, bem como, instruções para operações lógicas e matemáticas, controle de fluxo, acesso direto a memória, manipulação de erros, entre outros recursos.

Antes deste código ser gerado, ele precisa ser convertido em tempo de execução para um código de máquina que é entendido pela CPU do computador. Normalmente este processo é realizado por um compilador Just-in-Time (JIT). Como o CLR fornece um ou mais compiladores de acordo com a arquitetura do computador, o mesmo conjunto de instruções IL pode ser compilado e executado em qualquer arquitetura suportada pelo .NET

Quando o código intermediário é produzido, também são gerados metadados. Esses metadados descrevem os tipos utilizados no código incluindo as suas definições, as assinaturas dos seus membros, as referências a outros códigos e qualquer coisa que será utilizada em tempo de execução.

O IL e os metadados são contidos em um arquivo executável portátil (PE – portable executable), que é baseado e que estende o Microsoft PE e Common Object File Format (COFF), historicamente utilizado para conteúdo executável. Este formato de arquivo, que possui o IL ou código nativo, bem como metadados, permite que o sistema operacional reconheça características do CLR. A presença dos metadados com o código intermediário permite que o código se descreva, o que significa que não há necessidade de incluir bibliotecas de tipos ou interfaces de definição da linguagem (IDL).

Durante a execução, o sistema localiza e extrai dos metadados as informações que necessitar.

Compilando o código da linguagem intermediária para a nativa

Antes de executar o código IL, é necessário compilá-lo para a linguagem nativa, suportada pela arquitetura da máquina alvo. Para fazer este processo, o .NET fornece duas formas de conversão:

  • Um compilador Just-in-Time (JIT);
  • O Ngen.exe (Native Image Generator).

Compilação com um compilador JIT

A compilação JIT converte o código intermediário em código nativo em tempo de execução. Conforme um código IL for requisitado, o JIT o converte para código nativo.

Como o CLR fornece um compilador JIT para cada arquitetura suportada pelo .NET, um código IL pode ser compilado pelo JIT e executado em diferentes arquiteturas. No entanto, se o código chamar APIs nativas de uma plataforma, ou algum recurso específico, o código IL só poderá ser executado nesta plataforma.

A compilação JIT leva em conta a possibilidade de algum código nunca ser chamado durante a execução. Assim, em vez de usar tempo e memória para converter todo o código, em um arquivo PE nativo, ele converte apenas o que for necessário para a execução, e armazena este código nativo gerado, na memória, para ele ficar acessível para as chamadas subsequentes.

Por exemplo, na primeira vez que um método é invocado durante a execução, o JIT irá convertê-lo para código nativo e armazená-lo na memória. Quando este código for chamado novamente, o JIT irá diretamente na memória obter o código nativo, em vez de tentar convertê-lo novamente.

Compilação com o Ngen.exe

Como o compilador JIT gera o código nativo em tempo de execução, isso pode impactar um pouco na performance da aplicação. Na maioria dos casos, esta perda é aceitável ou irrisória. Além disso, o código gerado pelo compilador JIT fica vinculado ao processo que desencadeou a compilação, assim ele não pode ser compartilhado entre vários processos.

Para permitir que o código gerado possa ser compartilhado entre múltiplas chamadas ou entre vários processos que compartilham um conjunto de códigos nativos, o .NET possui o compilador Ngen.exe, também chamado de compilador Ahead-of-Time (AOT).

Este compilador gera o código nativo de uma forma muito parecida que o JIT, mas eles se diferenciam em três pontos:

  • O código IL é convertido antes do arquivo ser executado, no lugar de convertê-lo durante a execução.
  • Todo o código é convertido de uma única vez.
  • O código convertido é salvo no disco, e não na memória.

Verificação de código

Se não for desabilitado, durante a compilação para o código nativo, tanto com JIT ou com o AOT, o código IL passa por uma verificação que analisa se o código é “type safe”, o que significa que o código só acessa locais da memória que ele está autorizado a acessar.

Isso ajuda a isolar um objeto do outro e ajuda a protegê-lo contra corrupção indevida ou mal-intencionada. Ele também garante que as restrições de segurança podem ser aplicadas de forma confiável.

Para verificar se o código é “type safe”, a verificação se baseia em três afirmações (que precisam ser verdadeiras):

  • Uma referência a um tipo é estritamente compatível com o tipo a ser referenciado;
  • Somente operações apropriadamente definidas são invocadas em um objeto;
  • As identidades são de quem afirmam ser.

Se o requisito “type safe” estiver ativo e o código não passar na verificação acima, é gerando um erro de execução.

Executando o código

O processo final de execução, é a execução do código propriamente dito. Como já dito nos tópicos anteriores, durante esta execução o CLR e os metadados irão fornecer as informações que forem necessárias para a execução da aplicação.

Lendo assim, é possível notar que o processo não é simples, mas não é tão complexo, por isso que é importante o seu entendimento.

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES