Seu Programa Não Começa na main
por Frank de Alcantara em 03/07/2026
Existe uma pequena mentira pedagógica que ensinamos, repetimos, colocamos em slide e depois fingimos que não fomos nós: “o programa começa na função main()”. Funciona bem na primeira aula. Evita trauma. Dá à aluna um lugar para colocar o primeiro printf, ou o primeiro std::cout, e seguir a vida.
Ah! Como é boa a vida simples!
O problema é que essa frase é falsa. Útil, mas falsa. Como quase toda simplificação didática que sobrevive tempo demais, ela passa de escada para muleta. Em algum momento, precisamos tirá-la da frente.
Resolvi escrever sobre isso agora por culpa. Acabo de ver um post, de um ex-aluno, repetindo isso como se fosse um dos mandamentos dados por Deus, ou uma verdade absoluta do universo. Doeu minha consciência. Ele não fez programação imperativa comigo, nem sistemas operacionais. Talvez, a culpa nem seja minha. Mas, senti um distúrbio na força.
Em C e C++, main() é o ponto de entrada do seu código de aplicação em um ambiente hospedado (hosted environment). Mas não é, em geral, o primeiro endereço executado pelo processo. Antes de main() existir como experiência concreta, o sistema operacional, o carregador do programa, o linker dinâmico e a biblioteca de runtime já fizeram um pequeno coluio técnico nos bastidores.
Quando você digita:
int main() {
return 0;
}
o compilador não olha para isso e diz: “perfeito, vamos começar exatamente aqui, no primeiro byte desta função”. Seria bonito. Também seria ingênuo. O mundo real raramente perde a oportunidade de colocar uma camada a mais entre você e a verdade. Dura verdade.
A Mentira Conveniente
Em uma aula introdutória, dizer que “o programa começa na main()” é aceitável porque, do ponto de vista do programador iniciante, é isso que importa. O primeiro código escrito por ele normalmente está ali. O fluxo lógico que ele controla começa ali. O depurador costuma parar ali. Os exercícios começam ali. Tem a maior cara de ponto inicial.
Mas essa frase esconde uma diferença importante:
main()é o início convencional da lógica da aplicação, não o início absoluto da execução do processo.
O processo já nasceu antes. O sistema operacional já criou um espaço de endereçamento virtual. O executável já foi mapeado na memória. A pilha inicial já foi preparada. Bibliotecas compartilhadas podem ter sido carregadas. Endereços podem ter sido resolvidos. Variáveis globais podem ter sido inicializadas. Em C++, construtores de objetos globais podem ter rodado.
Ou seja: quando main() começa, muita gente já mexeu nos móveis e a casa já está arrumada.
O Que o Compilador Realmente Entrega
Quando compilamos um programa em C ou C++, não estamos gerando apenas as instruções correspondentes às funções que escrevemos. O resultado final passa por um processo que envolve compilador, montador, linker, bibliotecas e arquivos auxiliares de inicialização.
Em sistemas Unix-like, aparecem nomes como crt1.o, crti.o, crtn.o e parentes próximos. O prefixo crt vem de C Runtime. Em Windows, encontramos nomes como mainCRTStartup, WinMainCRTStartup e outras variações, dependendo do subsistema, do compilador e da configuração.
Esses componentes fazem o trabalho pouco glamouroso que precisa acontecer antes da sua função ser chamada. E, como todo trabalho pouco glamouroso, ele só recebe atenção quando alguma coisa quebra.
A biblioteca de runtime prepara o ambiente esperado pela linguagem. Ela organiza argumentos de linha de comando, inicializa partes da biblioteca padrão, registra funções de finalização, prepara suporte a exceções, inicializa estruturas associadas a threads, executa construtores de objetos globais em C++ e, só então, chama a main().
Isso explica por que um programa mínimo em C ou C++ não é tão mínimo assim. Você escreveu três linhas, mas está pendurado em uma pequena infraestrutura. O compilador apenas teve a gentileza de não jogar tudo isso na sua cara logo no primeiro “Olá, mundo”. Pequenas misericórdias ainda existem. São poucas, bem disfarçadas, mas existem.
A Sequência Em Um Sistema Típico
Vamos pensar em um programa comum, compilado para Linux ou Windows, usando bibliotecas padrão e o runtime normal do compilador.
1. O Sistema Operacional Carrega o Executável
No Linux, o executável costuma estar no formato ELF (Executable and Linkable Format). No Windows, no formato PE (Portable Executable). O kernel lê metadados do arquivo, cria o processo, configura o espaço de endereçamento virtual e mapeia as regiões necessárias na memória.
Essas regiões incluem, por exemplo:
- segmento de código, normalmente somente leitura e executável;
- dados inicializados;
- dados zerados, como variáveis globais não inicializadas;
- pilha inicial;
- informações auxiliares usadas pelo runtime e pelo loader.
É comum dizer que o sistema operacional “cria a stack e o heap”. A pilha inicial, sim, entra como parte essencial da preparação do processo. O heap, porém, merece cuidado: em muitos sistemas modernos ele não aparece como uma grande gaveta pronta e cheia. O que existe é um espaço de endereçamento e mecanismos para solicitar memória conforme a execução avança, usando chamadas como brk, mmap, VirtualAlloc ou camadas equivalentes escondidas atrás de malloc, new e companhia.
Pensa em uma coisa complicada. Esse tal de heap.
A frase correta é menos simpática, mas mais verdadeira seria: o sistema operacional prepara o ambiente de memória do processo e a runtime passa a administrar alocações dinâmicas sobre os mecanismos fornecidos pelo sistema.
Imagine ouvir isso na primeira aula de programação imperativa.
2. O Loader e o Linker Dinâmico Entram em Cena
Se o programa usa bibliotecas compartilhadas, como .so no Linux ou .dll no Windows, alguém precisa carregá-las, mapear suas páginas na memória e resolver os símbolos necessários.
No Linux, quando o executável é dinamicamente ligado, o kernel pode transferir o controle inicialmente para o interpretador indicado no próprio ELF, normalmente algo como ld-linux. Esse linker dinâmico carrega dependências, resolve relocations e prepara o caminho para o código do executável.
No Windows, o loader do sistema mapeia o arquivo PE, carrega DLLs necessárias, aplica relocations quando necessário e chama rotinas de inicialização associadas ao carregamento de bibliotecas.
Esta etapa é uma daquelas partes da computação que parecem invisíveis até você errar uma versão de biblioteca, misturar ABI incompatível ou descobrir que “funciona na minha máquina” não é uma arquitetura de distribuição de software.
3. O Entry Point Real É Executado
O executável possui um endereço de entrada registrado em seus metadados. Esse endereço normalmente não aponta para main(). Ele aponta para uma rotina de inicialização fornecida pela runtime.
Em Linux com glibc, é comum vermos uma função chamada _start. Em Windows, nomes como mainCRTStartup ou WinMainCRTStartup aparecem com frequência. O nome exato depende do compilador, da biblioteca C, da forma de linkedição (essa saiu direto das vozes na minha cabeça) e do tipo de aplicação.
Essa função de entrada real não é decoração. Ela é a ponte entre o mundo cru do processo recém-criado e o mundo confortável em que main(int argc, char** argv) parece simplesmente existir.
Uma visão simplificada seria:
Sistema operacional
-> loader / linker dinâmico
-> entry point real (_start, mainCRTStartup, ...)
-> inicialização da runtime
-> main(argc, argv)
-> finalização da runtime
-> encerramento do processo
Não, eu não estou brincando, esta é uma versão simplificada. Porém, não é uma sequência universal, byte por byte, para todos os sistemas, compiladores e formatos. É uma boa representação mental para programas C/C++ comuns em ambientes modernos.
O Que Roda Antes da main()
Antes de main() ser chamada, a runtime precisa construir a ilusão de que o programa começa em um ambiente civilizado.
Entre as tarefas comuns estão:
- preparar
argc,argve, quando disponível, variáveis de ambiente; - inicializar estruturas internas da biblioteca C;
- configurar suporte a
stdin,stdoutestderr; - preparar mecanismos usados por
malloc,free,newedelete; - inicializar suporte a exceções e informações de desenrolamento de pilha;
- configurar dados locais de thread, quando aplicável;
- inicializar partes da biblioteca padrão de C++;
- executar construtores de objetos globais e estáticos com inicialização dinâmica;
- registrar funções que devem rodar no final, como as passadas para
atexit().
Aqui mora uma pegadinha importante: em C++, existe código do programador que pode rodar antes de main(). Construtores de objetos globais são o exemplo clássico. A frase “seu código começa na main()” fica, portanto, tecnicamente suspeita. Parte do seu código pode ter sido executada antes, talvez com efeitos colaterais, talvez abrindo arquivos, talvez fazendo logs, talvez criando aquele bug que só aparece na máquina do cliente às 17h58 de uma sexta-feira. O universo tem senso de oportunidade e nenhuma piedade.
Repete comigo três vezes: Nunca faça deploy na sexta-feira.
Um Exemplo Que Entrega o Truque
O exemplo abaixo é simples e suficiente para desmontar a versão inocente da história.
#include <iostream>
class MinhaClasse {
public:
MinhaClasse() {
std::cout << "Construtor de objeto global rodou antes de main()\n";
}
~MinhaClasse() {
std::cout << "Destrutor de objeto global rodou depois de main()\n";
}
};
MinhaClasse objeto_global;
int main() {
std::cout << "Agora estamos dentro da main()\n";
return 0;
}
Testei apenas no Visual Studio Community Edition, o código roda e a saída esperada é:
Construtor de objeto global rodou antes de main()
Agora estamos dentro da main()
Destrutor de objeto global rodou depois de main()
O construtor rodou antes de main(). O destrutor rodou depois. O caixa abriu, fechou, e havia gente trabalhando antes e depois dele.
Quem acha que o supermercado começa quando o caixa sorri para a banana está confundindo experiência de compra com cadeia de abastecimento.
Esta analogia é infantil, mas funciona. main() é o caixa. O programa completo envolve fazenda, caminhão, depósito, nota fiscal, prateleira, balança, gerente e uma quantidade surpreendente de burocracia operacional. A banana não nasceu na gondola.
O Que Acontece Depois da main()
Outra parte frequentemente esquecida é o fim do programa. Quando main() retorna, o processo nem sempre evapora no ar. Existe um processo de finalização organizada.
Em um programa C++ típico, depois que main() retorna:
- o valor de retorno de
main()é tratado como código de saída; - funções registradas com
atexit()são chamadas; - destrutores de objetos estáticos e globais são executados;
- buffers de saída podem ser descarregados;
- recursos internos da runtime são finalizados;
- o sistema operacional recebe a solicitação de encerramento do processo.
Em C++, isso é particularmente relevante porque destrutores têm semântica. Se um objeto global abriu um arquivo, manteve um log, segurou um recurso externo ou representou um estado importante, sua finalização acontece fora da main().
Naturalmente, há formas de escapar desse caminho. Chamadas como _Exit, std::quick_exit, abort ou encerramentos abruptos podem pular partes da limpeza normal. O mundo C/C++ oferece várias maneiras de sair pela janela. Algumas são úteis. Outras são apenas convites para depuração suicida.
Como Ver o Entry Point
Se você quiser verificar que main() não é o ponto de entrada real, pode inspecionar o executável.
Em Linux, depois de compilar:
g++ exemplo.cpp -o exemplo
readelf -h ./exemplo
Você verá um campo parecido com:
Entry point address: 0x...
Esse endereço é o ponto inicial registrado no ELF. Para investigar o símbolo correspondente, ferramentas como objdump, nm e readelf -s ajudam:
objdump -d ./exemplo | less
nm -n ./exemplo | head
Em muitos casos, você encontrará _start antes de chegar em main.
No Windows, ferramentas como dumpbin ou utilitários equivalentes permitem examinar o cabeçalho PE:
dumpbin /headers exemplo.exe
O executável terá um endereço de entrada, e esse endereço normalmente levará para a rotina de startup da CRT, não diretamente para main.
Linux, Windows e a Diferença Entre Ideia e Implementação
É tentador decorar uma sequência rígida:
- kernel carrega;
- linker dinâmico resolve;
_startroda;- runtime chama
main; - fim.
Como modelo mental, ótimo. Como descrição universal, perigoso.
No Linux, a história depende de o programa ser estático ou dinâmico, da libc usada, das opções de linkedição e da arquitetura. glibc, musl e outras bibliotecas não precisam organizar tudo exatamente da mesma forma, embora a ideia geral seja parecida.
No Windows, a entrada também varia conforme o tipo de aplicação. Programas de console costumam usar main ou wmain; aplicações gráficas clássicas podem usar WinMain ou wWinMain; por baixo disso, a CRT ainda precisa fazer a adaptação entre o ponto de entrada do executável e a função esperada pelo programador.
Em ambos os mundos, a mensagem central permanece:
A função que você escreve geralmente não é a primeira função executada.
main() é uma convenção da linguagem e da runtime em ambiente hospedado. O processo, porém, começa em um endereço de entrada definido pelo formato executável e pelo sistema.
Quando Isso Não Se Aplica
Existem ambientes em que a história muda bastante.
Em sistemas embarcados, firmware, bootloaders e kernels, podemos estar em um ambiente freestanding. Nesse caso, a linguagem não promete a mesma infraestrutura de um sistema operacional completo. Pode não existir argc, argv, stdin, heap convencional, arquivos, processos ou runtime pronta.
Em um microcontrolador, o ponto de entrada real pode estar em uma tabela de vetores. O processador reinicia, carrega o ponteiro de pilha inicial a partir de um endereço fixo, salta para um Reset_Handler, copia dados inicializados da flash para a RAM, zera a seção .bss, configura clock, talvez inicialize partes mínimas da runtime e só então chama main(). Ou nem chama. Depende do projeto.
Em Assembly puro, você pode definir o símbolo de entrada diretamente. No Linux, por exemplo, um programa mínimo pode declarar _start e fazer uma chamada de sistema para sair, sem passar pela libc:
global _start
section .text
_start:
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status 0
syscall
Aqui não há main() porque ninguém pediu uma. O sistema operacional entra no endereço definido pelo executável, e dali em diante a responsabilidade é sua. Liberdade total, inclusive para errar sem rede de proteção. Um clássico.
Também é possível, em C/C++, usar opções como -nostartfiles ou scripts de linker personalizados para substituir a rotina de inicialização padrão. Isso aparece em kernels, runtimes próprios, sistemas embarcados e experimentos educacionais. É excelente para aprender. Também é excelente para descobrir quantas coisas a runtime fazia por você enquanto você reclamava dela.
Por Que Isso Importa
À primeira vista, isso parece curiosidade de quem gosta de desmontar relógio desde pequenininho. Mas entender o que acontece antes e depois de main() tem consequências práticas.
Ajuda a entender erros de inicialização estática em C++. Ajuda a depurar problemas com bibliotecas compartilhadas. Ajuda a compreender por que um programa falha antes de chegar no primeiro breakpoint dentro de main(). Ajuda a escrever firmware. Ajuda a entender ABI, linking, loaders, símbolos, relocations e chamadas de sistema.
Ajuda, principalmente, a perceber que “compilar” não é transformar magicamente texto em programa, mas encaixar várias peças em um contrato executável.
Também ajuda a manter uma humildade saudável. Nem muito para não parecer provocação, em pouca para não parecer fingimento. Compiladores farejam seu medo.
A função main() parece soberana porque aparece no livro, no curso e no exercício. Mas ela é mais parecida com um gerente de turno: importante, visível, necessário, mas cercado por gente que chegou antes, preparou o ambiente e ainda ficará depois para apagar a luz.
Então, sim: para ensinar programação, podemos dizer que o programa começa em main(). É uma aproximação aceitável.
Tecnicamente, um programa C/C++ típico começa antes. O sistema operacional cria o processo, o loader prepara o executável, a runtime inicializa o ambiente, construtores globais podem rodar, e só então a sua main() é chamada. Quando ela termina, ainda há finalização, destrutores, atexit() e limpeza.
A main() é o começo da narrativa que o programador iniciante consegue ver. Não é o começo do processo. Esta distinção separa quem apenas escreve código de quem começa a entender o que o computador está fazendo quando finge obedecer.
(Updated: )