O programa anterior tem 9 cabeçalhos de programa, então, o mapeamento de segmentos indica em qual cabeçalho de programa (de 00 a 08) cada seção está localizada.
PHDR - Cabeçalho do Programa
Contém as tabelas de cabeçalho do programa e os metadados em si.
INTERP
Indica o caminho do carregador a ser usado para carregar o binário na memória.
LOAD
Esses cabeçalhos são usados para indicar como carregar um binário na memória.
Cada cabeçalho LOAD indica uma região de memória (tamanho, permissões e alinhamento) e indica os bytes do binário ELF a serem copiados lá.
Por exemplo, o segundo tem um tamanho de 0x1190, deve estar localizado em 0x1fc48 com permissões de leitura e escrita e será preenchido com 0x528 a partir do deslocamento 0xfc48 (não preenche todo o espaço reservado). Esta memória conterá as seções .init_array .fini_array .dynamic .got .data .bss.
DYNAMIC
Este cabeçalho ajuda a vincular programas às suas dependências de biblioteca e aplicar realocações. Verifique a seção .dynamic.
NOTE
Isso armazena informações de metadados do fornecedor sobre o binário.
GNU_EH_FRAME
Define a localização das tabelas de desempilhamento de pilha, usadas por depuradores e funções de tempo de execução de tratamento de exceções em C++.
GNU_STACK
Contém a configuração da defesa de prevenção de execução de pilha. Se habilitado, o binário não poderá executar código da pilha.
GNU_RELRO
Indica a configuração RELRO (Relocation Read-Only) do binário. Esta proteção marcará como somente leitura certas seções da memória (como o GOT ou as tabelas init e fini) após o programa ter sido carregado e antes de começar a ser executado.
No exemplo anterior, está copiando 0x3b8 bytes para 0x1fc48 como somente leitura, afetando as seções .init_array .fini_array .dynamic .got .data .bss.
Note que RELRO pode ser parcial ou total, a versão parcial não protege a seção .plt.got, que é usada para lazy binding e precisa desse espaço de memória para ter permissões de escrita para escrever o endereço das bibliotecas na primeira vez que sua localização é pesquisada.
TLS
Define uma tabela de entradas TLS, que armazena informações sobre variáveis locais de thread.
Cabeçalhos de Seção
Os cabeçalhos de seção fornecem uma visão mais detalhada do binário ELF.
It also indicates the location, offset, permissions but also the tipo de dado it section has.
Meta Sections
String table: Contém todas as strings necessárias pelo arquivo ELF (mas não as realmente usadas pelo programa). Por exemplo, contém nomes de seções como .text ou .data. E se .text está no offset 45 na tabela de strings, usará o número 45 no campo name.
Para encontrar onde está a tabela de strings, o ELF contém um ponteiro para a tabela de strings.
Symbol table: Contém informações sobre os símbolos, como o nome (offset na tabela de strings), endereço, tamanho e mais metadados sobre o símbolo.
Main Sections
.text: As instruções do programa a serem executadas.
.data: Variáveis globais com um valor definido no programa.
.bss: Variáveis globais deixadas não inicializadas (ou inicializadas para zero). Variáveis aqui são automaticamente inicializadas para zero, evitando assim que zeros desnecessários sejam adicionados ao binário.
.rodata: Variáveis globais constantes (seção somente leitura).
.tdata e .tbss: Como .data e .bss quando variáveis locais de thread são usadas (__thread_local em C++ ou __thread em C).
.dynamic: Veja abaixo.
Symbols
Symbols é uma localização nomeada no programa que pode ser uma função, um objeto de dados global, variáveis locais de thread...
readelf -s lnstat
Symbol table '.dynsym' contains 49 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000001088 0 SECTION LOCAL DEFAULT 12 .init
2: 0000000000020000 0 SECTION LOCAL DEFAULT 23 .data
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strtok@GLIBC_2.17 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND s[...]@GLIBC_2.17 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.17 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fputs@GLIBC_2.17 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.17 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _[...]@GLIBC_2.34 (3)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.17 (2)
10: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT[...]
11: 0000000000000000 0 FUNC WEAK DEFAULT UND _[...]@GLIBC_2.17 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND putc@GLIBC_2.17 (2)
[...]
Cada entrada de símbolo contém:
Nome
Atributos de vinculação (fraco, local ou global): Um símbolo local só pode ser acessado pelo próprio programa, enquanto os símbolos globais são compartilhados fora do programa. Um objeto fraco é, por exemplo, uma função que pode ser substituída por outra diferente.
Tipo: NOTYPE (nenhum tipo especificado), OBJECT (variável de dados global), FUNC (função), SECTION (seção), FILE (arquivo de código-fonte para depuradores), TLS (variável local de thread), GNU_IFUNC (função indireta para realocação)
O diretório NEEDED indica que o programa precisa carregar a biblioteca mencionada para continuar. O diretório NEEDED é completado uma vez que a biblioteca compartilhada está totalmente operacional e pronta para uso.
Relocações
O carregador também deve realocar dependências após tê-las carregado. Essas realocações são indicadas na tabela de realocação nos formatos REL ou RELA e o número de realocações é dado nas seções dinâmicas RELSZ ou RELASZ.
Se o programa for carregado em um lugar diferente do endereço preferido (geralmente 0x400000) porque o endereço já está em uso ou por causa de ASLR ou qualquer outro motivo, uma relocação estática corrige ponteiros que tinham valores esperando que o binário fosse carregado no endereço preferido.
Por exemplo, qualquer seção do tipo R_AARCH64_RELATIV deve ter modificado o endereço no viés de relocação mais o valor do adendo.
Relocações Dinâmicas e GOT
A relocação também pode referenciar um símbolo externo (como uma função de uma dependência). Como a função malloc da libC. Então, o carregador ao carregar a libC em um endereço verificando onde a função malloc está carregada, escreverá esse endereço na tabela GOT (Global Offset Table) (indicado na tabela de relocação) onde o endereço de malloc deve ser especificado.
Tabela de Ligação de Procedimentos
A seção PLT permite realizar vinculação preguiçosa, o que significa que a resolução da localização de uma função será realizada na primeira vez que for acessada.
Assim, quando um programa chama malloc, ele na verdade chama a localização correspondente de malloc no PLT (malloc@plt). Na primeira vez que é chamado, resolve o endereço de malloc e o armazena, então na próxima vez que malloc for chamado, esse endereço é usado em vez do código PLT.
Inicialização do Programa
Depois que o programa foi carregado, é hora de ele ser executado. No entanto, o primeiro código que é executado não é sempre a função main. Isso ocorre porque, por exemplo, em C++ se uma variável global é um objeto de uma classe, esse objeto deve ser inicializadoantes que a main seja executada, como em:
Note que essas variáveis globais estão localizadas em .data ou .bss, mas nas listas __CTOR_LIST__ e __DTOR_LIST__ os objetos a serem inicializados e destruídos são armazenados para manter o controle deles.
A partir do código C, é possível obter o mesmo resultado usando as extensões GNU:
__attributte__((constructor)) //Add a constructor to execute before__attributte__((destructor)) //Add to the destructor list
De uma perspectiva de compilador, para executar essas ações antes e depois da função main, é possível criar uma função init e uma função fini que seriam referenciadas na seção dinâmica como INIT e FIN. e são colocadas nas seções init e fini do ELF.
A outra opção, como mencionado, é referenciar as listas __CTOR_LIST__ e __DTOR_LIST__ nas entradas INIT_ARRAY e FINI_ARRAY na seção dinâmica e o comprimento dessas é indicado por INIT_ARRAYSZ e FINI_ARRAYSZ. Cada entrada é um ponteiro de função que será chamado sem argumentos.
Além disso, também é possível ter um PREINIT_ARRAY com ponteiros que serão executados antes dos ponteiros INIT_ARRAY.
Ordem de Inicialização
O programa é carregado na memória, variáveis globais estáticas são inicializadas em .data e as não inicializadas são zeradas em .bss.
Todas as dependências para o programa ou bibliotecas são inicializadas e o linking dinâmico é executado.
Funções PREINIT_ARRAY são executadas.
Funções INIT_ARRAY são executadas.
Se houver uma entrada INIT, ela é chamada.
Se for uma biblioteca, dlopen termina aqui; se for um programa, é hora de chamar o ponto de entrada real (função main).
Armazenamento Local de Thread (TLS)
Eles são definidos usando a palavra-chave __thread_local em C++ ou a extensão GNU __thread.
Cada thread manterá uma localização única para essa variável, de modo que apenas a thread pode acessar sua variável.
Quando isso é usado, as seções .tdata e .tbss são usadas no ELF. Que são como .data (inicializado) e .bss (não inicializado), mas para TLS.
Cada variável terá uma entrada no cabeçalho TLS especificando o tamanho e o deslocamento TLS, que é o deslocamento que será usado na área de dados local da thread.
O __TLS_MODULE_BASE é um símbolo usado para se referir ao endereço base do armazenamento local de thread e aponta para a área na memória que contém todos os dados locais da thread de um módulo.