Como funciona o processo de compilação / vinculação?


416

Como funciona o processo de compilação e vinculação?

(Observação: isso deve ser uma entrada para as Perguntas frequentes sobre C ++ do Stack Overflow . Se você quiser criticar a idéia de fornecer uma FAQ neste formulário, a postagem na meta que iniciou tudo isso seria o lugar para isso. essa pergunta é monitorada na sala de chat do C ++ , onde a idéia de FAQ começou em primeiro lugar; portanto, é muito provável que sua resposta seja lida por quem a teve.)

Respostas:


554

A compilação de um programa C ++ envolve três etapas:

  1. Pré-processamento: o pré-processador pega um arquivo de código-fonte C ++ e lida com as diretivas #includes, se #defineoutras diretivas de pré-processador. A saída desta etapa é um arquivo C ++ "puro" sem diretivas de pré-processador.

  2. Compilação: o compilador pega a saída do pré-processador e produz um arquivo de objeto a partir dele.

  3. Vinculação: o vinculador pega os arquivos de objeto produzidos pelo compilador e produz uma biblioteca ou um arquivo executável.

Pré-processando

O pré-processador lida com as diretivas do pré - processador , como #includee #define. É independente da sintaxe do C ++, motivo pelo qual deve ser usado com cuidado.

Ele funciona em um arquivo de origem C ++ em um momento substituindo #includedirectivas com o conteúdo dos respectivos arquivos (que normalmente é apenas declarações), fazendo a substituição de macros ( #define), e selecionar diferentes partes do texto dependendo de #if, #ifdefe #ifndefdirectivas.

O pré-processador trabalha em um fluxo de tokens de pré-processamento. A substituição de macro é definida como a substituição de tokens por outros tokens (o operador ##permite mesclar dois tokens quando faz sentido).

Depois de tudo isso, o pré-processador produz uma única saída que é um fluxo de tokens resultantes das transformações descritas acima. Ele também adiciona alguns marcadores especiais que informam ao compilador de onde cada linha veio, para que ele possa ser usado para produzir mensagens de erro sensíveis.

Alguns erros podem ser produzidos nesta fase com o uso inteligente das diretivas #ife #error.

Compilação

A etapa de compilação é realizada em cada saída do pré-processador. O compilador analisa o código-fonte C ++ puro (agora sem diretivas de pré-processador) e o converte em código de montagem. Em seguida, chama o back-end subjacente (assembler no conjunto de ferramentas) que reúne esse código no código da máquina produzindo o arquivo binário real em algum formato (ELF, COFF, a.out, ...). Este arquivo de objeto contém o código compilado (em formato binário) dos símbolos definidos na entrada. Os símbolos nos arquivos de objetos são referidos pelo nome.

Arquivos de objeto podem se referir a símbolos que não estão definidos. Este é o caso quando você usa uma declaração e não fornece uma definição para ela. O compilador não se importa com isso e, felizmente, produzirá o arquivo de objeto, desde que o código-fonte esteja bem formado.

Os compiladores geralmente permitem que você pare a compilação neste momento. Isso é muito útil, pois com ele você pode compilar cada arquivo de código-fonte separadamente. A vantagem que isso oferece é que você não precisa recompilar tudo se alterar apenas um único arquivo.

Os arquivos de objetos produzidos podem ser colocados em arquivos especiais chamados bibliotecas estáticas, para facilitar a reutilização posteriormente.

É nesse estágio que são relatados erros "regulares" do compilador, como erros de sintaxe ou erros de resolução de sobrecarga com falha.

Linking

O vinculador é o que produz a saída final da compilação a partir dos arquivos de objeto que o compilador produziu. Essa saída pode ser uma biblioteca compartilhada (ou dinâmica) (e, embora o nome seja semelhante, eles não têm muito em comum com as bibliotecas estáticas mencionadas anteriormente) ou um executável.

Ele vincula todos os arquivos de objeto substituindo as referências a símbolos indefinidos pelos endereços corretos. Cada um desses símbolos pode ser definido em outros arquivos de objeto ou em bibliotecas. Se eles estiverem definidos em bibliotecas que não sejam a biblioteca padrão, você precisará informar o vinculador sobre elas.

Nesse estágio, os erros mais comuns estão em falta de definições ou definições duplicadas. O primeiro significa que as definições não existem (ou seja, não foram gravadas) ou que os arquivos ou bibliotecas de objetos em que residem não foram fornecidos ao vinculador. O último é óbvio: o mesmo símbolo foi definido em dois arquivos ou bibliotecas de objetos diferentes.


39
O estágio de compilação também chama o assembler antes de converter em arquivo de objeto.
usar o seguinte comando

3
Onde as otimizações são aplicadas? À primeira vista, parece que isso seria feito na etapa de compilação, mas por outro lado, posso imaginar que a otimização adequada só pode ser feita após a vinculação.
Bart van Heukelom

6
Tradicionalmente, o @BartvanHeukelom era feito durante a compilação, mas os compiladores modernos suportam a chamada "otimização do tempo de link", que tem a vantagem de poder otimizar as unidades de tradução.
R. Martinho Fernandes

3
C tem os mesmos passos?
Kevin Zhu

6
Se o vinculador converter símbolos referentes a classes / métodos em bibliotecas em endereços, isso significa que os binários da biblioteca são armazenados nos endereços de memória que o SO mantém constante? Estou confuso sobre como o vinculador saberia o endereço exato, digamos, do binário stdio para todos os sistemas de destino. O caminho do arquivo sempre seria o mesmo, mas o endereço exato pode mudar, certo?
9788 Dan Carter

42

Este tópico é discutido no CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Aqui está o que o autor escreveu:

Compilar não é o mesmo que criar um arquivo executável! Em vez disso, a criação de um executável é um processo de vários estágios dividido em dois componentes: compilação e vinculação. Na realidade, mesmo se um programa "compila bem", pode não funcionar de fato devido a erros durante a fase de vinculação. O processo total de passar de arquivos de código-fonte para um executável pode ser melhor referido como uma compilação.

Compilação

Compilação refere-se ao processamento de arquivos de código fonte (.c, .cc ou .cpp) e a criação de um arquivo 'objeto'. Esta etapa não cria nada que o usuário possa executar. Em vez disso, o compilador apenas produz as instruções de linguagem de máquina que correspondem ao arquivo de código-fonte que foi compilado. Por exemplo, se você compilar (mas não vincular) três arquivos separados, você terá três arquivos de objetos criados como saída, cada um com o nome .o ou .obj (a extensão dependerá do seu compilador). Cada um desses arquivos contém uma tradução do seu arquivo de código-fonte em um arquivo de idioma da máquina - mas você ainda não pode executá-los! Você precisa transformá-los em executáveis ​​que seu sistema operacional possa usar. É aí que o vinculador entra.

Linking

Vincular refere-se à criação de um único arquivo executável a partir de vários arquivos de objeto. Nesta etapa, é comum que o vinculador se queixe de funções indefinidas (geralmente, o próprio principal). Durante a compilação, se o compilador não conseguisse encontrar a definição para uma função específica, ele assumiria que a função foi definida em outro arquivo. Se não for esse o caso, não há como o compilador saber - ele não analisa o conteúdo de mais de um arquivo por vez. O vinculador, por outro lado, pode examinar vários arquivos e tentar encontrar referências para as funções que não foram mencionadas.

Você pode perguntar por que existem etapas separadas de compilação e vinculação. Primeiro, é provavelmente mais fácil implementar as coisas dessa maneira. O compilador faz a sua parte e o vinculador faz a sua parte - mantendo as funções separadas, a complexidade do programa é reduzida. Outra vantagem (mais óbvia) é que isso permite a criação de programas grandes sem precisar refazer a etapa de compilação toda vez que um arquivo é alterado. Em vez disso, usando a chamada "compilação condicional", é necessário compilar apenas os arquivos de origem que foram alterados; para o restante, os arquivos de objeto são entrada suficiente para o vinculador. Por fim, isso simplifica a implementação de bibliotecas de código pré-compilado: basta criar arquivos de objetos e vinculá-los como qualquer outro arquivo de objeto.

Para obter todos os benefícios da compilação de condições, provavelmente é mais fácil obter um programa para ajudá-lo do que tentar lembrar os arquivos que você alterou desde a última compilação. (Você pode, é claro, apenas recompilar todos os arquivos que tenham um carimbo de data / hora maior que o carimbo de data / hora do arquivo de objeto correspondente.) Se você estiver trabalhando com um ambiente de desenvolvimento integrado (IDE), ele já pode cuidar disso. Se você estiver usando ferramentas de linha de comando, há um utilitário bacana chamado make que vem com a maioria das distribuições * nix. Juntamente com a compilação condicional, ele possui vários outros recursos interessantes para programação, como permitir compilações diferentes do seu programa - por exemplo, se você possui uma versão que produz uma saída detalhada para depuração.

Saber a diferença entre a fase de compilação e a fase do link pode facilitar a busca de bugs. Os erros do compilador geralmente são de natureza sintática - um ponto-e-vírgula ausente, um parêntese extra. Erros de vinculação geralmente têm a ver com definições ausentes ou múltiplas. Se você receber um erro de que uma função ou variável é definida várias vezes no vinculador, é uma boa indicação de que o erro é que dois dos seus arquivos de código-fonte têm a mesma função ou variável.


1
O que eu não entendo é que, se o pré-processador gerencia coisas como #includes para criar um super arquivo, certamente não há nada para vincular depois disso?
binarysmacker

@binarysmacer Veja se o que escrevi abaixo faz algum sentido para você. Tentei descrever o problema de dentro para fora.
Visualização elíptica

3
@binarysmacker É tarde demais para comentar sobre isso, mas outros podem achar isso útil. youtu.be/D0TazQIkc8Q Basicamente, você inclui arquivos de cabeçalho e esses arquivos de cabeçalho geralmente contêm apenas as declarações de variáveis ​​/ funções e não existem definições; as definições podem estar presentes em um arquivo de origem separado. o vinculador ajuda. Você vincula o arquivo de origem que usa a variável / função ao arquivo de origem que os define.
Karan Joisher

24

Na frente padrão:

  • uma unidade de tradução é a combinação de arquivos de origem, cabeçalhos e arquivos de origem incluídos, menos as linhas de origem ignoradas pela diretiva de pré-processador de inclusão condicional.

  • o padrão define 9 fases na tradução. Os quatro primeiros correspondem ao pré-processamento, os próximos três são a compilação, o próximo é a instanciação de modelos (produzindo unidades de instanciação ) e o último é a vinculação.

Na prática, a oitava fase (a instanciação de modelos) geralmente é realizada durante o processo de compilação, mas alguns compiladores a atrasam para a fase de vinculação e outros a espalham nas duas.


14
Você poderia listar todas as 9 fases? Seria uma boa adição à resposta, eu acho. :)
jalf


@ jalf, basta adicionar a instanciação do modelo imediatamente antes da última fase na resposta apontada por @sbi. No IIRC, existem diferenças sutis no texto preciso no manuseio de caracteres largos, mas acho que eles não aparecem nos rótulos dos diagramas.
precisa saber é o seguinte

2
@sbi sim, mas essa deveria ser a pergunta do FAQ, não é? Portanto, essa informação não deveria estar disponível aqui ? ;)
jalf 7/06/11

3
@ AProgrammmer: simplesmente listá-los pelo nome seria útil. As pessoas sabem o que procurar se quiserem mais detalhes. De qualquer forma, marcado com +1 sua resposta em qualquer caso :)
jalf

14

O essencial é que uma CPU carrega dados dos endereços de memória, armazena dados em endereços de memória e executa instruções seqüencialmente fora dos endereços de memória, com alguns saltos condicionais na sequência de instruções processadas. Cada uma dessas três categorias de instruções envolve a computação de um endereço para uma célula de memória a ser usada nas instruções da máquina. Como as instruções da máquina têm um comprimento variável, dependendo da instrução específica envolvida, e porque as juntamos à medida que construímos nosso código de máquina, há um processo de duas etapas envolvido no cálculo e na construção de qualquer endereço.

Primeiro, organizamos a alocação de memória da melhor maneira possível, antes de podermos saber exatamente o que acontece em cada célula. Descobrimos os bytes, ou palavras, ou o que for que forma as instruções, literais e quaisquer dados. Apenas começamos a alocar memória e a construir os valores que criarão o programa à medida que avançamos, e anote em qualquer lugar que precisarmos voltar e corrigir um endereço. Nesse local, colocamos um manequim para preencher apenas o local, para que possamos continuar calculando o tamanho da memória. Por exemplo, nosso primeiro código de máquina pode levar uma célula. O próximo código de máquina pode levar três células, envolvendo uma célula de código de máquina e duas células de endereço. Agora, nosso ponteiro de endereço é 4. Sabemos o que se passa na célula da máquina, que é o código operacional, mas temos que esperar para calcular o que se passa nas células de endereço até sabermos onde esses dados estarão localizados.

Se houvesse apenas um arquivo de origem, um compilador poderia teoricamente produzir código de máquina totalmente executável sem um vinculador. Em um processo de duas passagens, ele poderia calcular todos os endereços reais para todas as células de dados referenciadas por qualquer instrução de carregamento ou armazenamento da máquina. E poderia calcular todos os endereços absolutos mencionados por qualquer instrução de salto absoluto. É assim que os compiladores mais simples, como o do Forth, funcionam, sem vinculador.

Um vinculador é algo que permite que blocos de código sejam compilados separadamente. Isso pode acelerar o processo geral de criação de código e permite certa flexibilidade na maneira como os blocos são usados ​​posteriormente, ou seja, eles podem ser realocados na memória, por exemplo, adicionando 1000 a cada endereço para aumentar o bloco por 1000 células de endereço.

Portanto, o que o compilador produz é um código de máquina aproximado que ainda não foi totalmente construído, mas é definido para sabermos o tamanho de tudo, em outras palavras, para que possamos começar a calcular onde todos os endereços absolutos serão localizados. o compilador também gera uma lista de símbolos que são pares de nome / endereço. Os símbolos relacionam um deslocamento de memória no código da máquina no módulo com um nome. O deslocamento é a distância absoluta da localização da memória do símbolo no módulo.

É aí que chegamos ao vinculador. O vinculador primeiro junta todos esses blocos de código de máquina de ponta a ponta e anota onde cada um começa. Em seguida, calcula os endereços a serem corrigidos adicionando o deslocamento relativo em um módulo e a posição absoluta do módulo no layout maior.

Obviamente, simplifiquei demais isso para que você possa entender e não usei deliberadamente o jargão de arquivos de objetos, tabelas de símbolos etc., o que para mim é parte da confusão.


13

O GCC compila um programa C / C ++ em executável em 4 etapas.

Por exemplo, gcc -o hello hello.cé realizado da seguinte maneira:

1. Pré-processamento

Pré-processamento através do GNU C Preprocessor ( cpp.exe), que inclui os cabeçalhos ( #include) e expande as macros ( #define).

cpp hello.c > hello.i

O arquivo intermediário resultante "hello.i" contém o código-fonte expandido.

2. Compilação

O compilador compila o código-fonte pré-processado no código de montagem para um processador específico.

gcc -S hello.i

A opção -S especifica para produzir código de montagem, em vez de código de objeto. O arquivo de montagem resultante é "hello.s".

3. Montagem

O assembler ( as.exe) converte o código de montagem em código de máquina no arquivo de objeto "hello.o".

as -o hello.o hello.s

4. Linker

Por fim, o linker ( ld.exe) vincula o código do objeto ao código da biblioteca para produzir um arquivo executável "hello".

    ld -o hello hello.o ... bibliotecas ...

9

Veja o URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
O processo completo de conformidade do C ++ é apresentado claramente nesta URL.


2
Obrigado por compartilhar isso, é tão simples e direto de entender.
Mark

Bom, recurso, você pode colocar aqui uma explicação básica do processo, a resposta é sinalizada pelo algoritmo, pois baixa qualidade b / c é curto e apenas o URL.
26418 JasonB

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.