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 printf
outra 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 printf
deslocamento 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 _main
deslocamento 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 1112
como 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.