O que são vinculadores e carregadores? Como eles funcionam?


8

Estou tentando entender melhor coisas como vinculadores e carregadores.

A que área da ciência da computação eles pertencem? Compilador, sistema operacional, arquitetura de computadores?

Onde vinculadores e carregadores entram em jogo durante o desenvolvimento?


Eu editei esta pergunta para saber mais sobre o que são os linkers e carregadores do que sobre outros lugares para aprender sobre eles, já que este site é o lugar certo para esse tipo de coisa. Se houver bons recursos por aí, deixe-os emergir naturalmente por meio de respostas, em vez de apenas tentar criar uma lista de recursos externos.
Adam Lear

9
Não se ofenda, mas leia os artigos da wikipedia, pois eles podem ajudar a focar sua pergunta: en.wikipedia.org/wiki/Linker_%28computing%29 e en.wikipedia.org/wiki/Loader_%28computing%29
Eric Wilson

Não sei por que recebi a votação - pensei que essa pergunta também poderia ajudar outros novatos que queriam saber disso - além de obter algumas orientações. esta é uma pergunta que não progroga?
Nishant

2
Eu pude ver os votos negativos, pois essa pergunta é bastante ampla e não mostra muito trabalho para descobrir a resposta por conta própria.
JB rei

Respostas:


17

A relação exata varia um pouco. Para começar, considerarei (quase) o modelo mais simples possível, usado por algo como o MS-DOS, em que um executável sempre estará estaticamente vinculado. Por uma questão de exemplo, vamos considerar o canônico "Olá, mundo!" programa, que assumiremos que está escrito em C.

Compilador

O compilador compilará isso em algumas partes. Ele pegará a string literal "Hello, World!" E a colocará em uma seção marcada como dados constantes, e sintetizará um nome para essa string específica (por exemplo, "$ L1"). Ele compilará a chamada para printfoutra seção marcada como código. Nesse caso, ele dirá que o nome é main(ou, freqüentemente _main). Também terá algo a dizer que esse pedaço de código tem N bytes de comprimento e (o mais importante) contém uma chamada para o printfdeslocamento M nesse código.

Linker

Depois que o compilador terminar de produzir isso, o vinculador será executado. É normalmente considerado parte da cadeia de ferramentas de desenvolvimento (embora haja exceções - o MS-DOS costumava incluir um vinculador, embora raramente fosse usado). Embora normalmente não seja visível externamente, normalmente serão transmitidos alguns argumentos de linha de comando, um especificando um arquivo de objeto contendo algum código de inicialização e outro especificando qualquer arquivo que contenha a biblioteca padrão C.

O vinculador examinará o arquivo de objeto que contém o código de inicialização e descobrirá que ele tem, digamos, 1112 bytes de comprimento e tem uma chamada para o _maindeslocamento 784 nele .

Com base nisso, ele começará a criar uma tabela de símbolos. Ele terá uma entrada dizendo ".startup" (ou qualquer outro nome) com 1112 bytes de comprimento e (até o momento) nada se refere a esse nome. Terá outra entrada dizendo "printf" é um comprimento desconhecido atual, mas é referido em ".startup + 784".

Em seguida, ele examinará a (s) biblioteca (s) especificada (s) para tentar encontrar definições dos nomes na tabela de símbolos que não estão atualmente definidas - nesse caso printf. Ele encontrará o arquivo de objeto para printf dizendo que tem 4087 bytes de comprimento e tem referências a outras rotinas para fazer coisas como converter um int em uma string, assim como coisas putchar(ou talvez fputc) para gravar a string resultante na saída Arquivo.

O vinculador fará uma nova varredura para tentar encontrar definições desses símbolos, recursivamente, até chegar a uma das duas conclusões: ou foram encontradas definições de todos os símbolos ou existe um símbolo para o qual não consegue encontrar uma definição.

Se encontrar uma referência, mas nenhuma definição, ele será interrompido e emitirá uma mensagem de erro dizendo algo sobre um "XXX externo indefinido", e cabe a você descobrir que outra biblioteca ou arquivo de objeto você precisa vincular .

Se encontrar definições de todos os símbolos, passará para a próxima fase: percorre a lista de locais que se referem a cada símbolo e preenche o endereço onde esse símbolo foi colocado na memória, então (por exemplo, ) onde o código de inicialização chama main, ele preencherá o endereço 1112como o endereço principal. Uma vez feito tudo isso, ele gravará todo o código e dados em um arquivo executável.

Existem alguns outros detalhes menores que provavelmente merecem ser mencionados: normalmente mantêm o código e os dados separados e, após a conclusão de cada um, os reúne em endereços (mais ou menos) consecutivos (por exemplo, todas as partes de código, depois todas as partes de dados). Normalmente, também haverá algumas regras sobre como combinar definições para seção / segmentos - por exemplo, se arquivos de objetos diferentes tiverem segmentos de código, apenas organizaremos os trechos de código um após o outro. Se duas ou mais literais de seqüência de caracteres idênticas (ou outras constantes) forem definidas, normalmente elas serão mescladas para que todas se refiram ao mesmo local. Existem também algumas regras para o que fazer quando / se encontrar definições duplicadas do mesmo símbolo. Em um caso típico, isso será simplesmente um erro. Em alguns casos, haverá coisas como "se alguém também o definir, não o considere um erro - basta usar essa definição em vez desta.

Depois de ter entradas para todos os símbolos, o vinculador deve organizar as "peças" e atribuir endereços a elas. A ordem em que ele organiza as partes variará um pouco - normalmente haverá algumas sinalizações sobre os tipos de partes diferentes; portanto, por exemplo, todos os dados constantes acabam próximos um do outro, todos os códigos ao lado de um ao outro e assim por diante. No nosso sistema simples, semelhante ao MS-DOS, a maior parte disso não importa muito.

Carregador

Isso nos leva à próxima fase: o carregador. o carregador geralmente faz parte do sistema operacional, que carrega o executável. Nas versões antigas (por exemplo, arquivos CP / M, MS_DOS .com, o carregador apenas lê os dados de um arquivo executável na memória e depois começa a executar em algum endereço. Carregadores um pouco mais recentes (por exemplo, arquivos MS-DOS .exe) irão comece (mais ou menos) da mesma maneira: leia um arquivo na memória.Neste caso, no entanto, com base nas entradas inseridas pelo vinculador, ele "corrigirá" quaisquer referências absolutas no executável para se referir ao arquivo No exemplo acima, nosso código de inicialização referido emmain no endereço 1112, mas o executável está sendo carregado em um endereço base de (digamos) 4000. Nesse caso, o carregador corrigirá esse endereço para se referir a 5112. Nesse sistema simples, no entanto, o carregador ainda é um um pedaço de código bastante simples - basicamente apenas percorrendo a lista de realocações e adicionando o endereço base a cada um.

Agora vamos considerar um sistema operacional um pouco mais moderno que suporta algo como arquivos de objeto compartilhado ou DLLs. Isso basicamente transfere parte do trabalho do vinculador para o carregador. Em particular, para um símbolo definido em um arquivo .so / DLL, o vinculador não tentará atribuir um endereço.

Em vez disso, criará uma entrada na tabela de símbolos que basicamente diz "definido no arquivo .so / DLL XXX". Quando o vinculador grava o executável, a maioria dessas entradas da tabela de símbolos é basicamente copiada para o executável, dizendo "o símbolo XXX está definido no arquivo AAAA". Cabe ao carregador encontrar o arquivo AAAA e o endereço do símbolo XXX nesse arquivo e preencher o endereço correto onde quer que seja usado no executável. Assim como no vinculador, isso será recursivo; portanto, a DLL A pode se referir a símbolos na DLL B, que podem se referir à DLL C e assim por diante. Embora a cadeia de executáveis ​​a todas as definições possa ser longa, a idéia básica do processo é bastante simples - verifique a lista de referências externas e encontre uma definição para cada uma. Observe também que, na maioria dos casos,

Novamente, existem alguns pedaços e considerações a serem considerados. Por exemplo, o compartilhamento normalmente ocorrerá apenas seção por seção, não arquivo por arquivo. Se um arquivo tiver algum código e alguns dados (não constantes), por exemplo, todos os processos compartilharão as mesmas seções de código, mas cada um terá sua própria cópia dos dados.


Os carregadores mais simples nem têm capacidade de correção. Estou pensando no antigo TRS-80. Os arquivos executáveis ​​eram simplesmente endereço / tamanho / dados, repetidos conforme necessário (o tamanho era de 1 byte, normalmente você precisava de vários blocos). Você pode até criar telas de apresentação especificando um endereço de carregamento na memória de vídeo.
Loren Pechtel 14/05

1

Para descobrir mais sobre os vinculadores, acho que eles geralmente serão discutidos em combinação com os compiladores. Eles servem para unir seus vários módulos em uma unidade coesa, finalizando os endereços dentro desse código. Alguns podem até tentar realizar otimizações.

Para descobrir mais sobre carregadores, acho que eles geralmente serão discutidos em combinação com a criação de compiladores para arquiteturas específicas, a menos que você queira dizer carregador como sinônimo de vinculador. Estou pensando no carregador como a parte do cabeçalho do arquivo executável que informa ao sistema operacional como abrir e executar o software compilado.

Concordo que a leitura dos artigos da Wikipedia provavelmente fornecerá mais informações do que você está procurando. Quanto ao local onde eles entram em desenvolvimento ... geralmente eles estão fora do controle do projeto e fazem parte da seleção do sistema operacional e do pacote de desenvolvimento que você escolhe usar. É muito raro você usar (por exemplo) o MSVC, mas deseja executar um vinculador baseado no GCC ... talvez nem seja possível. O ÚNICO lugar que eu já usei um vinculador não padrão foi na IBM quando estávamos usando cópias de desenvolvimento.

Se você tiver perguntas mais específicas e específicas sobre esses tópicos, acho que encontrará uma resposta muito melhor.


1

Vinculadores e carregadores são dois conceitos relacionados, mas separados.

Os vinculadores fazem parte da teoria do compilador. Quando você compila um projeto composto por mais de um módulo (arquivo de código-fonte), é comum que o compilador produza um único arquivo intermediário para cada módulo de origem. Isso tem vários benefícios, um dos quais é que, se você fizer apenas alterações em um arquivo e depois recompilar, não precisará reconstruir o projeto inteiro quando tiver feito apenas uma alteração local.

Mas isso significa que, se você tiver código em um módulo que chama uma função em um módulo diferente, o compilador não poderá gerar uma CALLinstrução para ele, porque não possui o local dessa outra função. Ele está em um arquivo intermediário diferente e o local exato da função pode mudar se você fizer uma alteração local no arquivo de origem do intermediário e recompilá-lo. Então, ao invés disso, ele insere um "token de referência externo" (exatamente o que é ou o que parece não importa, apenas pense nele como um conceito abstrato) que diz "Preciso dessa função cujo endereço exato não sei no momento."

Depois que tudo foi compilado em arquivos intermediários, o vinculador é o que finaliza o trabalho. Ele percorre todos os arquivos intermediários e os vincula em um binário final. Como está organizando as coisas, ele conhece os endereços reais de todas as funções e, portanto, pode substituir os tokens de referência externos por CALLinstruções reais para os locais corretos no binário.

O carregador, por outro lado, pertence ao sistema operacional, não ao compilador. Sua tarefa é carregar o binário na memória para que ele possa ser executado e finalizar o processo de vinculação, pois o vinculador pode resolver apenas o código que conhece. Se o seu programa estiver usando alguma DLL, ela é externa, mesmo para o binário compilado, para que o vinculador não saiba o endereço. Ele deixa tokens de referência externos no binário final em um formato que o carregador do SO conhece e, em seguida, o carregador os compara aos toques de função reais nas DLLs, depois que tudo foi carregado na memória.


-1

Os computadores trabalham basicamente com números binários.
As pessoas falam suas línguas nativas.

Linguagens de programação são para comunicação entre pessoas e computadores.
Se você disser: Adicione 2 e 3 e subtraia 1 a partir dele, duvido que o computador entenda alguma coisa (talvez em alguma linguagem de programação).

Portanto, você precisa traduzir seu código-fonte em um formato que o computador entenda; assim, você precisa de um compilador, que traduz uma linguagem de programação para o chamado código de objeto. Mas o código do objeto ainda não é o idioma que um computador entende e executa diretamente. Portanto, ele precisa de um vinculador que crie um arquivo executável que contenha instruções na chamada linguagem de máquina; uma linguagem de máquina é um conjunto de operações codificadas em números binários que o processador entende. Todas as instruções binárias têm sua estrutura e são publicadas pelos fabricantes de um processador. Você pode procurá-lo no site da Intel e ver como eles se parecem.


Aqui está um guia do desenvolvedor da AMD: developer.amd.com/documentation/guides/Pages/default.aspx
spc
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.