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. 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 --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.
perguntas e condições

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).
carrinho de mão

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... carregadeira 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! carregadeira-advanced-plus

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...


processamento serial

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

Escher 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
  1. Compilador
  2. Montador
  3. Ligador
  4. CPP
  5. Substituição de macros
  6. Inclusão de arquivos
  7. Compilação condicional
  8. Make
  9. Dependências
  10. Objetivos e ações
  11. Padrões e substituições
  12. O que faz nm?
  13. SED
  14. Filtros
  15. 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--