1 pontos por GN⁺ 2025-10-26 | Ainda não há comentários. | Compartilhar no WhatsApp
  • 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ção main definida 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
  • 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::Command do Rust chama execve internamente
    • Também realiza o processo de converter o nome do comando em um caminho completo, de forma semelhante à busca no PATH do shell
  • No caso de scripts com shebang (#!), o kernel executa o programa usando o interpretador especificado
    • Exemplo: #!/usr/bin/python3 → executado com o interpretador Python

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
  • No exemplo do cabeçalho ELF, trata-se de um executável ELF32 para a arquitetura RISC-V, com o endereço 0x10358 definido 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, malloc da libc
    • A seção PT_INTERP do ELF especifica o vinculador dinâmico (interpretador)
  • 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
  • Isso vem, em grande parte, da biblioteca padrão e do código de inicialização do runtime
    • Porque implementações de libc como musl ou glibc estão vinculadas
  • 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 argv e envp passados na chamada execve sã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_PAGESZ define 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, _start realiza a inicialização do runtime antes de chamar main
    • Exemplo: std::rt::lang_start no Rust, __libc_start_main em C
  • No exemplo em Rust, é possível definir _start diretamente sem runtime usando os atributos #![no_std] e #![no_main]
    • Dentro de _start, lê-se argc, argv e envp da pilha e chama-se o ponteiro para main
  • 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
    1. Chamada execve → o kernel carrega o arquivo ELF
    2. Interpretação do ELF → mapeamento das seções de código/dados, definição do interpretador
    3. Montagem da pilha → armazenamento de argumentos, variáveis de ambiente e vetor auxiliar
    4. Execução do ponto de entrada _start
    5. Inicialização do runtime e chamada de main()
  • 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

Ainda não há comentários.

Ainda não há comentários.