Sítio do Piropo

B. Piropo

< Coluna em Fórum PCs >
Volte
10/04/2006

< Computadores XXX: Compiladores >
<
e linkeditores
>


Na coluna anterior desta série, “Montando o programa executável”, vimos como um programa executável pode ser “montado” a partir de seu código fonte em assembly, uma operação que reduzida à sua expressão mais simples nada mais seria que a mera substituição de cada mnemônico do assembly pelo código binário equivalente à instrução em linguagem de máquina e dos rótulos pelos endereços correspondentes em binário, tarefa esta executada por um programa denominado “ Assembler”, ou “montador”. Mas mencionamos também que esta era uma interpretação simplificada e que na vida real a coisa haveria de ser mais complicada. Vejamos hoje em que consistem estas complicações.

Em princípio seria possível criar um programa montador que fosse simples como o acima descrito. Na verdade é provável que os primeiros montadores fossem mais ou menos assim. Mas na medida que os microprocessadores e seus conjuntos de instruções foram evoluindo, os programas montadores também evoluíram. Afinal, já que teríamos obrigatoriamente que submeter nosso código fonte a um programa montador, por que não facilitar a vida do programador dando a este programa maiores habilidades que efetuar uma mera substituição de mnemônicos e parâmetros por instruções e endereços?

E assim foi feito. Um moderno programa assembler, entre outras coisas, é capaz de interpretar operadores, obedecer a diretivas e expandir macros. Vamos ver o que é isto.

Operadores

Operadores são aquilo que o nome mesmo indica: sinais que representam operações, principalmente operações matemáticas. Quando o programa assembler encontra um operador em um código fonte, executa a operação e prossegue sua faina usando como operando o resultado da operação.

Um exemplo: na listagem do programa em assembly que temos usado como exemplo há as seguintes linhas:

Listagem 1: trecho de programa em assembly

A primeira linha, de endereço (ou rótulo) UMZR, representa uma instrução que solicita que o valor contido na posição de memória de endereço NRUM seja copiado no acumulador. A linha seguinte comanda um salto incondicional para o endereço EXIB, onde há uma instrução que envia o valor depositado no acumulador para a saída padrão (vídeo). Ou seja: em conjunto o efeito destas instruções é exibir no vídeo o valor contido na posição de memória de endereço (ou rótulo) NRUM. Pois bem, se o programa assembler usado para montar o executável souber interpretar operadores, a pequena listagem acima poderia ser substituída por (repare na segunda linha):

Listagem 2: trecho de programa em assembly

e o resultado seria o mesmo. Não entendeu o porquê? Então pense. Ao efetuar a montagem da listagem 1 o assembler substituiria o rótulo EXIB da segunda linha pelo endereço da linha por ele identificada e ao ser executado o programa saltaria para lá. Já ao efetuar a montagem da listagem 2, ao chegar à mesma linha ele efetuaria a operação (UMZR +3), ou seja, somaria três unidades ao número correspondente ao endereço representado pelo rótulo UMZR e efetuaria o salto para este local. Mas como o endereço identificado pelo rótulo EXIB fica exatamente três posições de memória adiante do endereço identificado pelo rótulo UMZR, o salto seria dado para o mesmo local, já que como cada linha em assembly corresponde a uma posição de memória, os valores EXIB e (UMZR + 3) são idênticos.

O exemplo é simples, mas suficiente para explicar como funcionam os operadores. Mas não se iluda pela simplicidade do exemplo: convenientemente usados, os operadores podem ser de imensa utilidade para um bom programador assembly.

Diretivas

Diretivas são palavras chave do programa Assembler que, quando incluídas em um código fonte, controlam a forma pela qual o programa se comporta para gerar código. Elas em si mesmas não geram qualquer código executável, apenas informam ao programa o que fazer (por exemplo, o tipo de código gerado, os passos intermediários que devem ser executados durante a montagem e coisas que tais).

A UCP concebida para auxiliar esta série de colunas é tão simples e seu conjunto de instruções tão limitado que não consigo conceber um meio de usá-la como exemplo para o uso de diretivas. Vamos então nos reportar a um caso real, o processador 8086 da Intel usado como UCP do bom e velho XP. Devido a sua forma confusa de endereçar memória, suportava diferentes tipos de programas dependendo do “modelo” de gerenciamento de memória usado por cada um, o que justifica a existência de programas com extensão .Com ou . Exe para o DOS, sistema operacional originalmente desenvolvido para esta UCP.

Pois bem: para que o programa Assembler “soubesse” que modelo de memória deveria ser usado, bastava que o programador incluísse no início do código fonte a diretiva “.MODEL” (nomes das diretivas aceitas pelos montadores do 8086 deviam começar com um ponto e serem grafados em maiúsculas). Por exemplo, a linha:

.MODEL tiny

gerava um programa executável que usava um modelo de memória em que tanto o código quanto os dados caberiam em um único segmento de 64 KB de memória (programa .Com). Já a diretiva:

.MODEL large

gerava programas executáveis em que tanto o código executável quanto os dados poderiam exceder 64 KB de memória (tipicamente programas . Exe).

A diretiva ocupa uma linha no código fonte, mas esta linha não corresponde a uma posição de memória. Quando o programa montador a encontra, simplesmente ajusta suas próprias variáveis internas de acordo com o que manda a diretiva.

Macros

Em linguagem assembly, um macro (há quem chame de “uma macro”, o que também está correto, já que o Dicionário Houaiss o registra como “substantivo de dois gêneros” na rubrica informática, mas francamente, no masculino soa bem melhor...) é um bloco de texto que contém um trecho de código fonte ao qual se atribui um nome.

No início do programa fonte o macro é “declarado”, ou seja, é informado seu nome, início do código, o próprio código e o final do macro. Quando aquele código fonte é montado (submetido ao programa montador), ao encontrar a declaração do macro o montador simplesmente anota o código e aguarda que o macro seja mencionado no restante do código. Cada vez que encontra uma menção ao macro, simplesmente substitui a linha que “invoca” o macro pelo conjunto de linhas de código fonte contido no macro.

A finalidade do macro é facilitar a vida do programador evitando que ele tenha que digitar diversas vezes o mesmo trecho de código quando este trecho é repetido ao longo do programa fonte.

Macros, portanto, geram código executável. Mas o conceito básico não difere muito daquele que rege o funcionamento do programa montador ( assembler). A diferença é que um mnemônico é substituído diretamente por uma instrução em linguagem de máquina enquanto um macro é substituído por um trecho de código que pode conter diversos mnemônicos e parâmetros que, por sua vez, são posteriormente montados.

 

Como se vê, estas novas capacidades introduzidas nos programas montadores fizeram com que a montagem, ou seja, a transformação do código fonte em código executável, exigisse um pouco mais que a mera substituição. Mas não é só isso: bons programas montadores devem ser capazes de receber módulos independentes de programas fonte e montar seu código que, algumas vezes, pode ser apenas um trecho de um programa executável.

Esta última característica é útil em pelo menos duas circunstâncias. A primeira, mais evidente, é um programa complexo desenvolvido por uma equipe de programadores da qual cada membro tomou para si a tarefa de gerar parte do código contendo algumas rotinas e funções. Cada um deles produzirá, então, um módulo de programa fonte. Este módulo não pode ser montado sob a forma de um programa executável pois não constitui um programa inteiro, apenas parte dele. Mas se não for possível montá-lo, ao menos parcialmente, não será igualmente possível verificar a consistência e a presença de eventuais erros de sintaxe e grafia.

O que se faz então é desenvolver o código fonte de cada módulo independente e submetê-lo ao montador ( assembler). Se, durante a montagem, o assembler detectar algum erro, interrompe a montagem e emite uma mensagem que permitirá ao programador do módulo efetuar a correção. Se não houver erro, o resultado da montagem será um arquivo híbrido entre um módulo em linguagem de máquina e um programa fonte em assembly, ou seja, um trecho de código com as instruções e a maioria dos parâmetros em binário, como o código executável final, mas com indicações das referências feitas aos demais módulos, que não podem ser “resolvidas” (ou seja, substituídas pelos endereços das “entradas” das rotinas correspondentes) porque os demais módulos não foram montados. Arquivos deste tipo contêm aquilo que se conhece por “código objeto” e geralmente usam a extensão .Obj. Cada módulo montado gera um arquivo .Obj.

Para se obter o programa executável é preciso ligar (em inglês, “to link”) os diversos arquivos .Obj e logo veremos como isso é feito.

Mas a possibilidade de modular um programa todo em assembly não justificaria a criação de um passo intermediário para gerar o código objeto. Há uma razão mais forte. E esta razão é a possibilidade de se criar um programa executável ligando módulos desenvolvidos em diferentes linguagens de programação.

Entrar em detalhes sobre linguagens de programação ditas “de alto nível”, como C, Pascal, C++ e C# estaria completamente fora do escopo desta série (não esqueça que o que nos levou a discutir a linguagem assembly foi tentar entender como uma UCP realmente funciona, e já nos desviamos bastante do caminho original). Portanto, vamos mencionar o único ponto que nos interessa.

Semelhantemente ao assembly, um programa fonte desenvolvido em uma destas linguagens nada mais é que um arquivo texto. A diferença é que este arquivo texto contém comandos (e seus parâmetros) desta linguagem de alto nível. Mas, ao contrário do assembly onde cada mnemônico corresponde a uma e somente uma instrução em linguagem de máquina, um comando de uma linguagem de alto nível pode ser desdobrado em muitas (dependendo do comando, muitas MESMO) instruções em linguagem de máquina. Então, para gerar o programa executável correspondente, antes é preciso transformar o código fonte da linguagem de alto nível em código fonte da linguagem assembly que por sua vez será transformado em linguagem de máquina. Este passo intermediário, ou seja, a transformação do código fonte de qualquer linguagem de algo nível em código fonte assembly, denomina-se “compilação”. E é executado por um programa denominado “ compiler”, ou “compilador”.

Então a conversão do código fonte de uma linguagem de alto nível em código executável é feita (pelo menos – e já veremos o porquê deste “pelo menos”) em dois passos: compilação e montagem. A compilação toma como entrada o arquivo texto que contém o código fonte em linguagem de alto nível e gera uma saída que consiste em um arquivo que contém código fonte em assembly. A montagem toma este arquivo como entrada e “monta” o programa executável. Pois bem: o arquivo de saída do primeiro passo, a compilação, também é gerado em “código objeto”, um misto de binário e assembly, onde as referências das chamadas das funções não foram “resolvidas”, ou seja, seus endereços não podem ser incluídos no código porque ainda não foi efetuada a montagem.

Se isso lhe parece complicado, esqueça. Tenha em mente apenas o seguinte: pode-se gerar código objeto (arquivos com extensão .Obj) seja submetendo ao assembler um código fonte em assembly, seja submetendo ao compilador um código fonte em linguagem de alto nível (é claro que cada linguagem de alto nível exigirá seu próprio compilador).

Resultado: isso me permite desenvolver programas em que alguns módulos (em geral aqueles em que a rapidez de execução é crítica) são desenvolvidos em assembly e outros (os mais trabalhosos, porém menos críticos) em linguagem de alto nível. Depois, gero os códigos objeto de cada um deles e, finalmente, os combino (ou “ligo”) em um único programa executável.

O programa que efetua a ligação dos módulos em código objeto e gera um único programa executável (um arquivo de extensão . Exe que quando invocado pelo sistema operacional resulta na execução do programa) chama-se “ Linker” (ou, em português mal traduzido, “ linkeditor”).

O linkeditor recebe um ou mais arquivos de código objeto (de extensão .Obj) gerados por montadores ou compiladores e cria um único arquivo executável, que nada mais é que o programa em linguagem de máquina.

Neste ponto cabe uma observação: tudo isto é, naturalmente, uma explicação bastante simplificada porém razoavelmente compatível com a realidade; se você desejar informações mais detalhadas sobre o processo de geração do arquivo executável a partir dos programas fonte em linguagem assembly ou de alto nível recomendo uma consulta à Wikipedia com o termo < http://en.wikipedia.org/wiki/Compiler > " compiler” e as expressões < http://en.wikipedia.org/wiki/Assembly_language > " assembly language” e < http://en.wikipedia.org/wiki/Object_code > " object code” ou “ object file” (um clique sobre qualquer um destes atalhos o levará ao item correspondente).

 

 

B. Piropo