A jornada antes da execução da função main()
(amit.prasad.me)- Antes de o programa ser executado, uma análise técnica explora o processo pelo qual o kernel cria e inicializa um processo por meio da chamada de sistema
execve - Essa chamada passa o caminho do executável, os argumentos e as variáveis de ambiente, e com base nisso o kernel carrega o executável no formato ELF
- O arquivo ELF inclui código, dados, símbolos e informações de ligação dinâmica, e o kernel o interpreta para realizar o mapeamento de memória e a inicialização da pilha
- Em seguida, o kernel transfere o controle para o ponto de entrada
_start, e só depois da inicialização do runtime específico da linguagem a funçãomaindefinida pelo usuário é chamada - Esse processo mostra a estrutura de colaboração entre sistema operacional, compilador e runtime e é importante para entender como a execução de programas acontece em nível de sistema
O ponto de partida da execução de programas: chamada execve
- No Linux, a execução de programas começa com a chamada de sistema
execve- No formato
execve(const char *filename, char *const argv[], char *const envp[]), ela recebe o nome do executável, a lista de argumentos e a lista de variáveis de ambiente - Com isso, o kernel determina qual programa será executado e em que ambiente
- No formato
- Em linguagens de alto nível, essa chamada costuma estar encapsulada pela API de execução de processos da biblioteca padrão
- Exemplo:
std::process::Commanddo Rust chamaexecveinternamente - Também realiza o processo de converter o nome do comando em um caminho completo, de forma semelhante à busca no PATH do shell
- Exemplo:
- No caso de scripts com shebang (
#!), o kernel executa o programa usando o interpretador especificado- Exemplo:
#!/usr/bin/python3→ executado com o interpretador Python
- Exemplo:
ELF: a estrutura do executável
- No Linux, os executáveis seguem o formato ELF (Executable and Linkable Format)
- ELF é um formato padrão de arquivo executável que inclui código, dados, símbolos e informações de relocação
- Outros sistemas operacionais usam formatos diferentes, como Mach-O (macOS) e PE (Windows)
- O cabeçalho ELF contém informações sobre a estrutura do arquivo e o layout em memória
- Exemplos de campos:
ELF Magic,Class,Entry point address,Program headers,Section headers Entry point addressé o endereço da primeira instrução a ser executada pelo programa
- Exemplos de campos:
- No exemplo do cabeçalho ELF, trata-se de um executável ELF32 para a arquitetura RISC-V, com o endereço
0x10358definido como ponto de entrada
Componentes internos do ELF
- Um arquivo ELF é composto por várias seções (section)
.text: código executável.data: variáveis globais inicializadas.bss: variáveis globais não inicializadas.plt: tabela para chamadas de bibliotecas compartilhadas.symtab,.strtab: tabelas de símbolos e strings
- A PLT (Procedure Linkage Table) dá suporte a chamadas de funções de bibliotecas compartilhadas
- Exemplo:
printf,mallocdalibc - A seção
PT_INTERPdo ELF especifica o vinculador dinâmico (interpretador)
- Exemplo:
- O kernel lê o ELF, posiciona em memória as seções carregáveis e, quando necessário, aplica recursos de segurança como ASLR e bit NX
Tabela de símbolos e ligação em runtime
- A tabela de símbolos (
symtab) do ELF contém informações de endereço de funções e variáveis- Exemplo: há entradas como
_start,main,__libc_start_main - Até um programa simples de “Hello, World!” pode conter mais de 2.300 símbolos
- Exemplo: há entradas como
- Isso vem, em grande parte, da biblioteca padrão e do código de inicialização do runtime
- Porque implementações de
libccomomuslouglibcestão vinculadas
- Porque implementações de
- Depois de carregar cada seção do ELF, o kernel transfere o controle para o interpretador (vinculador dinâmico)
- O interpretador cuida de relocação, randomização de endereços (ASLR), definição de permissões de execução (bit NX) etc.
Processo de inicialização da pilha
- Antes de executar o programa, o kernel precisa montar diretamente a pilha (stack)
- A pilha é usada para variáveis locais, frames de chamada de função, passagem de argumentos etc.
- Os
argveenvppassados na chamadaexecvesão armazenados na pilha- O programa acessa por eles os argumentos de linha de comando e as variáveis de ambiente
- O kernel também inclui na pilha o vetor auxiliar ELF (
auxv)- Ele contém cerca de 30 itens, como tamanho de página, metadados ELF e informações do sistema
- Exemplo:
AT_PAGESZdefine o tamanho da página de memória (por exemplo, 4KiB)
- No exemplo do emulador RISC-V, o ponteiro da pilha (
sp) começa em um endereço alto, e os argumentos, variáveis de ambiente e vetor auxiliar são empilhados em ordem inversa
Ponto de entrada e função _start
- O ponto de entrada do ELF é definido como o endereço da função
_start_starté o primeiro código em espaço de usuário para o qual o kernel transfere o controle
- Na maioria das linguagens,
_startrealiza a inicialização do runtime antes de chamarmain- Exemplo:
std::rt::lang_startno Rust,__libc_start_mainem C
- Exemplo:
- No exemplo em Rust, é possível definir
_startdiretamente sem runtime usando os atributos#![no_std]e#![no_main]- Dentro de
_start, lê-seargc,argveenvpda pilha e chama-se o ponteiro paramain
- Dentro de
- O runtime de cada linguagem executa tarefas de inicialização específicas da linguagem, como construtores globais, armazenamento local de thread e tratamento de exceções
Fluxo completo até a chamada de main()
- O processo completo pode ser resumido assim
- Chamada
execve→ o kernel carrega o arquivo ELF - Interpretação do ELF → mapeamento das seções de código/dados, definição do interpretador
- Montagem da pilha → armazenamento de argumentos, variáveis de ambiente e vetor auxiliar
- Execução do ponto de entrada
_start - Inicialização do runtime e chamada de
main()
- Chamada
- Essa sequência mostra a estrutura de cooperação entre o kernel do sistema operacional, o formato ELF e o runtime da linguagem
- O kernel Linux real inclui também lógicas internas adicionais, como espaço de endereçamento, tabela de processos e gerenciamento de grupos, mas este texto explica o fluxo essencial da etapa anterior
Conclusão e correção
- O processo de execução antes de
main()é uma combinação de inicialização em nível de kernel e configuração de runtime - Mesmo um programa simples de “Hello, World!” é executado somente depois de passar por uma estrutura ELF complexa e pela inicialização do runtime
- Na versão inicial do texto, parte da lógica de carregamento de seções havia sido atribuída ao kernel, mas foi corrigido que isso na verdade é papel do interpretador ELF
- Esta análise serve como material básico útil para compreender programação de sistemas, compiladores e arquitetura de sistemas operacionais
1 comentários
Comentários do Hacker News
Explica o processo de linkedição dinâmica de arquivos ELF
O kernel mapeia os segmentos PT_LOAD do ELF, carrega o linker dinâmico (
ld.so) indicado por PT_INTERP e então transfere o controle para eleDepois disso, o linker dinâmico faz sua própria relocação e carrega os objetos compartilhados necessários com
mmap/mprotectA estrutura é comparada ao mecanismo de shebang (
#!) de scriptsCompartilha uma experiência antiga em que tentou inserir um arquivo arbitrário em um ELF com
objcopye ficou confuso porque o kernel não o carregavaNo fim, acabou criando sua própria ferramenta de patch da tabela de cabeçalhos de programa, e diz que essa funcionalidade também foi adicionada ao linker mold
Artigo relacionado: Self-contained Lone Lisp Applications
Diz que fez experimentos empacotando todo o código antes de
main()ou até semmain()Artigo relacionado: Packing a codebase into a single function
Brinca dizendo que bastaria transformar todas as funções em algo como
main(100+n, ...)Diz que, se alguém tiver interesse no tema, pode consultar o cpu.land que ele criou
Em vez de focar no layout de memória, o site trata de multitarefa e do processo de carregamento de código
Diz que tem curiosidade sobre quantos projetos em C evitam a biblioteca padrão e fazem chamadas diretas apenas a syscalls do Linux
Acha esse estilo de programação bem mais divertido
Recursos como ALSA e DRM trazem muitas vantagens quando acessados por bibliotecas de sistema, em vez de syscalls do kernel
Explica que essa abordagem é melhor em termos de portabilidade e manutenção do que a abordagem no estilo Windows
Hoje abandonou isso porque os headers
nolibcdo Linux já estão muito bons,mas atualmente está desenvolvendo uma linguagem interpretada Lisp baseada em syscalls
Diz que foi uma jornada muito interessante experimentar montar diretamente um user space Linux por meio de chamadas de sistema
Explica que o interpretador ELF (
ld.so) assume todo o carregamento depois de mapear os segmentos ELF iniciaisO
execvemapeia os segmentos PT_LOAD, preenche o vetor auxiliar na pilha e entãosalta para o ponto de entrada do interpretador ELF
O kernel não sabe nada sobre PLT/GOT
Como alguém que ensina esse tema na universidade, diz que os alunos ficam confusos por causa dos diagramas de memória
Os livros desenham os endereços mais altos na parte de cima, mas um processo Linux real
é exibido com os endereços baixos em cima e os altos embaixo
Ao olhar
/proc/<pid>/maps, quanto mais se rola para baixo, maiores ficam os endereçosOu seja, a expressão “o heap cresce para cima (e a stack cresce para baixo)” fala apenas da direção numérica,
enquanto visualmente é justamente o contrário
Propõe que seria muito mais intuitivo desenhar como em uma IDE, com os endereços aumentando para baixo
Mas sugere que a visualização em orientação horizontal seria mais natural
Diz que gosta de fazer esse tipo de experimento com microcontroladores PIC16 antigos
Acha divertido lidar diretamente com ponteiro de pilha, temporizadores, configuração de variáveis e afins
Compartilha uma experiência relacionada a shebang (
#!)Uma aplicação Java mostrava um erro dizendo que não conseguia encontrar o script de execução,
mas o problema real era que o caminho do shebang no script estava errado
Localmente funcionava bem, mas o problema apareceu porque o caminho do interpretador era diferente no servidor remoto
Aconselha executar com
stracepara ver imediatamente em qual syscall o erro aconteceuCONFIG_BINFMT_SCRIPT=yDiz que, durante depuração, sempre fica confuso sobre em que momento se aplica a ordem de relocação do binário principal
Descreve como uma espécie de magia negra entender se isso acontece antes ou depois de o linker resolver seus próprios símbolos
Aponta que o link da parte “lang_start function (defined here)” no Markdown está quebrado