A tecnologia da Microsoft veio para ficar. Este blog regista o evoluir do meu conhecimento sobre .NET. No entanto este blog não pretende focar apenas a tecnologia em si mas também todas as aplicações desta.

Segunda-feira, Março 07, 2005

Assemblies

Bem, vamos fazer uma pequena pausa do desenvolvimento da aplicação Intersecções e tentar perceber um pouco mais sobre assemblies. Relembro o que disse na primeira mensagem deste blog e que ainda hoje é verdade: não percebo nada disto. O objectivo deste blog é precisamente tentar combater a minha ignorância. Por isso é natural que diga alguns disparates pelo meio. Os disparates fazem parte do processo de aprendizagem. Mas continuando o tema da mensagem, o objectivo é tentar perceber o que é um assembly e o porquê da existência deste conceito.

Antes de mais, vamos ver o que se passa num ambiente tradicional – C ou C++. O programador desenvolve a aplicação em vários ficheiros, em que os ficheiros ou têm definições de funções/tipos (.c ou .cpp) ou declarações de funções/tipos (.h). Ao compilar o conjunto de ficheiros da aplicação, cada ficheiro com definições, juntamente com todos os ficheiros de declarações que ele inclui, resulta num ficheiro de objecto, tipicamente com a extensão .o ou .obj. Este ficheiro contém a versão compilada, mas ainda incompleta, do conteúdo desse ficheiro. Então e porque é que está incompleta? De notar que cada ficheiro de declarações, juntamente com todos os ficheiros de definições que ele inclui, forma uma unidade de compilação – uma parte da aplicação que é compilada de forma independente do resto da aplicação. No entanto, mesmo que uma parte da aplicação seja compilada de forma independente, necessita de ter conhecimento da localização das funções nas outras unidades de compilação. Para clarificar ideias vamos tentar fazer um exemplo. Num ficheiro vamos escrever o seguinte código:

#include
void ShowHello() {
printf("This is the tipical 'Hello, World'\n");
}

Dado o conteúdo do ficheiro, vou chamá-lo de shhello.c. Agora quero poder utilizar esta função noutras partes do código. Para que isso seja possível, as outras partes só necessitam de saber a assinatura da função. Para isso vamos criar um ficheiro, chamado shhello.h, com o seguinte conteúdo:

void ShowHello();

E não é necessário mais. Porquê? Porque as restantes partes da aplicação só precisam de saber como delegar a execução para esta função, não precisam de saber qual o seu conteúdo. E para isso é preciso saber qual o nome da função, os tipos dos parâmetros que recebe e o tipo do retorno. E aquela linha diz isso tudo. Para utilizar a nossa função, vamos criar ainda um terceiro ficheiro (useshhello.c).

#include "shhello.h"
int main() {
ShowHello();
}

Temos agora duas unidades de compilação neste projecto: uma é constituída pelo useshhello.c e o shhello.h e a outra é constituída pelo shhello.c, pelo stdio.h e tudo o que este incluir. Para compilar cada uma destas unidades, utilizando o compilador da Microsoft, tem-se:

cl /c shhello.c
cl /c useshhello.c

O parâmetro /c indica que se pretende apenas realizar o processo de compilação. De cada uma das invocações do compilador, obtém-se um ficheiro .obj. Este corresponde ao código de cada uma das unidades de compilação. O código do useshhello.obj sabe como chamar uma função chamada ShowHello porque se foi fornecida informação para isso. No entanto, ainda não sabe onde é que ela se encontra fisicamente. É necessário agora juntar as duas unidades de compilação para formar um executável. De novo nas ferramentas da Microsoft, temos:

link shhello.obj useshhello.obj

Esta ferramenta irá produzir um ficheiro chamado shhello.exe que, para além de ter o conteúdo dos dois obj tem também preenchidas todas as falhas de ligação entre as duas unidades de compilação. Para ter uma melhor ideia do que significa preencher as falhas de ligação, vamos observar o código gerado pelo compilador. Para isso vamos utilizar a ferramenta dumpbin.

C:\dumpbin /disasm useshhello.obj
Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file useshhello.obj
File Type: COFF OBJECT

_main:
00000000: 55 push ebp
00000001: 8B EC mov ebp,esp
00000003: E8 00 00 00 00 call _ShowHello
00000008: 33 C0 xor eax,eax
0000000A: 5D pop ebp
0000000B: C3 ret

Summary
95 .debug$S
2A .drectve C .text

O que interessa notar é a linha “00000003: E8 00 00 00 00 call _ShowHello”, que representa a chamada da função ShowHello. O código assembly gerado é E8 00 00 00 00, em que E8 é o código para o CALL e 00 00 00 00 é o endereço da função. Calma lá, endereço da função todo a zeros? Isso não é estranho? Realmente é. Como disse em cima, esta unidade de compilação sabe que existe uma função chamada ShowHello, mas ainda não sabe onde é que esta se encontra. Então deixa o espaço para o endereço todo a zeros e delega essa tarefa para a ferramenta seguinte (o linker) que saberá preencher esse espaço. Vamos ver o código do executável:

Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file shhello.exe
File Type: EXECUTABLE IMAGE

_main:
00401040: 55 push ebp
00401041: 8B EC mov ebp,esp
00401043: E8 BD FF FF FF call @ILT+0(_ShowHello)
00401048: 33 C0 xor eax,eax
0040104A: 5D pop ebp
0040104B: C3 ret
0040104C: CC CC CC ÌÌÌ…

De notar que, para que se percebesse alguma coisa, optei por compilar com informação de debug. O linker acrescentou assim informação adicional. No entanto, dá para perceber que o endereço na chamada da função ShowHello já é diferente de zero.

Bem, mas a conversa sobre o ambiente tradicional já se estendeu demasiado. Interessa agora ver como se passa no mundo .NET, o que provavelmente terá que ficar para uma próxima mensagem para evitar que esta fique demasiado extensa. Mas ainda assim vamos começar o tema. Numa aplicação .NET, o programador divide também o projecto em diversos ficheiros, cada ficheiro podendo conter diversos tipos. Estes tipos, após compilação estão incluídos num módulo. Cada módulo, além do código de cada método, contém também informação que descreve o conteúdo do módulo, tal como os tipos que o módulo contém ou os métodos, campos, etc dos tipos. Cada módulo não necessita de corresponder necessariamente a um ficheiro de código. Vamos criar dois ficheiros, A.cs e B.cs, com o seguinte código:

using System;
class A {
Int32 i;
static void Main() { }
}

using System;
class B {
public String str;
}

E agora vamos colocá-los no mesmo módulo. Essa tarefa realiza-se com a seguinte instrução:

csc /t:module A.cs B.cs

Temos agora um novo ficheiro, A.netmodule, que contém os tipos A e B e a informação sobre eles. O nome A.netmodule é o nome por omissão, já que não foi indicado nenhum. Este corresponde ao nome do primeiro ficheiro passado como parâmetro, A.cs, com a extensão netmodule. Se trocarmos a ordem dos ficheiros, temos como saída um ficheiro chamado B.netmodule. Como este nome não é muito elucidativo relativamente ao conteúdo do ficheiro, vamos especificar o novo nome:

csc /t:module /out:AB.netmodule A.cs B.cs

Temos agora o ficheiro AB.netmodule que traduz melhor o conteúdo do ficheiro. E o que são assemblies .NET? Infelizmente a resposta a esta questão terá que ficar para uma próxima mensagem.