CI064, 2019-1 © Roberto André Hexsel, 2014-2019
GCC, Compilação Condicional, Makefiles
Neste laboratório veremos os comandos para fazer uso do compilador,
compilação condicional e como automatizar a compilação de programas
complexos.
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 na 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 contem a função main(), e y.c que
contem 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 --em breve 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 mais
adiante.
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 em clog.o. grog.o pode usar
funções definidas em clog.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 normalmente buscados 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/x86_64-linux-gnu/libm.a
prompt: gcc prog.o /usr/lib/x86_64-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 x.o y.o z.o -o x # gera x, a partir de 3 objetos: x, y e z
gcc x.o -lm -o x # compila e liga com biblioteca libm
gcc x.o -I caminho/para/includes -o x
gcc x.o -L caminho/para/bibliotecas -o x
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
Mais detalhes sobre otimização serão vistos numa próxima aula.
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
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.
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. O
primeiro 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,7);
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?
Macros não são funções e não devem ser usadas como tal.
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.
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 do mesmo diretório em que
está o arquivo com 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 falhará e o conteúdo do arquivo
será 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 empregada
#endif
A cláusula "#elsif booleano" tem com 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 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---
#ifndef cMIPS
#include <stdio.h>
#else
#include "cMIPS.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--
prompt: touch cMIPS.h
prompt: cpp -DcMIPS condicional.c
prompt: 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).
How make makes
Você trabalha no projeto gen e, inteligentemente, dividiu o código
em três módulos, sendo que cada módulo contém o código para uma das
três funcionalidades de gen.
O módulo principal consiste dos arquivos fonte gen.c hen.c ien.c. Estes
fazem a inclusão de gen.h.
O segundo módulo consiste de p.c q.c r.c. Estes fazem a inclusão de p.h.
O terceiro módulo consiste de x.c y.c z.c. Estes fazem a inclusão de x.h.
Todos os arquivos fonte fazem a inclusão das definições contidas em gen.h.
Suponha, para fins de exemplo, que a compilação de cada arquivo fonte
demora uma unidade de tempo (T), e que a ligação de todos os arquivos
objeto também demora T unidades de tempo.
A primeira compilação de seu executável custa 10T: 9 x *.c + ligação.
Se T=0,01s, a compilação pode ser considerada instantânea e o comando
abaixo é perfeitamente aceitável:
gcc -Wall -O0 -g -o gen *.c
Se T=60s, a compilação de cada objeto tem um custo significativo, e
re-compilar tudo a cada modificação trivial começa a parecer um
tanto estúpido.
Se somente um arquivo fonte foi alterado, tal como r.c, então o tempo
mínimo para compilar é 2T: a compilação de r.c mais a ligação. Para alguém
que ignora make, a compilação demoraria 600s, ou 10 minutos.
Se demora uma semana para completar seu programa, com possivelmente 60
ciclos de edição-compilação-depuração, ignorando make, você
demoraria 60x10min, o que é mais do que DEZ horas com compilações inúteis.
Se você sabe como tirar proveito de make, suas compilações custariam
não muito mais do que 60x20s, ou 20-30 minutos.
Há um efeito de segunda ordem importante aqui: se você deve esperar 10
minutos por uma compilação, seu foco se perde de uma forma mais irrevogável
do que se você esperar somente 2 minutos. Mais detalhes em IHC.
O programa make interpreta e executa os comandos contidos
num Makefile. Estes comandos descrevem um processo de compilação,
que pode ir de um simples
gcc -Wall x.c -o x
até a compilação automática de uma família de programas tão complexa como
as binutils ou o próprio gcc.
Este é um curso de Ciência da Computação e não um curso para secretárias.
Em breve vocês deverão instalar em suas casas o conjunto de ferramentas de
montagem-ligação-compilação para gerar código para o MIPS. Estas
ferramentas consistem de alguns milhões de linhas de código fonte, e a
construção dos programas é assaz complexa, e todo o processo é automatizado
e gerenciado por shell scripts e Makefiles.
Acredito piamente que, para tornarem-se computeiros ao invés de secretárias
melhoradas, vocês devem acompanhar como platéia, ao menos uma vez na vida,
todo o processo de construção do gcc e binutils. Nesta altura de suas
vidas acadêmicas, será uma hora mais instrutiva do que qualquer outro
intervalo dispendido com redes sociais.
O manual de make encontra-se aqui.
Considere a seguinte coleção de arquivos. O conteúdo de cada arquivo está
entre as duas linhas com '---' e deve ser salvado com o nome indicado no
topo.
-- strings.h ----------------------------
int strconcat(char *, const char *);
int strrev(const char *, char *);
int strcopy(const char *, char *);
int strlength(const char *);
-----------------------------------------
-- main.c -------------------------------
#include <stdio.h>
#include "strings.h"
#define myprintfnum(n) printf("%d\n",(n));
#define myprintfstr(s) printf("%s\n",(s));
char f16[16]="abcd.efgh_ijkl-";
char d16[16];
char f8[] = "abcdefg";
char d8[] = "ABCDEFG";
char s64[64];
int main (void)
{
int num=0;
num = strrev(f16,d16);
myprintfstr(d16);
num = strcopy(f16,s64);
myprintfnum(num);
myprintfstr(s64);
num = strconcat(s64,f8);
myprintfnum(num);
myprintfstr(s64);
num = strlength(s64);
myprintfnum(num);
return(0);
}
-----------------------------------------
-- strings.c ----------------------------
char slocal[256];
int strrev(const char *si, char *so)
{
int n = 0; int m;
char *l = slocal;
while ((*l = *si)) {
l++; si++ ;
n++;
}
l--;
for (m = n; m > 0; m--)
*so++ = *l--;
*so = '\0' ;
return(n);
}
int strcopy(const char *y, char *x)
{
int i = 0;
while ( (*x++ = *y++) != '\0' )
i = i+1;
*x = '\0';
return(i+1);
}
int strconcat(char *si, const char *sf)
{
char *p = si;
int n = 0;
while (*p != '\0') {
++p;
n++;
}
while ( (*p++ = *sf++) )
n++;
return(n+1);
}
int strlength(const char *s)
{
int n = 0;
do {
++n;
} while (*s++ != '\0');
return(n);
}
-----------------------------------------
A compilação deste programa pode ser efetuada com o comando
gcc -Wall -o strings main.c strings.c
Num ciclo normal de edição, compilação, teste, depuração, o que ocorreria
são repetidas seções de edição seguidas de comandos de compilação.
Suponha que você esteja alterando somente o arquivo strings.c;
compilar todos os programas é desnecessário e uma perda de tempo.
Evidentemente, com este programa exemplo, a perda de tempo é pequena, mas
num projeto que não seja trivial, o acúmulo de desperdício é estúpido.
make pode ajudar. Criemos um Makefile para compilar o nosso
programa exemplo.
-- Makefile -----------------------------
strings: main.c strings.c strings.h
[TAB]gcc -Wall -o strings main.c strings.c
-----------------------------------------
Nosso Makefile contém somente uma regra. A sintaxe das regras é:
objetivo: dependência1 dependência2 ...
[TAB]ação1
[TAB]ação2
...
[TAB]açãoN
O objetivo (target) é o objeto a ser construído
pelas ações. As dependências determinam e sequência de
execução das regras do Makefile.
Note que cada ação DEVE ser precedida de um único caractere TAB, e
estes são mostrados em cor cinza nos exemplos.
No nosso Makefile, a regra tem como objetivo construir strings, que
é o resultado da compilação na única ação da regra. O programa strings
depende dos arquivos main.c, strings.c e strings.h, e qualquer alteração
nestes arquivos, causará a execução das ações para o objetivo strings.
Por "alteração" make entende que a data de modificação nos arquivos
das dependências é mais recente que a data de criação do objetivo. Se as
dependências são mais recentes que o objetivo, então as ações
correspondentes devem ser executadas para reconstruí-lo.
Releia o parágrafo acima. Ele é fundamental para a compreensão do
funcionamento de make.
O comando
prompt: make
causa a recompilação de strings caso algum dos arquivos fonte seja
mais recente do que o executável.
A versão GNU do make procura, no diretório corrente, por arquivos
chamados GNUmakefile, makefile e Makefile, nesta
ordem. Se encontrado, as receitas do *akefile são executadas, caso
isso seja necessário.
Até aqui, nada de muito mais interessante do que um carrinho de mão.
O Makefile pode conter definições de constantes e estas podem ser usadas
nas ações.
Vejamos como pode ficar nosso Makefile com a definição de duas constantes:
-- Makefile -----------------------------
CC = gcc
CFLAGS = -Wall -O2
strings: main.o strings.o strings.h
[TAB]$(CC) $(CFLAGS) -o strings main.o strings.o
-----------------------------------------
A variável CC define qual é o compilador que deve ser empregado. A
variável CFLAGS define as opções que devem ser passadas ao compilador.
Com CC e CFLAGS definidas, make usa regras implícitas para compilar
individualmente os arquivos objeto a partir dos fontes em C.
A variável é definida com a atribuição ao "seu nome"
CC = ...
e é referenciada com $()
... $(CC) ...
Nosso segundo Makefile emprega um truque: as dependências são os arquivos
objeto, e não mais os arquivos fonte e make faz uso de uma regra de
compilação implícita para gerar os arquivos objeto. O resultado da
execução é mostrado abaixo.
prompt: make
gcc -Wall -O2 -c -o main.o main.c
gcc -Wall -O2 -c -o strings.o strings.c
gcc -Wall -O2 -o strings main.o strings.o
As duas primeiras linhas são a aplicação da regra implícita para a
construção de arquivos objeto, a terceira é a regra explícita para a
construção do objetivo strings.
Nosso segundo Makefile tem um problema: não há menção da dependência de
main.c em strings.h. Para garantir a compilação correta, acrescentamos uma
variável e uma regra.
-- Makefile -----------------------------
CC = gcc
CFLAGS = -Wall -O2
DEPS = strings.h
strings: main.o strings.o
[TAB]$(CC) $(CFLAGS) -o strings main.o strings.o
main.o: main.c $(DEPS)
[TAB]$(CC) $(CFLAGS) -c -o main.o main.c
-----------------------------------------
O objetivo main.o depende de main.c e strings.h. O arquivo objeto
strings.o é construído com a regra implícita porque não depende de nenhum
arquivo de cabeçalho.
Importante: make constrói o primeiro objetivo do Makefile.
No nosso exemplo, o objetivo é strings e este deve ser o primeiro
objetivo no arquivo. Os objetivos secundários devem estar após o objetivo
principal. Se o que se deseja é somente construir o objetivo main.o,
o argumento para o comando make deve explicitar qual o objetivo
a ser construído:
prompt: make main.o
Suponha que seu programa que trata de cadeias de caracteres é parte de um
projeto maior e portanto todos os arquivos com cabeçalhos são mantidos no
diretório ./include, e todas as bibliotecas no diretório ./lib.
Como ainda não sabemos criar bibliotecas, por enquanto somente moveremos
strings.h para ./include.
prompt: mkdir include
prompt: mv strings.h include
O arquivo main.c deve ser modificado para que o #include
aponte para os includes "de sistema" e não mais no diretório corrente, de
#include "strings.h"
para
#include <strings.h>
O Makefile deve também deve ser atualizado:
-- Makefile -----------------------------
CC = gcc
CFLAGS = -Wall -O2
INCLUDEDIR = ./include
INCL = -I$(INCLUDEDIR)
DEPS = $(INCLUDEDIR)/strings.h
strings: main.o strings.o
[TAB]$(CC) $(CFLAGS) -o strings main.o strings.o
main.o: main.c $(DEPS)
[TAB]$(CC) $(CFLAGS) -c -o main.o main.c $(INCL)
-----------------------------------------
Isso é legal, mas ainda não equivale a uma carregadeira de verdade...
Variáveis especiais e coringas podem transformar nosso Makefile numa
carregadeira padrão mineradora-advanced-plus. Senão, vejamos:
O caractere '%' (percento) é usado para casamento de padrões para objetivos
genéricos. A regra abaixo determina que cada arquivo objeto (X.o) depende de
um arquivo (X.c) e de $(DEPS), e que para cada arquivo objeto, a ação seja
executada.
%.o: %.c $(DEPS)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
O padrão '%' equivale ao prefixo do nome que aparece no objetivo, e pode
ser usado para representar o mesmo prefixo na ação. No exemplo do
parágrafo acima o % equivale a X (X.o) no objetivo, e a X na ação (X.c).
O padrão '$@' equivale ao nome completo do objetivo da regra (%.o).
O padrão '$<' equivale ao primeiro elemento na lista de dependências (%.c).
No nosso programa exemplo, a ação acima seria expandida para
gcc -Wall -O2 -c -o main.o main.c -I./include
gcc -Wall -O2 -c -o strings.o strings.c -I./include
Nas ações, os seguintes padrões podem ser usados para representar nomes de
arquivos:
$@ nome completo do objetivo desta regra
$? dependências que são mais novas que o objetivo desta regra
$* string que corresponde ao % no objetivo
$< primeiro nome na lista de dependências
$^ lista de todas as dependências, separadas por espaço
Com estes padrões, nosso Makefile pode ser reescrito de forma mais genérica
e concisa.
-- Makefile -----------------------------
CC = gcc
CFLAGS = -Wall -O2
INCLUDEDIR = ./include
INCL = -I$(INCLUDEDIR)
DEPS = $(INCLUDEDIR)/strings.h
OBJ = main.o strings.o
strings: $(OBJ)
[TAB]$(CC) $(CFLAGS) -o $@ $^
%.o: %.c $(DEPS)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
-----------------------------------------
No interesse da limpeza pública e conservação de bits, falta-nos uma regra
para a limpeza dos resíduos da compilação. Tipicamente, esta regra é
chamada de clean.
-- Makefile -----------------------------
CC = gcc
CFLAGS = -Wall -O2
INCL = -I./include
DEPS = include/strings.h
OBJ = main.o strings.o
strings: $(OBJ)
[TAB]$(CC) $(CFLAGS) -o $@ $^
%.o: %.c $(DEPS)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
clean:
[TAB]rm -f $(OBJ) core *~ $(INCL)/*~
-----------------------------------------
Para remover os arquivos intermediários, basta dizer:
prompt: make clean
Usuários de emacs provavelmente gostam de remover os arquivos
de backup (*~).
Por estas alturas, a leitora perspicaz já entendeu o objetivo do
exercício: escrever um Makefile genérico que possa ser empregado em
qualquer projeto. A variável SRC indica os nomes dos arquivos fonte, e
as regras implícitas automatizam todo o processo de compilação.
Vejamos um Makefile genérico, do tipo "preencha os espaços em branco":
-- Makefile -----------------------------
# defina aqui quaisquer ferramentas particulares ao projeto
CC = gcc
CFLAGS = -Wall -O2
# arquivos com os fontes, objetos e inclusões
SRCx = x.c y.c z.c
SRCp = p.c q.c r.c
SRCgen = gen.c hen.c ien.c
OBJx = x.o y.o z.o
OBJp = p.o q.o r.o
OBJgen = gen.o hen.o ien.o
INCL = -I./include
# dependências particulares para minimizar as compilações
DEPSgen = include/gen.h
DEPSx = include/x.h
DEPSp = include/p.h
# objetivo principal para o passo final de ligação dos objeto
gen: $(OBJgen) $(OBJx) $(OBJp)
[TAB]$(CC) $(CFLAGS) -o $@ $^
$(DEPSx) : $(DEPSgen)
$(DEPSp) : $(DEPSgen)
# regra de compilação para os OBJx
$(OBJx): $(SRCx) $(DEPSx)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
# regra de compilação para os OBJp
$(OBJp): $(SRCp) $(DEPSp)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
# regra de compilação para os OBJgen
$(OBJgen): $(SRCgen) $(DEPSgen)
[TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL)
clean:
[TAB]rm -f $(OBJx) $(OBJp) $(OBJgen) core *~ $(INCL)/*~
-----------------------------------------
Essa complexidade toda não faz o menor sentido no exemplo
das strings. Em qualquer projeto não trivial, com dezenas de
arquivos fonte, minimizar o tempo de compilação é crucial. Ao
particularizar, e assim minimizar, as listas de dependências, o Makefile
garante que o menor número possível de arquivos objeto são re-compilados a
cada ciclo edição-compilação-teste. O ganho de tempo ao longo do
desenvolvimento (2 semanas) compensa plenamente o tempo para
eleborar um Makefile detalhado (1 hora).
Agora sim, temos uma carregadeira melhor que um carrinho de mão!
Sobre ferramentas cuja utilidade será demonstrada adiante
file O programa file mostra o "tipo" de arquivo do seu argumento.
Por exemplo, com argumentos strings.c e ./strings:
prompt: file strings.c
strings.c: C source, ASCII text
prompt: file strings # linhas quebradas para facilitar a leitura
strings: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), \
dynamically linked (uses shared libs), for GNU/Linux 2.6.26, \
BuildID[sha1]=0x1751c47dd11fc63a94099b16f2d4dc4bd87a35d4, not stripped
nm O programa nm (names) mostra os símbolos definidos no
arquivo objeto ou executável. Note que os símbolos printf e puts estão
marcados com U (undefined) e que pertencem à biblioteca glibc-2.0.
Os símbolos d16, s64 e slocal são dados não inicializados, marcados com B
(seção .bss); os símbolos main, strconcat strcopy strlength strrev marcados
com T (seção .text) são funções; os símbolos d8, f16, f8, marcados com D
são dados inicializados (seção .data). Detalhes nas próximas aulas.
prompt: nm strings
080497d8 d _DYNAMIC
080498cc d _GLOBAL_OFFSET_TABLE_
0804869c R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w _Jv_RegisterClasses
...
08049aa0 A _end
08048680 T _fini
08048698 R _fp_hw
080482e0 T _init
0804840c T _start
08049920 b completed.5730
08049940 B d16
080498f0 D d8
080498e8 W data_start
08049900 D f16
080498f8 D f8
080484c0 t frame_dummy
08048360 T main
U printf@@GLIBC_2.0
U puts@@GLIBC_2.0
08048460 t register_tm_clones
08049960 B s64
080499a0 B slocal
08048590 T strconcat
08048550 T strcopy
080485f0 T strlength
080484f0 T strrev
ldd O programa ldd mostra a lista de bibliotecas compartilhadas de
que seu argumento necessita.
prompt: ldd strings
linux-gate.so.1 => (0xb7750000)
libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xb75d2000)
/lib/ld-linux.so.2 (0xb7751000)
Mudando completamente de assunto...
SED
O editor SED (Serial EDitor) é um dos filtros mais úteis que
conheço e uso. O editor recebe uma sequência de caracteres em sua entrada
e emite uma sequência modificada em sua saída. SED é ideal para
pequenas/simples modificações em arquivos textuais. Tentarei apresentar o
básico sobre Expressões Regulares, que é a linguagem empregada por SED para
descrever as sequências de caracteres por substituir durante a edição.
Lembre que as substituições efetuadas por Bash parecem ser
expressões regulares mas não o são.
O manual de SED encontra-se aqui.
A maneira mais comum de uso do SED para substituições é
prompt: cat arquivo | sed -e "expressão" [-e "expressão"]
SED lê a entrada padrão, e para cada linha obtida da entrada, executa os
comandos nas expressões, um comando para cada expressão, e
mostra o resultado na saída padrão. A forma geral de um comando é:
[ender1][,ender2][!]comando[argumentos]
Itens entre colchetes são opcionais -- não escreva os colchetes.
Os dois endereços delimitam uma região sobre a qual o comando é aplicado.
Com somente um endereço o comando é aplicado em todas as linhas que "casam"
com o endereço. Com o '!' o comando é aplicado a todas as linhas
que não casam o endereço ou região.
Na linha de comando da shell um comando ao SED é sinalizado pela opção
-e, conforme exemplos abaixo. As expressões devem estar protegidas
da shell com aspas simples ou duplas.
O comando de substituição é um dos mais úteis e tem a forma geral:
[ender1][,ender2][!]s/padrao/subst/[argumentos]
ou a forma simplificada e utilíssima:
sed -e "s/padrao1/subst1/" -e "s:padrao2:subst2:g" ARQUIVO
O primeiro caractere após o 's' é o marcador de substituição e pode ser
qualquer caractere. Nos exemplos abaixo é o ':' ou o '/' mas poderia ser
qualquer outro que não apareça em padrao ou em subst.
O padrao para substituição é uma expressão regular
-- detalhes adiante.
Vejamos alguns exemplos. Na primeira linha é mostrado o comando do SED,
enquanto que a segunda linha, com o echo, pode ser executada para
evidenciar o resultado. Note as aspas para proteger o comando de Bash.
Substitui "abc" na entrada padrão por "def", trocando somente a primeira
ocorrência:
sed -e 's:abc:def:'
echo "12abc34def56 abc" | sed -e 's:abc:def:'
Idem, trocando todas as ocorrências numa mesma linha; g = global
sed -e 's:abc:def:g'
echo "12abc34def56 abc" | sed -e 's:abc:def:g'
Idem, trocando somente a terceira ocorrência de "abc":
sed -e 's:abc:def:3'
echo "12abc34def56 abc abc abc" | sed -e 's:abc:def:3'
Idem, troca uma vez, somente nas linhas em que ocorre "o endereço" xxx
sed -e '/xxx/s:abc:def:'
echo "12abc34def56 abc abc abc" | sed -e '/xxx/s:abc:def:g'
echo "12abc34def56 abc abc abc xxx" | sed -e '/xxx/s:abc:def:g'
Idem, troca uma vez, somente nas linhas SEM (!) o "endereço" xxx
sed -e '/xxx/!s:abc:def:'
echo "12abc34def56 abc abc abc" | sed -e '/xxx/!s:abc:def:g'
echo "12abc34def56 abc abc abc xxx" | sed -e '/xxx/!s:abc:def:g'
Idem, troca uma vez, somente nas linhas que iniciam (^) com xxx
sed -e '/^xxx/s:abc:def:'
echo "12abc34def56 abc abc abc xxx" | sed -e '/^xxx/s:abc:def:g'
echo "xxx 12abc34def56 abc abc abc" | sed -e '/^xxx/s:abc:def:g'
Idem, troca uma vez, somente nas linhas que terminam ($) com xxx
sed -e '/xxx$/s:abc:def:'
echo "12abc34def56 abc abc abc xxx" | sed -e '/xxx$/s:abc:def:g'
echo "xxx 12abc34def56 abc abc abc" | sed -e '/xxx$/s:abc:def:g'
Substitui "cdefg" por "xdefy"; a construção \(..\) é um agrupador
que pode ser usado em padrão; na substituição, o primeiro
agrupador é representado por \1, o segundo por \2, etc.
sed -e 's:c\(def\)g:x\1y'
echo "xxx cdefg abc 1def2 xxx" | sed -e 's:c\(def\)g:x\1y:'
"c" e "g" estão fora do agrupamento e são removidos na substituição; "x" e
"y" são acrescentados à "def".
O conteúdo do agrupador faz parte do padrão; todo o padrão é substituído
(cdefg) mas o agrupamento (def) pode ser reutilizado na substituição.
echo "xxx cdefg abc 1def2 xxx" | sed -e 's:c\(def\)g:\1 x\1y \1:'
Elimina linhas com xxx; d = delete
sed -e '/xxx/d'
echo "cdefg xxx 1def2" | sed -e '/xxx/d'
echo "cdefg zzz 1def2" | sed -e '/xxx/d'
Elimina linhas sem (!) xxx; d = delete
sed -e '/xxx/!d'
echo "cdefg xxx 1def2" | sed -e '/xxx/!d'
echo "cdefg zzz 1def2" | sed -e '/xxx/!d'
Elimina linhas que iniciam (^) com xxx; d = delete
sed -e '/^xxx/d'
echo "xxx cdefg 1def2" | sed -e '/^xxx/d'
echo "cdefg xxx 1def2" | sed -e '/^xxx/d'
Elimina linhas que terminam ($) com xxx; d = delete
sed -e '/xxx$/d'
echo "xxx cdefg 1def2" | sed -e '/xxx$/d'
echo "cdefg 1def2 xxx" | sed -e '/xxx$/d'
As expressões podem ser combinadas. O que fazem os programas abaixo?
sed -e '/xxx/s:abc:def:' -e '/xxx/!d' -e 's/aqui/acola/g'
echo "12abc34def56 aqui abc xxx" | \
sed -e '/xxx/s:abc:def:' -e '/xxx/!d' -e 's/aqui/acola/g'
echo "12abc34def56 aqui abc zzz" | \
sed -e '/xxx/s:abc:def:' -e '/xxx/!d' -e 's/aqui/acola/g'
Importante:
1) a ordem das expressões afeta o resultado porque elas são aplicadas sobre
a entrada na ordem em que aparecem na linha de comando; e
2) os exemplos todos mostram uma única linha; numa situação normal, um
arquivo com várias linhas seria processado por SED e os comandos são todos
aplicados a cada linha do texto, uma linha por vez. Existem maneiras de se
processar padrões de mais de uma linha; detalhes no manual, e exemplos nos
one-liners indicados no final deste texto.
Expressões Regulares
Expressões regulares (ER) são similares à algumas das substituições
efetuadas por Bash. Só similares, não iguais...
Repito porque é importante: programas
como sed, awk, emacs
empregam suas próprias definições de ERs, que são geradores de conjuntos.
As substituições de bash são coringas que lembram ERs.
Não escreva as aspas duplas quando empregar as ERs mostradas abaixo.
A ER "." equivale a um caractere.
A ER ".." equivale a dois caracteres.
A ER "*" equivale a zero ou mais instâncias da ER que a precede.
A ER "[a-z]" equivale a uma letra minúscula ([0-9] = um algarismo).
A ER "[a-z]*" equivale zero ou mais minúsculas.
A ER "[A-Za-z0-5]" equivale a uma maiúscula OU uma minúscula OU dígito
dentre 0,1,2,3,4,5.
A ER " " equivale a um caractere ESPAÇO (" *" = zero ou mais espaços).
A ER " *" equivale a um ou mais ESPAÇOs.
A ER "fusca\|BMW\|5" equivale a um dentre fusca, BMW, ou ao dígito 5.
A barra vertical escapada "\|" indica "alternativas".
A ER "..*" equivale a um ou mais caracteres.
A ER "^$" equivale a uma linha em branco (início ^ encostado no final $).
Vejamos alguns exemplos:
Substitui a ER "a.c" por "def", trocando todas as ocorrências:
sed -e 's:a.c:def:g'
echo "abc ac abbbbbbc" | sed -e 's:a.c:def:g'
Substitui a ER "a*c" por "def", trocando todas as ocorrências:
sed -e 's:a*c:def:g'
echo "abc ac abbbbbbc" | sed -e 's:a*c:def:g'
Esta ER significa: "zero ou mais ocorrências de a, seguido de c".
Substitui a ER "a*b" por "def", trocando todas as ocorrências:
sed -e 's:a*b:def:g'
echo "abc ac abbbbbbc" | sed -e 's:a*b:def:g'
Resumo
- Compilador
- Montador
- Ligador
- CPP
- Substituição de macros
- Inclusão de arquivos
- Compilação condicional
- Make
- Dependências
- Objetivos e ações
- Padrões e substituições
- O que faz nm?
- SED
- Filtros
- Expressões regulares
Exercícios
1) O que faz o comando abaixo?
cat zip_dec_4kA2B4WwAn | \
sed -n -e '/din_dmm/s:[\t ][\t ]*: :g'
2) O que faz o comando abaixo?
cat zip_dec_4kA2B4WwAn | \
sed -n -e '/din_dmm/s:[\t ][\t ]*: :g' \
-e 's:din_dmm \(0.....\) \(0.....\) \(0.....\) \(0.....\) .*:\4:p'
3) O que faz o comando abaixo?
A=1
B=16
W=WwAn
for P in frag jpeg_enc rtr zip_dec zip_enc ; do
echo -n -e "$P\n1k\t2k\t4k\t8k\t16k\t32k\t64k\n"
for C in 1k 2k 4k 8k 16k 32k 64k; do
cat ${P}_${C}A${A}B${B}${W} |\
sed -n -e '/din_dmm/s:[\t ][\t ]*: :g' \
-e 's:din_dmm \(0.....\) \(0.....\) \(0.....\) \(0.....\) .*:\4:p'
done
echo
done
4) Altere o comando acima para que ele resolva o primeiro
problema da aula anterior.
Agora é a hora de dar uma boa olhada na página do manual que trata das
expressões regulares.
5) Responda antes de executar: o que fazem os comandos abaixo?
echo "abc ac abbbbbbc" | sed -e 's:a\(b\)c:\1:g'
echo "abc ac abbbbbbc" | sed -e 's:a*\(b\)*c:\1:g'
echo "abc ac abbbbbbc abxc avbc" | sed -e 's:a[bx]*c:def:g'
echo "abc ac abbbbbbc abxc avbc" | sed -e 's:a.[bx]*c:def:g'
echo "abc ac abbbbbbc abxc avbc abvc" | sed -e 's:a.[bx]*c:def:g'
Salve a matriz abaixo num arquivo chamado X:
--corte aqui--
1 2 3 4 5
6 7 8 9 0
a b c d e
f g h i j
k l m n o
2 7 b g l
4 9 d i n
--corte aqui--
Para completar os exercícios abaixo, use o seguinte comando como modelo;
não salve os resultados em X, apenas observe-os na saída padrão.
cat X | sed -e 's/xxx/yyy/'
6) Troque todas ocorrências de 4 por 9.
7) Troque todas as ocorrências de "l m n" por "x p t".
8) Troque as ocorrências de n por N, somente nas linhas com k.
9) Remova as linhas que iniciam com números pares.
10) Mostre somente o terceiro elemento de uma linha que começa com uma
letra.
11) Mostre somente a primeira e a segunda colunas.
12) Mostre somente a primeira e a segunda colunas, trocando as duas.
13) Troque por Z o terceiro elemento da cada linha que começa com uma letra.
Use 5 agrupadores no "padrao".
14) Troque todas as ocorrências de 7 por i e todas as ocorrências de i por 7.
Isso feito, veja a página de manual. Não tente entender tudo; SED é
poderoso e sua linguagem de programação é telegráfica...
Esta página contém uma infinidade de exemplos de programas SED de uma
linha, ou os one-liners.
Desafios
1) Na lista de chamada, os "nomes" que são preposições tais como "da",
"de", "von", aparecem em maiúsculas, quando idealmente apareceriam em
minúsculas. Dada uma lista de nomes, obtida com o comando finger,
escreva um comando com SED que troque todas as ocorrências das preposições
que estejam em maiúsculas por minúsculas, e que capitaliza (somente a
primeira letra é maiúscula) corretamente os nomes. Inicie com algo como:
for N in /home/bcc/a* ; do
finger $(basename $N) | sed -e '/Login/!d' -e 's: D[Ee] : de :'
done
2) Escreva um script com SED que imite a funcionalidade de expansão
de macros do CPP. Considere somente os casos mais simples, de macros com
um ou dois parâmetros.
--fim da aula--