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.
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)
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
- Compilador
- Montador
- Ligador
- CPP
- Substituição de macros
- Inclusão de arquivos
- Compilação condicional
--fim da aula--