Pular para o conteúdo

Carregamento de ELF

ELF (Executable and Linkable Format) é o formato binário que praticamente todo sistema operacional tipo Unix usa para executáveis e bibliotecas compartilhadas. Carregar um arquivo ELF significa analisar um conjunto fixo de headers e mapear os segmentos de memória que descrevem, antes de transferir o controle para o ponto de entrada que esses headers nomeiam.

Um arquivo ELF abre com um file header: um número mágico (0x7F, depois E, L, F), um byte de classe marcando 32 ou 64 bits, um endereço de ponto de entrada, e ponteiros para duas tabelas mais adiante no arquivo. A program header table descreve como carregar o arquivo na memória. A section header table descreve como o arquivo é organizado para linkagem e depuração. Um loader só precisa da primeira. Section headers importam para um linker, não para o que quer que coloque um programa na CPU.

Cada entrada de program header do tipo PT_LOAD nomeia um trecho do arquivo, seu tamanho, o endereço virtual a que pertence (aleatorizado por ASLR quando habilitado), e flags de permissão (legível, gravável, executável, em alguma combinação). O trabalho de um loader é mecânico uma vez que os headers são analisados. Para cada entrada PT_LOAD: alocar páginas cobrindo aquela faixa virtual, copiar os bytes correspondentes do arquivo, zerar qualquer coisa além do próprio comprimento do arquivo até o tamanho de memória declarado do segmento, e definir permissões de página correspondentes às flags.

for (int i = 0; i < header.e_phnum; i++) {
Elf64_Phdr *ph = &program_headers[i];
if (ph->p_type != PT_LOAD) continue;
void *dest = map_pages(ph->p_vaddr, ph->p_memsz, ph->p_flags);
memcpy(dest, file_data + ph->p_offset, ph->p_filesz);
memset((char *)dest + ph->p_filesz, 0, ph->p_memsz - ph->p_filesz);
}

O tamanho de arquivo e o tamanho de memória de um segmento nem sempre são iguais. A diferença, quando existe, corresponde a variáveis globais não inicializadas: declaradas no código-fonte, presentes no espaço de endereçamento, ausentes do arquivo em disco, já que uma variável cujo valor inicial é zero não exige dado algum armazenado.

Dois segmentos PT_LOAD copiados de seus offsets de arquivo para seus endereços virtuais, com o segmento de dados estendendo seu memsz além do filesz numa região BSS zeradaArquivo ELF (disco)Espaço de endereçamento virtualheaders.text.data.textR-X, p_filesz = p_memsz.data — copiado (p_filesz)zerado (BSS)p_memsz − p_filesz, ausente do arquivo

O ponto de entrada do file header é um endereço virtual, não um offset de arquivo, e só é válido uma vez que todo segmento PT_LOAD tenha sido mapeado. Executá-lo antes disso transfere o controle para o que quer que anteriormente ocupasse aquele endereço.

Um processo carregado dessa forma consegue executar seu próprio código e acessar seus próprios dados, mas não tem capacidade alguma além disso. Não consegue chamar uma função de biblioteca C, já que nenhuma está linkada, e não consegue fazer uma chamada de sistema sem uma stack já estabelecida para esse propósito. Um binário linkado dinamicamente exige adicionalmente que seu interpretador, nomeado em uma entrada de program header PT_INTERP, seja carregado e executado antes que o próprio ponto de entrada do binário seja alcançado. Um binário linkado estaticamente sem dependências externas não tem exigência alguma dessas: apenas seus segmentos PT_LOAD precisam ser mapeados antes que seu ponto de entrada possa ser invocado.

  1. ^ System V Application Binary Interface: a especificação que define o formato de arquivo ELF e a estrutura de program header descritos acima.
  • Multitasking: de onde vem um Process Control Block para o processo carregado.
  • System Calls: o que um processo carregado precisa antes de poder pedir qualquer coisa ao kernel.
  • Mitigações de Segurança: ASLR, a técnica que aleatoriza onde os segmentos PT_LOAD acima acabam mapeados.