A compilação de um programa C ++ envolve três etapas:
Pré-processamento: o pré-processador pega um arquivo de código-fonte C ++ e lida com as diretivas #include
s, se #define
outras diretivas de pré-processador. A saída desta etapa é um arquivo C ++ "puro" sem diretivas de pré-processador.
Compilação: o compilador pega a saída do pré-processador e produz um arquivo de objeto a partir dele.
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 #include
e #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 #include
directivas 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
, #ifdef
e #ifndef
directivas.
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 #if
e #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.
Este tópico é discutido no CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html
Aqui está o que o autor escreveu:
fonte
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.
fonte
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.
fonte
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
).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.
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".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".fonte
Veja o URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
O processo completo de conformidade do C ++ é apresentado claramente nesta URL.
fonte