CI067, 2018-2                             © Roberto André Hexsel, 2014-2018

Compilação Condicional

Neste laboratório veremos os comandos para fazer uso do compilador e de compilação condicional.
Antes de mais nada, crie um prompt vazio, copiando a linha abaixo no seu terminal alias prompt:="" Três clicks no botão esquerdo para copiar, um click no meio para colar

GNU Compiler Collection

O compilador é o programa que traduz código escrito em uma linguagem de alto nível, tal como C ou Pascal, para um programa que é o equivalente lógico em linguagem assembly do processador que executará o programa. Considere uma aplicação cujo código fonte está separado em dois arquivos, chamados de x.c, que contém a função main(), e y.c que contém a função fun(). A figura abaixo mostra um diagrama com as etapas da compilação dos dois arquivos para produzir o executável a.out. Os círculos indicam os programas que traduzem "o programa", de código C para código executável; junto às setas estão os nomes dos arquivos processados pelos estágios da compilação. compilador Os arquivos fonte contém diretivas do tipo #include e #define que são processadas pelo preprocessador cpp, que faz a expansão das macros, a inclusão dos arquivos com cabeçalhos, e remove os comentários. A saída do cpp é entregue ao compilador gcc, que faz a tradução de C para linguagem de montagem. Ao contrário do cpp, que só manipula texto, o compilador traduz comandos da linguagem C para instruções em assembly. O código em assembly é processado pelo montador as, que produz um arquivo objeto, já com as instruções traduzidas para seus equivalentes em binário, além da tabela de símbolos. Na disciplina de Projetos Didigtias veremos uma descrição do processo de montagem. Os arquivos objeto, e se necessário, o código mantido em bibliotecas, são agregados pelo ligador (ld), que finalmente produz o arquivo executável a.out. O processo de ligação será estudado em Software Básico. A documentação online, completa, do GCC está em http://gcc.gnu.org Suponha que você editou seu programa em um arquivo fonte.c. O comando abaixo produz um executável a.out: prompt: gcc -Wall fonte.c prompt: a.out # seu programa é executado A opção -Wall força o compilador a emitir todos os avisos sobre possíveis problemas com o código que está sendo compilado. Esta opção é excepcionalmente útil para a resolução de problemas durante um ciclo de edição--compilação. Se desejamos salvar o executável em um arquivo chamado prog, a opção -o nos permite fazê-lo. Esta opção é normalmente a última das opções. Sem ela a saída é sempre gravada em a.out. prompt: gcc -Wall fonte.c -o prog prompt: prog # seu programa é executado Se desejamos traduzir o programa em C para um arquivo objeto, e salvar o resultado em prog.o, a opção -c deve ser usada. O sufixo .o denota arquivo com código objeto, que é um arquivo binário mas não é um executável porque, possivelmente, deve ser ligado a outros arquivos objeto e a uma ou mais bibliotecas. Se a opção -o não for usada, o objeto será gravado em fonte.o. prompt: gcc -Wall -c fonte.c -o prog.o Se desejamos ligar os arquivos objeto prog.o, grog.o e clog.o para produzir o executável prog, o comando abaixo invoca o ligador para produzir o executável. Note que a opção -Wall não é necessária neste caso porque os arquivos objeto foram compilados anteriormente. prompt: gcc prog.o grog.o clog.o -o prog A ordem dos objetos é importante! prog.o pode usar funções definidas em grog.o, e clog.o pode usar funções definidas em grog.o. Se, ao percorrer os arquivos objeto, o uso das funções não for encontrado antes de suas definições, a compilação resultará em erro. Se nosso programa emprega funções da biblioteca de matemática libm.a, então esta biblioteca deve ser indicada na linha de comando. prompt: gcc prog.o -lm -o prog Note a forma abreviada para nominar a biblioteca: -lm indica que a biblioteca  libm.a  deve ser usada, e esta pode ser encontrada num dos diretórios buscados normalmente pelo compilador (/lib e /usr/lib). O caminho completo para a biblioteca também pode ser empregado. Na minha instalação o caminho completo para libm.a é apontado pelo comando locate. prompt: locate libm.a /usr/lib/i386-linux-gnu/libm.a prompt: gcc prog.o /usr/lib/i386-linux-gnu/libm.a -o prog O caminho completo é útil se você necessita ligar seu programa a uma biblioteca que não é uma das bibliotecas oficiais. Os arquivos com cabeçalhos de funções das "bibliotecas oficiais" são buscados num conjunto de ``diretórios oficiais'' (/usr/include e /usr/local/include). Suponha que você trabalha num projeto grande e que os arquivos de cabeçalho sejam armazenados todos num só diretório. O caminho para estes diretórios pode ser indicado ao compilador com a opção -I. O primeiro caminho a ser procurado é aquele apontado pelo -I, e então os caminhos dos diretórios oficiais são percorridos. Mais de uma opção -I pode ser usada. prompt: gcc -Wall -c prog.c -I /path/to/my/headers A opção -L pode ser usada para indicar um diretório com "bibliotecas não oficiais", de forma similar à da opção -I. prompt: gcc -Wall -c prog.c -L /path/to/my/libs Suponha que você instalou bibliotecas opcionais no seu sistema em /opt. O comando abaixo informa ao compilador os locais dos cabeçalhos e da biblioteca libprecious.a. prompt: gcc -Wall -c ring.c -I/opt/include -L/opt/lib -lprecious -o ring A opção -v (verbosa) pode ser usada para mostrar na tela os passos intermediários da compilação. Além de informativa, esta opção ajuda a detectar problemas com a instalação do compilador ou bibliotecas. A opção -D (define) pode ser usada para definir uma macro com o valor 1, ou para atribuir um valor a uma macro. -D é processada antes da inclusão de arquivos com #include. prompt: gcc -Wall -c prog.c -DcMIPS # equivale a #define cMIPS 1 prompt: gcc -Wall -c prog.c -DcMIPS=YES # equivale a #define cMIPS YES A definição de uma macro pode ser cancelada com a opção -U (undefine). prompt: gcc -Wall -c prog.c -UcMIPS # equivale a #undef cMIPS Em Resumo gcc --version # imprime versão do compilador, abreviada gcc -v # imprime versão do compilador, verbosa gcc -Wall x.c # gera a.out gcc -Wall -v x.c # gera a.out, saída verbosa no terminal gcc -Wall x.c -o x # gera executável x gcc -Wall -S x.c -o x.s # traduz para assembly, sufixo .s gcc -Wall -c x.c -o x.o # gera objeto, sufixo .o gcc -o x x.o y.o z.o # gera x, a partir de 3 objetos: x, y e z gcc -o x x.o -lm # compila e liga com biblioteca libm gcc -o x x.o -I caminho/para/includes gcc -o x x.o -L caminho/para/bibliotecas gcc -Wall -c prog.c -DcMIPS # equivale a #define cMIPS 1 gcc -Wall -c prog.c -DcMIPS=YES # equivale a #define cMIPS YES gcc -Wall -c prog.c -UcMIPS # equivale a #undef cMIPS gcc -Wall -o x -O0 -g x.c # sem otimização, para depuração gcc -Wall -o x -O1 x.c # com alguma otimização gcc -Wall -o x -O2 x.c # com mais otimização gcc -Wall -o x -O3 x.c # com muita otimização gcc -Wall -o x -Os x.c # com otimização para menor tamanho (small)
Cheshire the Cat

Compilação Condicional

O programa CPP (C PreProcessor) processa o código fonte C e efetua uma série de substituições, antes de entregá-lo ao compilador. CPP é também conhecido com um "processador de macros" porque efetua a substituição de macros em sua entrada. Macros são trechos de texto que são trocados pela sua expansão. A documentação online completa do CPP está em http://gcc.gnu.org/onlinedocs/cpp

1- Substituição de Macros, ou #define

Quando o CPP encontra #define X Y no código fonte, cada instância de X é substituída por uma instância de Y. Neste caso, X é o "nome" da macro e Y o "corpo" ou a "expansão" da macro. Por exemplo, se estas duas definições forem encontradas no código, cada instância de X será substituída por 1000. #define X Y #define Y 1000 Não coloque um ';' após a expansão da macro; o ';' pode violar a sintaxe de um comando, dependendo de onde ela for usada, como p.ex. "no meio" de um comando. Macros podem conter "parâmetros" e estes também sofrem substituição textual pura e simples. É sempre uma boa ideia proteger os parâmetros com parenteses na expansão da macro para garantir que a substituição textual não causa nenhum efeito colateral. Os parenteses após o nome da macro são necessários. A definição de #define MAX(p,q) ( (p) > (q) ? (p) : (q) ) provocaria as substituições indicadas abaixo: x = MAX(a,b); ==> x = ( (a) > (b) ? (a) : (b) ); y = MAX(5,7); ==> y = ( (5) > (7) ? (5) : (7) ); z = MAX(*r,s+3); ==> z = ( (*r) > (s+3) ? (*r) : (s+3) ); O terceiro exemplo mostra um uso particularmente capcioso das macros. Remova os parenteses e a confusão ficará clara. Uma vez que uma macro foi expandida, o texto resultante é examinado novamente e macros que ainda não tenham sido expandidas são então expandidas. Este comportamento é necessário para imitar aquele de funções aninhadas. O CPP deve ser invocado com dois arquivos, um de entrada e um de saída. Um dos dois pode ser substituído por "-" (hífen), o que indica que aquele arquivo é a entrada padrão. Se o segundo nome é omitido, o resultado é mostrado na saída padrão. Copie o trecho abaixo para um arquivo chamado teste.c e verifique se as macros são substituídas corretamente. --teste.c-- #define X Y #define Y 1000 a = X; #define MAX(p,q) ( (p) > (q) ? (p) : (q) ) x = MAX(a,b); y = MAX(5,X); z = MAX(*r,s+3); --fim-- Para verificar o resultado diga: cpp teste.c O CPP troca a definição da macro por uma linha em branco. Exercícios: 1) Escreva uma macro para testar igualidade entre inteiros, e uma para descobrir o mínimo dentre dois inteiros; 2) Escreva macros para encontrar o máximo e o mínimo dentre três inteiros; 3) Escreva uma macro que compute a Fórmula de Báscara. Os resultados devem ser devolvidos em parâmetros passados para a macro. Note que os valores devem ser float e não int; 4) Escreva uma macro que soma os N elementos de um vetor apontado por um pointer. Quais problemas podem ser causados pela sua macro?

2- Inclusão de Arquivos, ou #include

É sempre uma boa ideia armazenar todas as macros num arquivo de cabeçalho (header file), que é incluído no topo de cada arquivo fonte. Isso garante que todas as definições são mantidas num único local, o que simplifica enormemente o trabalho de atualização das definições. Além das definições de macros, arquivos de cabeçalho tipicamente incluem o protótipo das funções exportadas e definições de tipos de dados. Arquivos de cabeçalho podem ser locais a um determinado programa ou projeto, e são encontrados no diretório corrente, ou são "arquivos de sistema" e contêm definições geradas pelos programadores do sistema operacional ou de bibliotecas com código de sistema, tais como as funções que efetuam operações de entrada e saída, por exemplo. Para arquivos de sistema a sintaxe é (maior e menor): #include <file> e o arquivo é buscado numa lista padronizada de diretórios, e normalmente, os diretórios que são buscados para arquivos de sistema são: /usr/local/include /usr/include A opção -v do cpp mostra os caminhos que são usados: prompt: cpp -v ... #include "..." search starts here: #include <...> search starts here: /usr/lib/gcc/x86_64-linux-gnu/4.8/include /usr/local/include /usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed /usr/include/x86_64-linux-gnu /usr/include End of search list. ... CTRL-D Esta lista pode ser aumentada com a opção -I dir do CPP. Os diretórios indicados são apensados no início da lista de busca. Para arquivos locais a sintaxe é (aspas duplas): #include "file" O texto do arquivo de cabeçalho é lido e copiado no local onde está o #include. Inclusões podem ser aninhadas e são processadas da maneira óbvia. Pode ocorrer em inclusões aninhadas, que um mesmo arquivo seja incluído duas vezes. Provavelmente isso provocará um erro de compilação, se por exemplo o arquivo incluído contiver a definição de tipos, porque o compilador reclamará da segunda definição. Para evitar inclusões duplas, o que se faz é definir um símbolo no topo do arquivo incluído; se este símbolo já foi definido, então a nova inclusão deve ser ignorada. Segue um exemplo; os números não fazem parte do arquivo. --foo.h-- 1 /* File foo. */ 2 #ifndef FILE_FOO_SEEN 3 #define FILE_FOO_SEEN 4 5 /* the entire file with several definitions */ 6 7 #endif /* !FILE_FOO_SEEN */ --fim-- A linha 2 (#ifndef) testa o estado do símbolo/macro FILE_FOO_SEEN; se este símbolo não foi previamente definido (Not DEFined), então ele é definido na linha 3 e o conteúdo de foo.h é incluído. A região protegida termina no #endif da linha 7. Na próxima inclusão, o teste da linha 2 falha e o conteúdo do arquivo é ignorado. FILE_FOO_SEEN é chamada de macro de guarda ou macro controladora.

3- Compilação Condicional, ou #if #else #endif

O teste "#if condição booleana" avalia a condição, e se falsa, o trecho de código que o segue é removido por CPP. O trecho de código abaixo é um exemplo de como empregar #if 0 para comentar um longo trecho de código que contém comentários. O teste é terminado por #endif. #if 0 /* longo trecho de código por ignorar */ // mais comentários #endif Versões alternativas podem ser escolhidas facilmente: #if 0 // versão por ignorar #else // versão a empregar #endif A cláusula "#elsif booleano" tem o comportamento esperado. O teste #ifdef resulta em verdadeiro caso o símbolo/macro esteja definido. Esta construção é usada para selecionar trechos de código por compilar. #ifndef resulta em verdadeiro se o símbolo não estiver definido. O que segue é um exemplo de uso destas construções. O símbolo cMIPS indica que o código deve ser gerado para execução direta no simulador cMIPS e portanto não pode conter código de bibliotecas. Copie o trecho de código para um arquivo chamado condicional.c e execute os dois comandos abaixo. --condicional.c--- // cMIPS.h define o símbolo cMIPS // #include "cMIPS.h" #ifndef cMIPS #include <stdio.h> #endif void main(void) { while( x != y ) { if( x > y ) { x -= y; #ifdef cMIPS print(x); // impressão sem função de biblioteca #else printf("%08x ",x); #endif } else { y -= x; #ifdef cMIPS print(y); // impressão sem função de biblioteca #else printf("%08x ",y); #endif } } } --fim-- cpp -DcMIPS condicional.c cpp -UcMIPS condicional.c Com -DcMIPS, a saída deve ser pequena (qual?), porque nenhum dos arquivos com cabeçalhos é incluído. Com -UcMIPS a saída deve ser longa (qual?) porque todos os arquivos com definições de entrada/saída são incluídos. A opção -DcMIPS define o símbolo cMIPS com o valor 1. A opção -DX=Y define o símbolo X com o valor Y. A opção -UcMIPS remove qualquer definição prévia do símbolo. Estas opções são iguais às suas equivalente no GCC, como preconiza a Lei do Menor Espanto (law of the least astonishment).
Resumo
  1. Compilador
  2. Montador
  3. Ligador
  4. CPP
  5. Substituição de macros
  6. Inclusão de arquivos
  7. Compilação condicional

--fim da aula--