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.
Dois tipos de header
Seção intitulada “Dois tipos de header”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.
Program headers: o que mapear
Seção intitulada “Program headers: o que mapear”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.
O ponto de entrada
Seção intitulada “O ponto de entrada”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.
Referências
Seção intitulada “Referências”- ^ System V Application Binary Interface: a especificação que define o formato de arquivo ELF e a estrutura de program header descritos acima.
Ver também
Seção intitulada “Ver também”- 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.