Para entender os vinculadores, é melhor entender primeiro o que acontece "oculto" quando você converte um arquivo de origem (como um arquivo C ou C ++) em um arquivo executável (um arquivo executável é um arquivo que pode ser executado em sua máquina ou em outro computador). máquina de outra pessoa executando a mesma arquitetura de máquina).
Sob o capô, quando um programa é compilado, o compilador converte o arquivo de origem em código de byte do objeto. Esse código de byte (às vezes chamado de código de objeto) é instruções mnemônicas que apenas a arquitetura do computador entende. Tradicionalmente, esses arquivos têm uma extensão .OBJ.
Depois que o arquivo de objeto é criado, o vinculador entra em ação. Na maioria das vezes, um programa real que faça qualquer coisa útil precisará fazer referência a outros arquivos. Em C, por exemplo, um programa simples para imprimir seu nome na tela consistiria em:
printf("Hello Kristina!\n");
Quando o compilador compilou seu programa em um arquivo obj, ele simplesmente coloca uma referência à printf
função. O vinculador resolve essa referência. A maioria das linguagens de programação possui uma biblioteca padrão de rotinas para cobrir o material básico esperado dessa linguagem. O vinculador vincula seu arquivo OBJ a esta biblioteca padrão. O vinculador também pode vincular seu arquivo OBJ com outros arquivos OBJ. Você pode criar outros arquivos OBJ que possuem funções que podem ser chamadas por outro arquivo OBJ. O vinculador funciona quase como copiar e colar de um processador de texto. Ele "copia" todas as funções necessárias que o seu programa faz referência e cria um único executável. Às vezes, outras bibliotecas copiadas dependem de outros arquivos OBJ ou de biblioteca. Às vezes, um vinculador precisa ficar bastante recursivo para fazer seu trabalho.
Observe que nem todos os sistemas operacionais criam um único executável. O Windows, por exemplo, usa DLLs que mantêm todas essas funções juntas em um único arquivo. Isso reduz o tamanho do seu executável, mas o torna dependente dessas DLLs específicas. O DOS costumava usar coisas chamadas Sobreposições (arquivos .OVL). Isso tinha muitos propósitos, mas um era manter as funções comumente usadas juntas em um arquivo (outro objetivo que serviu, caso você esteja se perguntando, era conseguir encaixar programas grandes na memória. O DOS tem uma limitação na memória e sobreposições) ser "descarregado" da memória e outras sobreposições podem ser "carregadas" na parte superior da memória, daí o nome "sobreposições"). O Linux compartilhou bibliotecas, o que é basicamente a mesma idéia que as DLLs (os caras mais importantes do Linux que eu conheço me diriam que existem MUITAS GRANDES diferenças).
Espero que isso ajude você a entender!
Exemplo mínimo de realocação de endereços
A realocação de endereços é uma das funções cruciais da vinculação.
Então, vamos dar uma olhada em como funciona com um exemplo mínimo.
0) Introdução
Resumo: a realocação edita a
.text
seção dos arquivos de objeto para traduzir:Isso deve ser feito pelo vinculador, porque o compilador vê apenas um arquivo de entrada por vez, mas precisamos conhecer todos os arquivos de objetos de uma só vez para decidir como:
.text
e.data
seções de múltiplos arquivos de objetoPré-requisitos: entendimento mínimo de:
A vinculação não tem nada a ver com C ou C ++ especificamente: os compiladores apenas geram os arquivos de objeto. O vinculador as aceita como entrada, sem nunca saber qual idioma as compilou. Pode muito bem ser Fortran.
Então, para reduzir a crosta, vamos estudar um olá mundo NASM x86-64 ELF Linux:
compilado e montado com:
com NASM 2.10.09.
1) .texto de .o
Primeiro, descompilamos a
.text
seção do arquivo de objeto:que dá:
as linhas cruciais são:
que deve mover o endereço da cadeia hello world para o
rsi
registro, que é passado para a chamada do sistema de gravação.Mas espere! Como o compilador pode saber onde
"Hello world!"
ficará a memória quando o programa for carregado?Bem, não pode, especialmente depois de vincularmos um monte de
.o
arquivos a várias.data
seções.Somente o vinculador pode fazer isso, pois somente ele terá todos esses arquivos de objeto.
Então, o compilador apenas:
0x0
na saída compiladaEsta "informação extra" está contida na
.rela.text
seção do arquivo de objeto2) .rela.text
.rela.text
significa "realocação da seção .text".A palavra realocação é usada porque o vinculador precisará realocar o endereço do objeto para o executável.
Podemos desmontar a
.rela.text
seção com:que contém;
O formato desta seção é corrigido documentado em: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Cada entrada informa ao vinculador sobre um endereço que precisa ser realocado, aqui temos apenas um para a sequência.
Simplificando um pouco, para esta linha em particular, temos as seguintes informações:
Offset = C
: qual é o primeiro byte do.text
que esta entrada altera.Se olharmos para o texto descompilado, ele estará exatamente dentro do crítico
movabs $0x0,%rsi
e aqueles que conhecem a codificação da instrução x86-64 perceberão que isso codifica a parte do endereço de 64 bits da instrução.Name = .data
: o endereço aponta para a.data
seçãoType = R_X86_64_64
, que especifica exatamente o que cálculo deve ser feito para converter o endereço.Esse campo é realmente dependente do processador e, portanto, documentado na seção 4.464 "Relocação" da extensão AMD64 System V ABI .
Esse documento diz que
R_X86_64_64
:Field = word64
: 8 bytes, portanto, o00 00 00 00 00 00 00 00
endereço0xC
Calculation = S + A
S
é o valor no endereço que está sendo realocado,00 00 00 00 00 00 00 00
A
é o adendo que está0
aqui. Este é um campo da entrada de realocação.Então
S + A == 0
, seremos realocados para o primeiro endereço da.data
seção.3). Texto de .out
Agora vamos ver a área de texto do executável
ld
gerado para nós:dá:
Portanto, a única coisa que mudou no arquivo de objeto são as linhas críticas:
que agora apontam para o endereço
0x6000d8
(d8 00 60 00 00 00 00 00
em little-endian) em vez de0x0
.Esse é o local certo para a
hello_world
string?Para decidir, precisamos verificar os cabeçalhos do programa, que informam ao Linux onde carregar cada seção.
Nós os desmontamos com:
que dá:
Isso nos diz que a
.data
seção, que é a segunda, começa emVirtAddr
=0x06000d8
.E a única coisa na seção de dados é a nossa string hello world.
Nível de bônus
PIE
link: Qual é a opção -fPIE para executáveis independentes de posição no gcc e no ld?fonte
Em linguagens como 'C', módulos de código individuais são tradicionalmente compilados separadamente em blobs de código de objeto, prontos para serem executados em todos os aspectos que não sejam todas as referências que o módulo faz fora de si (por exemplo, para bibliotecas ou outros módulos). ainda não foi resolvido (ou seja, eles estão em branco, aguardando alguém aparecer e fazer todas as conexões).
O que o vinculador faz é olhar para todos os módulos juntos, para o que cada módulo precisa se conectar com fora de si e para todas as coisas que está exportando. Em seguida, ele corrige tudo e produz um executável final, que pode ser executado.
Onde a vinculação dinâmica também está em andamento, a saída do vinculador ainda não pode ser executada - ainda existem algumas referências a bibliotecas externas ainda não resolvidas e elas são resolvidas pelo SO no momento em que carrega o aplicativo (ou possivelmente até mais tarde durante a corrida).
fonte
Quando o compilador produz um arquivo de objeto, ele inclui entradas para símbolos definidos nesse arquivo de objeto e referências a símbolos que não são definidos nesse arquivo de objeto. O vinculador as pega e as reúne para que (quando tudo funcione corretamente) todas as referências externas de cada arquivo sejam satisfeitas pelos símbolos definidos em outros arquivos de objetos.
Em seguida, combina todos esses arquivos de objeto e atribui endereços a cada um dos símbolos, e onde um arquivo de objeto tem uma referência externa a outro arquivo de objeto, ele preenche o endereço de cada símbolo sempre que for usado por outro objeto. Em um caso típico, ele também criará uma tabela com todos os endereços absolutos usados, para que o carregador possa / conserte os endereços quando o arquivo for carregado (ou seja, adicionará o endereço de carregamento base a cada um desses endereços para que todos se refiram ao endereço de memória correto).
Alguns linkers modernos também podem executar algumas (em alguns casos, muito ) outras "coisas", como otimizar o código de maneiras que só são possíveis quando todos os módulos estiverem visíveis (por exemplo, removendo funções incluídas) porque era possível que algum outro módulo os chamasse, mas depois que todos os módulos estiverem reunidos, é evidente que nada os chama).
fonte