Compilação separada e Make
As etapas da compilação, a compilação de projetos com vários arquivos e como automatizá-la com Makefile.
Na página de Tipo Abstrato de Dados, um programa
passou a ter vários arquivos .c e .h. Esta página trata das ferramentas
para compilar esses programas: as etapas da compilação, a compilação de cada
arquivo em separado e o make, que automatiza todo o processo. As flags usadas
em cada comando são as mesmas de sempre, explicadas em
Compilando em C.
Do código ao executável
O comando gcc faz, na verdade, três trabalhos diferentes:
- Pré-processamento: trata as linhas que começam com
#. Cada#includeé trocado pelo conteúdo do arquivo incluído, cada#defineé substituído onde aparece, e os trechos desligados por#if/#ifdefsão removidos. Para ver o resultado, usegcc -E arquivo.c. - Compilação: traduz cada arquivo
.c, já pré-processado, para código de máquina, gerando um arquivo objeto (.o). Cada.cé compilado sozinho: o compilador não olha os outros arquivos do projeto, só os protótipos que vieram dos#include. - Ligação: o ligador (
ld) junta os arquivos objeto e o código das bibliotecas usadas em um único executável. É aqui que cada chamada é associada ao código da função chamada.
Saber em que etapa um erro aconteceu ajuda a entender a mensagem:
| Etapa | Erro típico | Exemplo de mensagem |
|---|---|---|
| Pré-processamento | cabeçalho não encontrado | fatal error: Ponto.h: No such file or directory |
| Compilação | erro de sintaxe, tipo incompatível | error: expected ';' before '}' token |
| Ligação | função declarada mas não definida em nenhum .o | undefined reference to 'criaPonto' |
Os erros de ligação aparecem com ld ou collect2 na mensagem e citam o
arquivo objeto, não uma linha do código: o programa compilou, mas falta (ou
sobra) alguma função na hora de juntar as partes.
Compilando em etapas
-c interrompe o processo depois da compilação: gcc -c Aluno.c gera
Aluno.o, e a ligação fica para um comando posterior. Assim, cada .c pode
ser compilado separadamente, e só o que mudou precisa ser compilado de novo.
gcc -c Aluno.c # Aluno.o
gcc -c main.c # main.o
gcc main.o Aluno.o -o TADAluno # executável TADAlunoNa prática, cada comando leva as flags da disciplina. Duas delas exigem atenção quando a compilação é feita em etapas.
Quando a compilação é feita em etapas (-c e depois a ligação), a flag
precisa aparecer nas duas. Na compilação, ela instrumenta o código,
incluindo uma verificação antes de cada acesso à memória. Na ligação, ela
inclui a biblioteca do sanitizer. Só na ligação, o programa roda sem
verificar os acessos e erros como o estouro de vetor passam despercebidos.
Só na compilação, a ligação falha com undefined reference to
'_asan...'.
A -lm (e qualquer outra -l) vai no comando de ligação, e depois
dos arquivos objeto: gcc main.o Ponto.o -lm -o TADPonto. Em muitas
instalações, o ligador só aproveita de uma biblioteca as funções pedidas
por algum arquivo que veio antes dela na linha de comando (veja o
Makefile 5 dos exercícios).
Make
Em um projeto com muitos arquivos, é fácil esquecer o que precisa ser
recompilado depois de uma alteração. Recompilar tudo sempre funciona, mas é
lento, e recompilar "de cabeça" é arriscado: um .o desatualizado gera um
executável que não corresponde ao código. O make resolve isso: ele lê as
regras de um arquivo chamado Makefile, compara as datas de modificação dos
arquivos e refaz só o que ficou desatualizado.
Regras
Um Makefile é uma lista de regras no formato:
alvo: pré-requisitos
comando
comando- alvo (target): o arquivo a ser gerado, como
main.oou o executável; - pré-requisitos: os arquivos de que o alvo depende. Se algum deles for mais novo que o alvo, o alvo está desatualizado;
- comandos: o que fazer para gerar o alvo.
Cada linha de comando precisa começar com um caractere de tabulação
(Tab), não com espaços. Se o editor trocar o Tab por espaços, o make
recusa o arquivo com uma mensagem pouco clara: *** missing separator. Stop.
Um primeiro Makefile
Para o TAD Aluno (arquivos main.c, Aluno.c e Aluno.h):
all: TADAluno
TADAluno: main.o Aluno.o
gcc main.o Aluno.o -o TADAluno
main.o: main.c Aluno.h
gcc -c main.c
Aluno.o: Aluno.c Aluno.h
gcc -c Aluno.c
clean:
rm -f *.o TADAluno
run: TADAluno
./TADAlunoDigitando make, o primeiro alvo do arquivo (all) é construído. Ele
depende de TADAluno, que depende de main.o e Aluno.o, que dependem dos
fontes. O make percorre essas dependências e executa os comandos na ordem
necessária:
$ make
gcc -c main.c
gcc -c Aluno.c
gcc main.o Aluno.o -o TADAlunoRodando de novo, sem alterar nada:
$ make
make: Nothing to be done for 'all'.Agora altere só Aluno.c (um comentário basta) e rode make:
$ make
gcc -c Aluno.c
gcc main.o Aluno.o -o TADAlunoAluno.o ficou mais velho que Aluno.c e foi recompilado. main.o continua
mais novo que main.c e Aluno.h, e não precisou ser tocado. Mas o novo
Aluno.o é mais novo que TADAluno, e o executável foi ligado de novo.
Com esse Makefile, e tudo já compilado, você altera só Aluno.h e roda
make. Quais comandos são executados?
Alvos que não são arquivos
all, clean e run não geram arquivos com esses nomes: são apenas nomes
para ações. Os nomes mais comuns são all (o primeiro alvo, construído por
um make sem argumentos), clean (apaga o que foi gerado) e run (executa
o programa).
make # constrói o primeiro alvo (all)
make run # constrói, se necessário, e executa
make clean # apaga os .o e o executávelHá um detalhe: se existir um arquivo chamado clean na pasta, o make
acha que o alvo já está pronto e não faz nada:
$ make clean
make: 'clean' is up to date.Para evitar isso, declare esses alvos como .PHONY (falsos): o make passa
a executá-los sempre, sem procurar um arquivo com o nome.
.PHONY: all clean runVariáveis
O Makefile acima ainda não usa as flags da disciplina. Copiá-las em cada comando seria repetitivo e fácil de errar. Em vez disso, use variáveis:
CFLAGS = -std=c11 -O0 -g -Wall -Wextra -Werrore escreva $(CFLAGS) onde elas forem usadas. Para trocar uma flag, basta
alterar uma linha. Alguns nomes são convenções seguidas pela maioria dos
projetos:
| Variável | Uso |
|---|---|
CC | o compilador (gcc) |
CFLAGS | flags de compilação (-c) |
LDFLAGS | flags de ligação |
LDLIBS | bibliotecas (-lm) |
Em EXECUTAVEL = TADAluno # nome do executável, o espaço antes do # faz
parte do valor da variável, que fica "TADAluno ". Na maioria dos usos não
faz diferença, mas pode quebrar comandos de forma difícil de entender.
Prefira comentários em linhas separadas.
Variáveis automáticas e o @
Dentro de uma regra, o make define algumas variáveis automaticamente:
| Variável | Valor |
|---|---|
$@ | o alvo da regra |
$< | o primeiro pré-requisito |
$^ | todos os pré-requisitos |
Na regra main.o: main.c Aluno.h, $@ é main.o, $< é main.c e $^ é
main.c Aluno.h. Assim, o comando de compilação fica
$(CC) $(CFLAGS) -c $<, sem repetir o nome do arquivo.
Por padrão, o make mostra cada comando antes de executá-lo. Um @ no
começo do comando esconde essa linha, o que é útil para trocar comandos
longos por mensagens curtas com echo.
Um Makefile completo
Juntando tudo:
# Makefile do TAD Aluno
CC = gcc
CFLAGS = -std=c11 -O0 -g -fno-omit-frame-pointer \
-Wall -Wextra -Werror -Wshadow \
-Wno-sign-compare -Wno-unused-parameter \
-Wno-unused-variable -Wno-unused-result \
-fsanitize=address,undefined
LDFLAGS = -fsanitize=address,undefined
LDLIBS = -lm
EXECUTAVEL = TADAluno
all: $(EXECUTAVEL)
$(EXECUTAVEL): main.o Aluno.o
@echo "Ligando $^ em $@"
@$(CC) $^ $(LDFLAGS) $(LDLIBS) -o $@
main.o: main.c Aluno.h
@echo "Compilando $<"
@$(CC) $(CFLAGS) -c $<
Aluno.o: Aluno.c Aluno.h
@echo "Compilando $<"
@$(CC) $(CFLAGS) -c $<
run: $(EXECUTAVEL)
./$(EXECUTAVEL)
clean:
rm -f *.o $(EXECUTAVEL)
.PHONY: all run clean$ make
Compilando main.c
Compilando Aluno.c
Ligando main.o Aluno.o em TADAlunoRepare que -fsanitize=address,undefined aparece em CFLAGS e em
LDFLAGS, que as bibliotecas vêm depois dos arquivos objeto, e que cada
.o lista como pré-requisitos o seu .c e os .h que ele inclui. A barra
invertida no fim de uma linha continua a definição na linha seguinte. Para
outro projeto, basta trocar o nome do executável e as regras dos arquivos
objeto.
Para ir além: regras de padrão e dependências automáticas
Em projetos com muitos arquivos, escrever uma regra para cada .o cansa. Uma
regra de padrão vale para todos os arquivos que seguem o padrão:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@Ela diz como gerar qualquer x.o a partir de x.c. Os .h de cada um
ainda precisam ser listados, em linhas sem comando:
main.o: Aluno.h
Aluno.o: Aluno.hManter essa lista à mão também é fácil de errar. As flags -MMD -MP fazem o
próprio gcc gerar, a cada compilação, um arquivo .d com os cabeçalhos que
cada .c inclui, e a linha -include $(wildcard *.d) no Makefile os
carrega. Não é necessário para os projetos da disciplina, mas é o que
projetos maiores usam.
Exercícios
Encontre o erro
O executor desta página não roda o make, então os exercícios abaixo mostram
o Makefile e o que aconteceu ao usá-lo. Em todos, o projeto é o TAD Aluno ou
o TAD Ponto, com os arquivos corretos.
Makefile 1
TADAluno: main.o Aluno.o
gcc main.o Aluno.o -o TADAluno
main.o: main.c Aluno.h
gcc -c main.c
Aluno.o: Aluno.c Aluno.h
gcc -c Aluno.c$ make
Makefile:2: *** missing separator. Stop.Resposta
As linhas de comando começam com quatro espaços, e não com um Tab. Muitos
editores trocam o Tab por espaços automaticamente. No VS Code, o canto
inferior direito da janela mostra "Spaces" ou "Tab Size", e para arquivos
Makefile ele normalmente já usa Tab.
Makefile 2
CFLAGS = -std=c11 -Wall -Wextra
TADPonto: main.o Ponto.o
gcc main.o Ponto.o -lm -o TADPonto
main.o: main.c
gcc $(CFLAGS) -c main.c
Ponto.o: Ponto.c
gcc $(CFLAGS) -c Ponto.cO projeto compilou e funcionou. Depois, os parâmetros de criaPonto e de
atribuiPonto mudaram de double para float, em Ponto.h e em Ponto.c:
$ make
gcc -std=c11 -Wall -Wextra -c Ponto.c
gcc main.o Ponto.o -lm -o TADPonto
$ ./TADPonto
Distancia: 0.0000
q = (0.0, 0.0), distancia: 0.0000Depois de um rm -f *.o TADPonto e de um novo make, tudo volta a
funcionar.
Resposta
main.o não lista Ponto.h como pré-requisito. Quando o cabeçalho mudou, o
make não recompilou main.c, e main.o continuou chamando criaPonto
como se ela recebesse double. O executável juntou um main.o compilado com
a interface antiga e um Ponto.o compilado com a nova, e os valores chegaram
errados, sem nenhuma mensagem de erro. A correção é listar os cabeçalhos:
main.o: main.c Ponto.h
Ponto.o: Ponto.c Ponto.hMakefile 3
Aluno.c tem um erro proposital: a->nome[MAX] = '\0'; escreve uma posição
além do vetor nome.
CFLAGS = -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow
LDFLAGS = -fsanitize=address,undefined
EXECUTAVEL = TADAluno
$(EXECUTAVEL): main.o Aluno.o
gcc main.o Aluno.o $(LDFLAGS) -o $(EXECUTAVEL)
main.o: main.c Aluno.h
gcc $(CFLAGS) -c main.c
Aluno.o: Aluno.c Aluno.h
gcc $(CFLAGS) -c Aluno.c
run: $(EXECUTAVEL)
./$(EXECUTAVEL)$ make run
gcc -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow -c main.c
gcc -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow -c Aluno.c
gcc main.o Aluno.o -fsanitize=address,undefined -o TADAluno
./TADAluno
Nome: Maria | Matricula: 2024001 | CR: 8.50
Nome: Maria | Matricula: 2024001 | CR: 10.00Por que o erro não foi detectado?
Resposta
-fsanitize só aparece na ligação. Os arquivos objeto foram compilados sem
instrumentação, então nenhum acesso à memória é verificado. A ligação inclui
a biblioteca do sanitizer, que ainda detecta vazamentos e free inválidos,
mas não o acesso fora do vetor. Com a flag também em CFLAGS, a execução
acusa o erro:
Aluno.c:29:12: runtime error: index 50 out of bounds for type 'char [50]'Makefile 4
O mesmo projeto, agora com a flag só em CFLAGS:
CFLAGS = -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow -fsanitize=address,undefined
EXECUTAVEL = TADAluno
$(EXECUTAVEL): main.o Aluno.o
gcc main.o Aluno.o -o $(EXECUTAVEL)
main.o: main.c Aluno.h
gcc $(CFLAGS) -c main.c
Aluno.o: Aluno.c Aluno.h
gcc $(CFLAGS) -c Aluno.c$ make
gcc -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow -fsanitize=address,undefined -c main.c
gcc -std=c11 -O0 -g -Wall -Wextra -Werror -Wshadow -fsanitize=address,undefined -c Aluno.c
gcc main.o Aluno.o -o TADAluno
/usr/bin/ld: main.o: in function `_sub_D_00099_0':
main.c:15:(.text+0x8b): undefined reference to `__asan_unregister_globals'
...
collect2: error: ld returned 1 exit status
make: *** [Makefile:5: TADAluno] Error 1Resposta
Agora o contrário: os arquivos objeto foram instrumentados e chamam funções
do sanitizer (as __asan_...), mas a ligação não inclui a biblioteca que as
define. A flag precisa estar também no comando de ligação, por exemplo em
uma variável LDFLAGS usada ali.
Makefile 5
Os arquivos objeto já estão compilados. Falta ligar.
TADPonto: main.o Ponto.o
gcc -lm main.o Ponto.o -o TADPonto$ make
gcc -lm main.o Ponto.o -o TADPonto
/usr/bin/ld: Ponto.o: in function `distanciaPonto':
Ponto.c:(.text+0x15d): undefined reference to `sqrt'
collect2: error: ld returned 1 exit statusResposta
A -lm está, mas no lugar errado. O ligador processa os arquivos na ordem
da linha de comando e, em muitas instalações (como no Ubuntu), só aproveita
de uma biblioteca as funções que já foram pedidas por algum arquivo
anterior. Quando ele lê -lm, ninguém pediu
sqrt ainda, e a biblioteca é descartada. As bibliotecas vão depois dos
arquivos objeto: gcc main.o Ponto.o -lm -o TADPonto.
Referências
- GNU make. Manual oficial.
- Overall Options (
-c,-o,-E). Documentação do gcc.
Tipo Abstrato de Dados
Interface, implementação e cliente. Como esconder os campos de uma struct, separar um programa em módulos e compilar cada parte separadamente.
Listas simplesmente encadeadas
Nós ligados por ponteiros, o TAD Lista com início, fim e tamanho, e as operações de inserir, buscar, remover, copiar e liberar.