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.

Você vai aprender
  • O caminho completo: pré-processador → compilador → montador → ligador.
  • Como parar o gcc no meio do caminho para ver cada etapa: -E, -S e -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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

EtapaFerramentaRecebeProduzPara parar aqui
1. JuntarPré-processadorola.c (o seu código)ola.i: o código com os #include já coladosgcc -E
2. TraduzirCompilador (propriamente dito)o código pré-processadoola.s: assembly, as instruções do processador escritas em textogcc -S
3. MontarMontador (assembler)ola.sola.o: arquivo objeto, código de máquina com "espaços em branco"gcc -c
4. LigarLigador (linker)ola.o + a biblioteca padrão do Cola: 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 \n do fim sumiu. Já vamos ver por quê.
call puts@PLT
"Chame a função puts". Mas você escreveu printf! O compilador percebeu que você estava imprimindo um texto fixo, terminado em \n, e trocou pela função puts (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 o main.

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
ArquivoTamanhoÉ texto?O que tem dentro
ola.c83 bytesSimO que você escreveu.
ola.i21.430 bytesSimO seu código com o stdio.h (e o que ele inclui) colado em cima.
ola.s664 bytesSimAs instruções do processador em texto (assembly).
ola.o1.496 bytesNãoCódigo de máquina, com um buraco para o puts.
ola15.952 bytesNãoO 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 rodargcc ola.c -o ola uma vez, depois ./ola quantas vezes quiserpython3 ola.py, sempre
Quem lê o código-fonteSó o compilador, na hora de compilarO interpretador, a cada execução
O que você entrega para outra pessoaO executável (ela não precisa do compilador nem do código)O código-fonte (ela precisa do Python instalado)
Erros de digitaçãoAparecem antes de rodar: sem compilar, não há programaMuitos 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.

Etapa 1, pré-processador: 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).

Etapa 2, compilador: 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.

Etapa 4, ligador: 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).

"Permissão negada" ao rodar o .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

ComandoO que fazGera
gcc -E ola.c -o ola.iSó pré-processa (sem -o, mostra na tela).i (texto)
gcc -S ola.c -o ola.sPré-processa e compila para assembly.s (texto)
gcc -c ola.c -o ola.oPré-processa, compila e monta; não liga.o (objeto)
gcc ola.o -o olaSó liga (o .o já estava pronto)executável
gcc -std=c23 -Wall -Wextra ola.c -o olaAs quatro etapas de uma vez (o comando do guia)executável
gcc ola.cAs quatro etapas, sem escolher o nomea.out
gcc -save-temps ola.c -o olaTudo, guardando os arquivos intermediários.i, .s, .o e executável
gcc -v ola.c -o olaTudo, mostrando cada programa chamadoexecutável
file arquivoDiz o tipo do arquivo (texto, objeto, executável)(nada)
nm ola.oLista os símbolos: T = definido aqui, U = usado, mas está em outro lugar(nada)
ldd olaMostra as bibliotecas de que o executável precisa(nada)
A mensagem tem…Veio deExemplo
fatal error: … .h e compilation terminatedPré-processador#include com nome errado
arquivo.c:linha:coluna: error:CompiladorFalta de ;, nome escrito errado
ld, referência não definida (undefined reference), collect2LigadorFalta o main, função declarada e nunca escrita
Nenhuma mensagem, mas o programa escreve algo errado ou "morre"O programa, ao rodarConta 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:

gcc ola.c -o ola.o

7. Ver o assembly

Complete o comando para ver o assembly que o compilador gera:

gcc ola.c -o ola.s

8. Só o pré-processador

Complete o comando para ver o código depois do pré-processador, com o #define já trocado:

gcc saudacao.c

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-temps guarda todos.
  • O stdio.h só declara o printf; o código dele já vem compilado na biblioteca do C (a libc), e o ligador faz a ligação.
  • O executável roda sozinho, sem o .c e sem o gcc; 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 reference com collect2 é do ligador.
  • Próximo passo: conhecer as versões do C e saber qual o seu compilador usa, em Versões do C.