C Como Funciona
Você escreve um texto num arquivo ola.c, digita um comando, e aparece um programa que roda. O que acontece no meio? Nesta aula você vai abrir a "caixa-preta" do compilador e ver, de verdade, as quatro etapas que transformam o seu texto num programa executável. Você vai olhar o arquivo que cada etapa produz, entender por que um programa em C roda sem precisar do compilador, e por que toda mudança no código pede uma nova compilação.
- O caminho completo: pré-processador → compilador → montador → ligador.
- Como parar o
gccno meio do caminho para ver cada etapa:-E,-Se-c. - O que são o arquivo
.i, o.s(assembly), o.o(objeto) e o executável. - A diferença prática entre linguagem compilada e interpretada.
- Por que o programa pronto roda sem o
gcc, e por que é preciso compilar de novo depois de cada mudança. - Como reconhecer de qual etapa veio uma mensagem de erro.
Os comandos desta aula foram executados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o gcc 15.2. Não se preocupe em decorar as opções -E, -S e -c: no dia a dia você vai usar um único comando, que faz as quatro etapas de uma vez. Aqui elas servem para você enxergar o caminho.
A ideia: publicar um livro traduzido
Imagine que a Ana escreveu um livro em português e quer vendê-lo num país que só lê japonês. O livro passa por quatro pessoas, uma depois da outra:
- A organizadora junta tudo num manuscrito só. Onde a Ana escreveu "inserir aqui o capítulo de receitas", ela cola o capítulo inteiro. Ela não traduz nada: só junta.
- O tradutor lê o manuscrito completo, confere se as frases fazem sentido (se não fizerem, devolve com um bilhete apontando a linha) e escreve a versão em japonês.
- A gráfica transforma o texto traduzido em páginas impressas, prontas, mas ainda soltas. Onde o livro diz "veja o mapa no apêndice", fica um espaço em branco: o mapa vem de outro lugar.
- O encadernador junta as páginas da Ana com as partes que vêm de fora (o mapa, o índice padrão da editora), preenche os espaços e entrega o livro pronto.
Compilar um programa em C é exatamente isso. Cada pessoa é uma ferramenta, e cada uma produz um arquivo:
| Etapa | Ferramenta | Recebe | Produz | Para parar aqui |
|---|---|---|---|---|
| 1. Juntar | Pré-processador | ola.c (o seu código) | ola.i: o código com os #include já colados | gcc -E |
| 2. Traduzir | Compilador (propriamente dito) | o código pré-processado | ola.s: assembly, as instruções do processador escritas em texto | gcc -S |
| 3. Montar | Montador (assembler) | ola.s | ola.o: arquivo objeto, código de máquina com "espaços em branco" | gcc -c |
| 4. Ligar | Ligador (linker) | ola.o + a biblioteca padrão do C | ola: o executável, pronto para rodar | (é o final) |
O gcc é, na verdade, um "gerente": ele chama uma ferramenta de cada vez e passa o resultado de uma para a outra. Quando você digita gcc ola.c -o ola, as quatro etapas acontecem em sequência, e os arquivos intermediários são criados numa pasta temporária e apagados no fim. Vamos fazer uma por uma.
O ponto de partida: o código-fonte
O arquivo que você escreve se chama código-fonte (source code). É um texto comum, que qualquer editor abre. Este é o programa mais simples que existe:
#include <stdio.h>
int main(void) {
printf("Olá, mundo!\n");
return 0;
}
$ ls -l ola.c -rw-rw-r-- 1 ana ana 83 out 17 10:30 ola.c « 83 bytes: só texto
São 83 bytes, ou seja, 83 caracteres (o á conta como 2, porque no formato UTF-8 ele ocupa dois bytes, e cada quebra de linha conta como 1). Guarde esse número: vamos ver o que acontece com ele em cada etapa.
Etapa 1: o pré-processador (gcc -E)
O pré-processador é a "organizadora" do livro. Ele procura as linhas que começam com #, as chamadas diretivas, e as executa. A diretiva #include <stdio.h> quer dizer: "cole aqui o conteúdo do arquivo stdio.h". Esse arquivo vem com o sistema e contém tudo o que o compilador precisa saber sobre o printf e as outras funções de entrada e saída (stdio = standard input/output).
A opção -E manda o gcc parar logo depois do pré-processador. O -o ola.i diz em que arquivo guardar o resultado (-o = output, saída; .i é a extensão tradicional de código C pré-processado):
$ gcc -E ola.c -o ola.i
$ wc -l ola.c ola.i « conta as linhas de cada arquivo
6 ola.c
844 ola.i « de 6 linhas para 844!
850 total
As 6 linhas viraram 844. Isso porque o stdio.h inclui outros arquivos, e o pré-processador colou todos eles. Os comandos head e tail mostram só o começo e o fim do arquivo:
$ head -5 ola.i
# 0 "ola.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
$ tail -8 ola.i
# 2 "ola.c" 2
# 3 "ola.c"
int main(void) { « o seu código está lá no final
printf("Olá, mundo!\n");
return 0;
}
As linhas que começam com # e um número são marcadores: anotações que o pré-processador deixa para o compilador saber de que arquivo e de que linha veio cada trecho. É graças a elas que, quando há um erro, a mensagem diz ola.c:4 e não "linha 841 do arquivo colado".
E onde está o printf? O comando grep procura um texto num arquivo (o -n mostra o número da linha):
$ grep -n "extern int printf" ola.i 492:extern int printf (const char *__restrict __format, ...); « veio do stdio.h
A linha 492 é uma declaração: ela diz ao compilador "existe uma função chamada printf, que recebe um texto e mais o que vier (...) e devolve um int". Repare: ela diz só como usar o printf. As instruções que de fato escrevem na tela não estão aqui: estão guardadas em outro lugar, já compiladas. Lembre-se desse "outro lugar": ele volta na etapa 4.
O pré-processador só mexe em texto
Além de colar arquivos, o pré-processador apaga os comentários e troca as palavras definidas com #define. Veja este outro programa:
#include <stdio.h>
#define NOME "Ana"
int main(void) {
// escreve a saudação
printf("Oi, %s!\n", NOME);
return 0;
}
Sem o -o, o gcc -E escreve o resultado na tela; o | tail -6 mostra só as 6 últimas linhas:
$ gcc -E saudacao.c | tail -6
# 4 "saudacao.c"
int main(void) {
« o comentário sumiu
printf("Oi, %s!\n", "Ana"); « NOME virou "Ana"
return 0;
}
O comentário virou uma linha em branco, e o NOME foi trocado por "Ana", como um "localizar e substituir" de editor de texto. O compilador nem fica sabendo que existia um NOME. O pré-processador não entende C: ele não sabe se o seu código está certo, só mexe no texto. A aula Pré-processador mostra todas as diretivas.
Missão: rode e veja Oi, Ana!. Agora mude só a linha do #define para o programa cumprimentar a Bia, sem mexer no printf. Rode e confira Oi, Bia!: o pré-processador trocou o NOME pelo texto novo antes de o compilador ver o código.
#include <stdio.h>
#define NOME "Ana"
int main(void) {
// escreve a saudação
printf("Oi, %s!\n", NOME);
return 0;
}
#include <stdio.h>
#define NOME "Bia"
int main(void) {
// escreve a saudação
printf("Oi, %s!\n", NOME);
return 0;
}
Etapa 2: o compilador (gcc -S)
Agora entra o "tradutor", o compilador propriamente dito (o programa cc1, que o gcc chama por baixo). Ele lê o código pré-processado, confere se tudo segue as regras do C (é aqui que aparecem quase todas as mensagens de erro) e traduz para assembly: as instruções do processador, uma por linha, escritas com palavras curtas em vez de números. A opção -S para o gcc nesse ponto:
$ gcc -S ola.c -o ola.s $ head -28 ola.s « as 28 primeiras linhas .file "ola.c" .text .section .rodata .LC0: .string "Ol\303\241, mundo!" « o seu texto, guardado como dados .text .globl main .type main, @function main: « aqui começa a função main .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rax « pega o endereço do texto movq %rax, %rdi call puts@PLT « chama a função puts movl $0, %eax « o return 0 popq %rbp .cfi_def_cfa 7, 8 ret « volta: fim do main .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 15.2.0-16ubuntu1) 15.2.0"
O resto do arquivo (mais 17 linhas) são anotações para o sistema. Você não precisa entender assembly para programar em C (essa é justamente a vantagem do C!), mas vale reparar em três coisas:
- .string "Ol\303\241, mundo!"
- O texto do
printf, guardado como dados do programa. Oáaparece como\303\241: são os dois bytes que o representam em UTF-8, escritos em números (octal). Repare que o\ndo fim sumiu. Já vamos ver por quê. - call puts@PLT
- "Chame a função
puts". Mas você escreveuprintf! O compilador percebeu que você estava imprimindo um texto fixo, terminado em\n, e trocou pela funçãoputs(put string, "pôr um texto"), que faz a mesma coisa mais depressa e sempre acrescenta a quebra de linha sozinha. O compilador tem liberdade para fazer trocas assim, desde que o resultado seja o mesmo. - movl $0, %eax · ret
- O
return 0;: coloca o 0 no lugar combinado para a resposta e volta para quem chamou omain.
O assembly depende do processador: este é o de um processador x86-64 (Intel e AMD de 64 bits). Num celular ou num Mac com chip Apple (processadores ARM), o mesmo ola.c vira um assembly completamente diferente. É exatamente isso que o C resolveu em 1973: você escreve o código uma vez, e o compilador de cada máquina traduz para a língua dela.
Etapa 3: o montador (gcc -c)
A "gráfica" é o montador (o programa as, de assembler). Ele transforma o texto do assembly nos números do código de máquina e guarda tudo num arquivo objeto, com a extensão .o. A opção -c (de compile only, "só compilar") faz as etapas 1, 2 e 3 e para antes de ligar:
$ gcc -c ola.c -o ola.o $ ls -l ola.o -rw-rw-r-- 1 ana ana 1496 out 17 10:35 ola.o $ file ola.o « o comando file diz que tipo de arquivo é ola.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
O .o não é mais texto: é código de máquina (abri-lo num editor mostra só símbolos estranhos). O file diz que é um arquivo ELF (o formato de programas do Linux) do tipo relocatable, "realocável": um pedaço de programa que ainda não está pronto para rodar. Por que não? O comando nm lista os nomes (os símbolos) que estão dentro do arquivo objeto:
$ nm ola.o
0000000000000000 T main « T: o main está aqui dentro
U puts « U: o puts NÃO está aqui
O T (de text, que nesse mundo quer dizer "código") diz que o main está definido neste arquivo. O U quer dizer undefined, "não definido": o arquivo usa o puts, mas não sabe onde ele está. Essa é a página da gráfica com o "espaço em branco para o mapa". Alguém precisa preencher esse buraco.
Etapa 4: o ligador
O "encadernador" é o ligador (linker; o programa ld). Ele junta o seu ola.o com a biblioteca padrão do C (no Linux, a glibc, onde mora o puts já compilado), preenche os buracos e escreve o executável. Para ligar, basta dar o .o ao gcc:
$ gcc ola.o -o ola $ file ola ola: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2240de07eb0be18171c33c62c7b593f967db3832, for GNU/Linux 3.2.0, not stripped $ ./ola Olá, mundo!
Agora o file diz executable, "executável", e o programa roda. Ele é dynamically linked ("ligado dinamicamente"): em vez de copiar o puts para dentro do executável, o ligador anotou "use o puts da biblioteca do sistema". O comando ldd mostra de quais bibliotecas o programa precisa:
$ ldd ola linux-vdso.so.1 (0x000076c4d0380000) libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x000076c4d0000000) « a biblioteca do C /lib64/ld-linux-x86-64.so.2 (0x000076c4d0382000)
A libc.so.6 é a biblioteca padrão do C, que vem instalada em todo Linux. Quando o programa começa, o sistema carrega essa biblioteca junto e liga o puts de verdade. A aula Bibliotecas explica a diferença entre ligar "dinamicamente" e "estaticamente".
Todos os arquivos, lado a lado
$ ls -l ola.c ola.i ola.s ola.o ola -rwxrwxr-x 1 ana ana 15952 out 17 10:35 ola « 4. executável (o x quer dizer "pode rodar") -rw-rw-r-- 1 ana ana 83 out 17 10:30 ola.c « 0. código-fonte -rw-rw-r-- 1 ana ana 21430 out 17 10:35 ola.i « 1. pré-processado -rw-rw-r-- 1 ana ana 1496 out 17 10:35 ola.o « 3. objeto -rw-rw-r-- 1 ana ana 664 out 17 10:35 ola.s « 2. assembly
| Arquivo | Tamanho | É texto? | O que tem dentro |
|---|---|---|---|
ola.c | 83 bytes | Sim | O que você escreveu. |
ola.i | 21.430 bytes | Sim | O seu código com o stdio.h (e o que ele inclui) colado em cima. |
ola.s | 664 bytes | Sim | As instruções do processador em texto (assembly). |
ola.o | 1.496 bytes | Não | Código de máquina, com um buraco para o puts. |
ola | 15.952 bytes | Não | O programa pronto: o código de máquina e mais as informações que o sistema precisa para carregá-lo e ligá-lo à libc. |
Tudo de uma vez
No dia a dia, ninguém roda as quatro etapas separadas. O comando do guia faz tudo de uma vez:
$ gcc -std=c23 -Wall -Wextra ola.c -o ola « as quatro etapas, em sequência $ ./ola Olá, mundo!
Os arquivos intermediários existem só por um instante, numa pasta temporária. Se você quiser guardá-los, a opção -save-temps ("guardar os temporários") deixa todos eles na pasta:
$ gcc -save-temps ola.c -o ola $ ls ola* ola ola.c ola.i ola.o ola.s
E a opção -v (verbose, "falante") faz o gcc mostrar cada programa que ele chama. Entre dezenas de linhas, lá estão as ferramentas das etapas (as linhas foram cortadas no fim, porque são enormes):
$ gcc -v ola.c -o ola 2>&1 | grep -E "^ " | cut -c1-70 /usr/libexec/gcc/x86_64-linux-gnu/15/cc1 -quiet -v -imultiarch x86_64 « etapas 1 e 2: o cc1 /usr/lib/gcc/x86_64-linux-gnu/15/include /usr/local/include /usr/include/x86_64-linux-gnu /usr/include « as pastas onde ele procura os .h /usr/bin/x86_64-linux-gnu-as -v --64 -o /tmp/ccsCx5i2.o /tmp/cc0hNEg2 « etapa 3: o montador (as) /usr/libexec/gcc/x86_64-linux-gnu/15/collect2 -plugin /usr/libexec/gc « etapa 4: o collect2, que chama o ld
Repare: o cc1 faz o pré-processamento e a compilação juntos (antigamente eram programas separados), o as monta, e o collect2 chama o ligador ld. O nome collect2 vai aparecer de novo nas mensagens de erro do ligador.
No editor do guia
As caixas Experimente fazem o mesmo caminho, só que dentro do navegador e com o compilador Clang: pré-processar, compilar, montar e ligar, e depois rodar. O "computador" do editor é diferente (é o WebAssembly, um processador imaginário que o navegador sabe executar), então o código de máquina gerado é outro, mas as etapas são as mesmas. Você vai ver isso na missão do fim da aula.
Compilado × interpretado, na prática
Na aula O que é C você viu a diferença em teoria. Agora dá para ver na prática. Num programa Python, o arquivo .py é o próprio programa: toda vez que ele roda, o interpretador python3 lê o texto e executa. No C, o arquivo .c só é lido uma vez, na compilação; quem roda depois é o executável.
| C (compilado) | Python (interpretado) | |
|---|---|---|
| Para rodar | gcc ola.c -o ola uma vez, depois ./ola quantas vezes quiser | python3 ola.py, sempre |
| Quem lê o código-fonte | Só o compilador, na hora de compilar | O interpretador, a cada execução |
| O que você entrega para outra pessoa | O executável (ela não precisa do compilador nem do código) | O código-fonte (ela precisa do Python instalado) |
| Erros de digitação | Aparecem antes de rodar: sem compilar, não há programa | Muitos só aparecem quando a linha com erro é executada |
| Mudou o código? | Precisa compilar de novo | É só rodar de novo |
| Funciona em outro tipo de computador? | Só depois de compilar de novo para ele (o executável x86-64 não roda num processador ARM) | Sim, se lá tiver o Python |
O executável roda sem o código-fonte
Depois de compilado, o programa não precisa mais do ola.c nem do gcc. Para provar, a Ana apaga o código-fonte e roda o programa:
$ rm ola.c « apaga o código-fonte! $ ./ola Olá, mundo! « o programa continua funcionando
O executável já contém as instruções de máquina; ele só precisa da biblioteca do sistema que o ldd mostrou, que vem instalada em todo Linux. É assim que os programas que você baixa funcionam: eles chegam prontos, e você não precisa de um compilador para usá-los. (Não faça isso com o seu código de verdade: sem o .c, você não consegue mais mudar o programa. A Ana tinha uma cópia.)
Mudou o código? Compile de novo
O outro lado da moeda: o executável é uma "fotografia" do código no momento em que foi compilado. A Ana trocou mundo por Ana no ola.c, salvou, e rodou o programa:
$ cat ola.c
#include <stdio.h>
int main(void) {
printf("Olá, Ana!\n");
return 0;
}
$ ./ola
Olá, mundo! « o programa velho: o .c mudou, o executável não
$ gcc -std=c23 -Wall -Wextra ola.c -o ola « compila de novo
$ ./ola
Olá, Ana! « agora sim
Esquecer de recompilar é uma das confusões mais comuns de quem começa: "mudei o código e nada mudou!". Lembre-se: editou, compilou, rodou. As caixas Experimente fazem isso por você: o botão Rodar sempre compila antes de rodar. Em projetos grandes, compilar tudo de novo demora; por isso existe o Makefile, que recompila só os arquivos que mudaram.
Quando dá errado
Cada etapa tem os seus erros, e saber de qual etapa veio a mensagem já é meio caminho para consertar. A pista está no começo e no fim da mensagem.
fatal error: … No such file
#include <stdoi.h>
int main(void) {
printf("Olá, mundo!\n");
return 0;
}
$ gcc -std=c23 -Wall -Wextra e1.c -o e1
e1.c:1:10: fatal error: stdoi.h: Arquivo ou diretório inexistente
1 | #include <stdoi.h>
| ^~~~~~~~~
compilation terminated.
O que aconteceu: o pré-processador tentou colar o arquivo stdoi.h e não achou (as letras i e o trocaram de lugar). "Fatal" quer dizer que o gcc desistiu ali mesmo, sem nem olhar o resto do código. A parte "Arquivo ou diretório inexistente" veio em português porque é o sistema que a escreve; em fóruns, em inglês, ela aparece como No such file or directory. No editor do guia, o Clang diz fatal error: 'stdoi.h' file not found.
Como consertar: confira o nome do arquivo no #include: stdio.h (st-d-i-o, de standard input/output).
error: com linha e coluna
$ gcc -std=c23 -Wall -Wextra e2.c -o e2
e2.c: In function ‘main’:
e2.c:4:5: error: implicit declaration of function ‘pritnf’; did you mean ‘printf’? [-Wimplicit-function-declaration]
4 | pritnf("Olá, mundo!\n");
| ^~~~~~
| printf
O que aconteceu: a Ana escreveu pritnf. Essa função não foi declarada em lugar nenhum (o stdio.h declara printf, não pritnf), e o compilador reclama: "declaração implícita da função pritnf; você quis dizer printf?". As mensagens do compilador sempre têm o formato arquivo:linha:coluna: error:, aqui linha 4, coluna 5, e vêm em inglês.
Curiosidade: no C antigo, isso não era erro. O compilador "adivinhava" que pritnf era alguma função de outro lugar e deixava passar; o problema só aparecia na etapa 4. Com o padrão de 1989, dá para ver:
$ gcc -std=c89 e2.c -o e2 /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccGcXJoA.o: na função "main": e2.c:(.text+0x18): referência não definida para "pritnf" collect2: error: ld returned 1 exit status
Desde o C99 a declaração implícita não é mais permitida, e o gcc 14 passou a tratá-la como erro. É um bom exemplo de como as versões novas do C pegam erros mais cedo (aula Versões do C).
Como consertar: corrija o nome para printf. A aula Lendo Erros do Compilador ensina a ler todas as partes dessas mensagens.
referência não definida e collect2
#include <stdio.h>
int mian(void) {
printf("Olá, mundo!\n");
return 0;
}
$ gcc -std=c23 -Wall -Wextra e3.c -o e3 /usr/bin/x86_64-linux-gnu-ld.bfd: /usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/Scrt1.o: na função "_start": (.text+0x1b): referência não definida para "main" collect2: error: ld returned 1 exit status $ gcc -std=c23 -Wall -Wextra -c e3.c -o e3.o « só até a etapa 3: nenhum erro! $ echo $? 0
O que aconteceu: a Ana escreveu mian em vez de main. Para o compilador, tudo bem: mian é um nome válido de função, e a etapa 3 termina sem erro (o echo $? mostra 0). Mas todo programa em C precisa de uma função chamada main: é por ela que o sistema começa a rodar o programa. Na hora de montar o executável, o ligador procurou o main e não achou. As pistas de que o erro é do ligador: a mensagem começa com ld (o ligador), não tem número de linha do seu código, e termina com collect2: error: ld returned 1 exit status ("o ld terminou com o código 1"). O ligador fala português na máquina da Ana; em inglês, a frase é undefined reference to `main', a forma que você vai encontrar em fóruns.
Como consertar: corrija o nome para main. Esse mesmo erro aparece quando você chama uma função que foi declarada mas nunca escrita, ou quando esquece de compilar um dos arquivos do projeto (aula Vários Arquivos).
.c
$ ./ola.c bash: ./ola.c: Permissão negada $ echo $? 126
O que aconteceu: a Ana tentou rodar o código-fonte. O ola.c é texto, não um programa: o processador não sabe o que fazer com ele, e o sistema nem deixa tentar (ele não tem a permissão de "executável", o x que você viu no ls -l).
Como consertar: rode o executável que o gcc criou, o nome que veio depois do -o: ./ola. Se você não usou -o, o gcc chama o programa de a.out, e aí é ./a.out.
Agora é a sua vez. O programa abaixo tem um erro de cada etapa. O compilador só mostra um por vez: ele para no primeiro erro de etapa e não chega às seguintes.
Missão: rode, leia a mensagem, conserte, e rode de novo, até o programa escrever Olá, mundo! e Tchau!. Você vai encontrar, nesta ordem: um erro do pré-processador (fatal error), um do compilador (error com linha e coluna) e um do ligador (a palavra undefined, "não definido", com o nome de uma função). Dica para o último: o programa promete uma função despedir lá em cima, mas a que foi escrita lá embaixo tem outro nome. (Funções têm a sua própria aula, Funções; aqui basta achar a diferença.)
#include <stdoi.h>
void despedir(void);
int main(void) {
pritnf("Olá, mundo!\n");
despedir();
return 0;
}
void despedr(void) {
printf("Tchau!\n");
}
#include <stdio.h>
void despedir(void);
int main(void) {
printf("Olá, mundo!\n");
despedir();
return 0;
}
void despedir(void) {
printf("Tchau!\n");
}
No editor, a mensagem do terceiro erro vem do wasm-ld, o ligador do WebAssembly: undefined symbol: despedir ("símbolo não definido"), seguida de linker command failed ("o comando do ligador falhou"). No terminal da Ana, com o gcc, o mesmo erro sai assim:
$ gcc -std=c23 -Wall -Wextra tchau.c -o tchau /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccAZKudP.o: na função "main": tchau.c:(.text+0x18): referência não definida para "despedir" collect2: error: ld returned 1 exit status
Referência
| Comando | O que faz | Gera |
|---|---|---|
gcc -E ola.c -o ola.i | Só pré-processa (sem -o, mostra na tela) | .i (texto) |
gcc -S ola.c -o ola.s | Pré-processa e compila para assembly | .s (texto) |
gcc -c ola.c -o ola.o | Pré-processa, compila e monta; não liga | .o (objeto) |
gcc ola.o -o ola | Só liga (o .o já estava pronto) | executável |
gcc -std=c23 -Wall -Wextra ola.c -o ola | As quatro etapas de uma vez (o comando do guia) | executável |
gcc ola.c | As quatro etapas, sem escolher o nome | a.out |
gcc -save-temps ola.c -o ola | Tudo, guardando os arquivos intermediários | .i, .s, .o e executável |
gcc -v ola.c -o ola | Tudo, mostrando cada programa chamado | executável |
file arquivo | Diz o tipo do arquivo (texto, objeto, executável) | (nada) |
nm ola.o | Lista os símbolos: T = definido aqui, U = usado, mas está em outro lugar | (nada) |
ldd ola | Mostra as bibliotecas de que o executável precisa | (nada) |
| A mensagem tem… | Veio de | Exemplo |
|---|---|---|
fatal error: … .h e compilation terminated | Pré-processador | #include com nome errado |
arquivo.c:linha:coluna: error: | Compilador | Falta de ;, nome escrito errado |
ld, referência não definida (undefined reference), collect2 | Ligador | Falta o main, função declarada e nunca escrita |
| Nenhuma mensagem, mas o programa escreve algo errado ou "morre" | O programa, ao rodar | Conta errada, falha de segmentação |
Exercícios
1. Qual é a ordem das etapas da compilação?
2. O que o pré-processador faz com a linha #include <stdio.h>?
3. A mensagem termina com collect2: error: ld returned 1 exit status. De qual etapa veio o erro?
4. No nm ola.o, a linha U puts quer dizer:
5. A Ana mudou o texto do printf no ola.c, rodou ./ola e apareceu o texto antigo. Por quê?
6. Parar antes de ligar
Complete o comando para gerar o arquivo objeto ola.o, sem ligar:
7. Ver o assembly
Complete o comando para ver o assembly que o compilador gera:
8. Só o pré-processador
Complete o comando para ver o código depois do pré-processador, com o #define já trocado:
Resumo
- Compilar tem quatro etapas: pré-processador (cola os
#include, troca os#define, tira os comentários), compilador (confere as regras e traduz para assembly), montador (gera o código de máquina, o.o) e ligador (junta tudo com a biblioteca do C e gera o executável). - Para parar no meio:
-E(até o.i),-S(até o.s) e-c(até o.o). O-save-tempsguarda todos. - O
stdio.hsó declara oprintf; o código dele já vem compilado na biblioteca do C (alibc), e o ligador faz a ligação. - O executável roda sozinho, sem o
.ce sem ogcc; mas, se o código mudar, é preciso compilar de novo. - Cada etapa tem os seus erros:
fatal error … .hé do pré-processador;arquivo:linha:coluna: erroré do compilador;referência não definida/undefined referencecomcollect2é do ligador. - Próximo passo: conhecer as versões do C e saber qual o seu compilador usa, em Versões do C.