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

Makefiles


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 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. 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. 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. 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 prosaico 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 '%' (per-cento) é 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 # defina os arquivos com os fontes SRCx = x.c y.c z.c SRCp = p.c q.c r.c SRCgen = gen.c hen.c ien.c INCL = -I./include # defina as dependências particulares para minimizar as compilações DEPSgen = include/gen.h DEPSx = include/x.h DEPSp = include/p.h # defina o objetivo principal para o passo final de ligação dos objeto gen: $(OBJgen) $(OBJx) $(OBJp) [TAB]$(CC) $(CFLAGS) -o $@ $^ # defina a regra de compilação para os OBJx $(OBJx): $(SRCx) $(DEPSx) $(DEPSgen) [TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL) # defina a regra de compilação para os OBJp $(OBJp): $(SRCp) $(DEPSp) $(DEPSgen) [TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL) # defina a regra de compilação para os OBJgen $(OBJgen): $(SRCgen) $(DEPSgen) [TAB]$(CC) $(CFLAGS) -c -o $@ $< $(INCL) clean: [TAB]rm -f $(OBJ) 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/meses) 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). Mais 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-vdso.so.1 => (0x00007ffe169e6000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc202f5c000) /lib64/ld-linux-x86-64.so.2 (0x0000558adc76d000)

Resumo
  1. Make
  2. Dependências
  3. Objetivos e ações
  4. Padrões e substituições
  5. O que faz nm?

Exercícios --fim da aula--