por Renie » 10 Fev 2011 11:12
Post do Polesapart
Seguinte:
O processo de linkedição de um binário *dinâmico* (o padrão no linux) é um processo complicadinho.
Em primeiro lugar, o formato do executável do linux é o ELF. O ELF é um formato bastante versátil, cheio de cabeçalhos, não é simplesmente um arquivo binário que o sistema carrega pra memória e manda ver.
A linkedição de um binário dinâmico ocorre em duas etapas: quando você compila um programa e faz a linkedição final pra gerar o arquivo elf, o que na verdade ocorre é que o linker agrupa os arquivos objeto e gera o arquivo binário, mas não coloca junto as bibliotecas. Ao invés disto, ele cria uma secção no arquivo ELF contendo os nomes dos símbolos não resolvidos e cria as chamadas tabelas de relocação, que podemos pensar que são tabelas de ponteiros para símbolos, que fazem referência aquela outra tabela de nomes.
Feito isto tudo e mais umas coisinhas, está completa a primeira etapa da linkedição, e você tem o seu executável com todo o código compilado, exceto as dependências dinâmicas, que via de regra são as bibliotecas (a própria libc é uma). Nada te impediria também de linkar bibliotecas estáticas, neste caso elas são agregadas ao executável, normalmente, como ocorreria em qualquer linker que você já trabalhou antes. Dá até pra passar a opção -static para o linker, e aí ele degenera em um linker burrinho que não faz nenhuma dessas mágicas e gera um arquivo ELF que pra todos os efeitos é quase tão tapado quanto um .bin (ok, tou exagerando, mas só um pouco).
A segunda etapa ocorre em tempo de execução: quando você decide executar o arquivo elf que você criou, o que acaba ocorrendo é que o shell (ou outro processo qualquer ao qual voce deu a ordem para que execute o programa) faz um fork() de si mesmo, criando um processo filho, e este processo filho faz um exec() contra o executável que você forneceu (isto tem a ver com o fato de, no unix, todo processo ter um processo pai, a excessão do init, mas isto está fora do contexto aqui. No ruindows é diferente, no ruindows um processo manda o kernel criar um novo processo e ele faz isso sozinho).
O que ocorre é que a chamada de sistema exec() faz com que o processo filho seja "limpo", ou seja, na prática é como se fosse um novo processo, e então o kernel substitui a imagem do processo por aquela do arquivo executável que foi passado como parametro pro exec.
Pra fazer esta última etapa, o kernel, que é espertinho, olha o cabeçalho do arquivo e descobre que é um arquivo ELF dinâmico. O kernel não sabe carregar um arquivo ELF dinâmico sozinho, então no próprio arquivo ELF existe um secção que aponta para um programa auxiliar, o dynamic linker. É um simples texto com o caminho do executável, geramente /lib/ld-linux.so.2 ou algo que o valha.
O kernel então carrega este /lib/ld-linux.so.2 , que é um executável ELF estático, para uma área de memória que é sempre alocada inicialmente.
Mas onde fica esta área de memória? Fisicamente, não importa: cada processo no linux usa endereçamento virtual de memória, então o processo pensa que tem (quase) todo o espaço de endereçamento de 4GB (32 bits) pra ele.
O kernel então executa o código nesta área de memória. O dynamic linker então começa a sua brincadeira: ele começa a escarafunchar os cabeçalhos do arquivo ELF e a carregar secções para a memória. Como existe apenas uma área de memória inicialmente alocada pelo kernel, o que o dynamic linker faz é criar novas, e setar suas propriedades, usando a chamada de sistema mmap() (neste ponto, se fosse um executável estático, o kernel estaria fazendo esta etapa sozinho).
Então quando ele achou uma secção de dados somente leitura, ele a carrega para a memória e marca seus atributos como área com permissão apenas de leitura. Quando acha uma secção de código executável, ele marca como permissão para leitura e execução, e assim por diante, sendo que certas áreas de dados terão permissão pra gravação. Se o teu programa, quando for executar, desrespeitar estas permissões, o kernel dará um chute no traseiro dele, mandando um sinal SIGSEGV ou equivalente, e terminando o processo.
Mas teu programa não tá pronto pra executar ainda. O dynamic linker descobre que existem dependências para bibliotecas dinamicas, entao o que ele faz é ir atrás do arquivo mencionado no cabeçalho do teu arquivo ELF, por exemplo, procurando o nome libc.so.6 num pequeno banco de dados que ele possui, e achando o arquivo /lib/libc-2.3.2.so como sendo o responsavel por prover este nome, e então o dynamic linker faz a carga desta biblioteca para uma área da memória, e aí descobre que esta biblioteca tem dependencias para outras, e vai fazendo o processo recursivamente, até resolver todas as pendencias.
Durante este processo, ele analisa o teu programa, consultando a secção de relocações não resolvidas, e descobre que os símbolos que o teu programa precisa são providos por certas bibliotecas, e assim sucessivamente, colocando nas tabelas de relocações ponteiros para os endereços onde ele carregou os símbolos das bibliotecas.
Basicamente, ele fez boa parte do que o linker estático varia em tempo de linkedição, e tem que fazer tudo isso pra cada ELF dinâmico que for carregado.
Mas em que endereço da memória virtual ele carregas as bibliotecas? geralmente, ele vai alocando os endereços a medida em que carrega as bibliotecas, sendo que eles podem até ser de certa forma randomizados.
O mapa de endereços alocados ao teu executável costuma ser fixo, mas as bibliotecas podem ir parar em faixas de memória diferentes, razão pela qual em certas arquiteturas, elas precisam ser compiladas com a opção PIC (position-independent code), de modo que endereços absolutos não sejam usados diretamente.
Existem algumas sutilezas neste processo, mas em linhas gerais, é isso.
_________________
Warning: time of day goes back (-163479us), taking countermeasures. Smile