sexta-feira, 24 de fevereiro de 2012

C: Como compilar

Como compilar um programa em C? É uma dúvida simples, mas comum entre os iniciantes. A resposta varia conforme o sistema e o desenvolvedor do compilador, mas tem um básico que é comum nos Unix, e nos outros sistemas cujo compilador seja por linha de comando. E sobre este modo que aqui tratarei.

O mais simples

A forma mais simples é:

cc prog.c

Que no caso  compila o programa prog.c, sem qualquer otimização, deixando a tabela de símbolos (que contém a descrição das variáveis, funções etc), e faz a saída no arquivo a.out.

O comando strip remove esta tabela de símbolos:

strip a.out

Esta tabela de símbolos pode ser usado na depuração do programa, e alguns compiladores tem opções para gerar tabelas bem mais detalhadas para usar na depuração.

Mas que tal já gerar o arquivo sem esta tabela de símbolos?

cc -s prog.c

A opção -s já diz que o arquivo final não terá a tabela de símbolos.

Mas posso escolher o nome do arquivo de saída? Claro que sim, com a opção -o (note bem, letra o minúsculo), seguida pelo nome desejado:

cc -s -o prog prog.c

Neste exemplo, ao invés de gravar o programa executável no arquivo a.out, este será escrito no arquivo prog.

Mas o código não está bom, pois não está otimizado. Para isto usamos a opção -O (note, aqui é maiúsculo):

cc -s -O -o prog prog.c

A maioria dos compiladores tem níveis de otimização, que são escolhidos colocando um número depois da opção -O. Normalmente , quanto maior o número, maior é o nível de otimização.

cc -s -O3 -o prog prog.c

Neste ponto você já consegue compilar o seu programa, mas ainda não viu nada sobre passos de compilação. Para entender melhor como funciona a compilação, continue lendo.

Passos de compilação

A compilação do programa não acontece de uma vez só. Ela é feita em etapas, chamadas de passos. O programa cc basicamente organiza os passos, segundo as opções, e chama vários programas, cada um responsável por um passo, passando os parâmetros adequados.

Abaixo estão, na ordem, os programas que são chamados durante a compilação de um programa. Nota, segundo a implementação, esta ordem pode variar em forma, em quantidade, podem ter fusões de etapas etc.

cpp: C Preprocessor

Ele é o processador de macros responsável por tratar todas as compilações condicionais (#ifdef, #if, #ifndef, #else e #endif), as macros (#define), inclusões (#include) e qualquer outra coisa relacionada com linhas que comecem com #. É ele que faz as substituições das macros.

cc1: Primeiro passo de compilação

Este programa pega o programa fonte modificado pelo preprocessador e compila, gerando um código em Assembler, com a extensão .s.

cc2: Otimização

Se não foi pedida nenhuma otimização, este passo não é executado, mas se foi pedida, este programa é executado, pegando a saída do programa anterior e otimizando o código, A saída é um outro arquivo .s.

as: Montador Assembler

O Montador Assembler pega o arquivo .s, que é o programa fonte convertido para Assembler, e faz a tradução para um Arquivo Objeto Realocável, com extensão .o. Este arquivo já é binário, em código de máquina, e tem a tabela de símbolos mencionada anteriormente. Mas a tabela de símbolo aqui é fundamental, pois descreve o que ainda vai ser necessário pegar da biblioteca para terminar a compilação.

Se ao chamar o cc for passada a opção -c, a compilação irá parar neste ponto, não executando a etapa seguinte. Isto será usado em um artigo futuro.

Já usei este programa quando brincava com Assembler do PDP 11/70, e a saída já era executável quando não dependia de nada de nenhuma biblioteca.

ld: Link edição

Última etapa. Tecnicamente o programa está compilado a um bom tempo, mas não está pronto para ser executado.

Este programa pega todos os Arquivos Objetos Realocáveis (.o), sejam eles criados por você ou vindos das bibliotecas, que compõem o programa e os junta, ligando as referências entre si, resolvendo os endereços das funções, variáveis extern etc.

Por exemplo. Se o seu programa usa a função printf(), então no Arquivo Objeto Realocável do seu programa, que saiu do as, tem a informação que precisa que o endereço da printf() seja colocado nos pontos onde ela é chamada. O ld procura nas bibliotecas pelo Arquivo Objeto Realocável que contenha a printf() e copia para o Programa Executável que está gerando, resolvendo os endereçamentos.

A opção -s do cc é tratada nesta etapa. O cc repassa a opção para o ld, informando que a tabela de símbolos não deve ser gravada no arquivo final.

Compilando programa com vários arquivos fontes

Programas grandes normalmente são divididos em vários arquivos fontes, para facilitar o trabalho de programação. Cada um pode ser feito por um programador, cada um pode tratar de uma etapa de processamento etc. Como estes programas são compilados? Normalmente usando o programa make para ajudar, que será assunto de um outro artigo, mas de qualquer forma a ideia é derivada do que vou apresentar aqui.

cc -s -O3 -o prog principal.c parte1.c parte2.c parte3.c parte4.c

No exemplo acima, o programa prog é composto por um arquivo principal e mais outros 4 arquivos. Neste caso, todas as 5 partes são compiladas independentemente até a etapa do as. Então os cinco arquivos .o, os cinco Arquivos Objetos Realocáveis, são passados ao ld, que fará a junção deles, mais o que eles precisarem das bibliotecas, em um Programa Executável, um Arquivo Objeto Executável.

Conclusão

Aqui tratei como a se usa o compilador, como são os seus passos, e como compilar um programa com vários arquivos fontes. Faltou falar do make, que torna a compilação de programas muito grandes, com muitos fontes, prática e rápida, mas estabeleci as bases que serão necessárias quando falar do make.

Sugiro a leitura, mesmo que não detalhada e sem entender muitos dos detalhes, do manual do seu compilador C. Ele pode esclarecer muitas dúvidas.

Nenhum comentário:

Postar um comentário