Compilar programas no Linux com o GCC [Guia Básico]

Um compilador é um programa especial que processa instruções escritas em uma linguagem de programação particular e a transforma em linguagem ou código de máquina.

Geralmente, um programador escreve instruções em uma linguagem de alto nível como Pascal ou C, usando um editor de textos. O arquivo que é criado contém o que chamamos de fonte do programa.

O programador então executa o compilador de linguagem apropriado, especificando o nome do arquivo que contém as instruções fonte.

Quando executado, o compilador primeiro analisa todas as instruções da linguagem sintaticamente uma após a outra e então, em um ou mais estágios ou “passos” sucessivos, constrói o código de saída.

Tradicionalmente, a saída da compilação é chamada de código objeto ou algumas vezes de módulo objeto. O código objeto é o código de máquina que o processador pode executar.

Tradicionalmente em alguns sistemas e linguagens, um passo adicional é necessário após a compilação.

Este passo é destinado a resolver o local relativo de instruções e dados quando mais de um módulo objeto é executado ao mesmo tempo e eles possuírem referências cruzadas de um para o outro. Este processo é conhecido como ligação.

O compilador mais utilizado no Linux é a Coleção de Compiladores GNU – GCC. Ele compila códigos C ANSI, bem como C++, Java e Fortran. O GCC suporta vários níveis de checagem de erros nos códigos-fonte, produz informações de debug e pode ainda otimizar o arquivo objeto produzido.

A compilação envolve até quatro estágios: pré-processamento, compilação propriamente dita, assembly e ligação, sempre nesta ordem.

O GCC é capaz de pré-processar e compilar vários arquivos em um ou mais arquivos em assembler.

O arquivo de assembler gera um ou mais arquivos chamados de objeto e estes são ligados às bibliotecas (linking) para tornarem um arquivo executável.

fig001 Compilar programas no Linux com o GCC [Guia Básico]

PRÉ-PROCESSAMENTO

O pré-processamento é responsável por expandir as macros e incluir os arquivos de cabeçalho no arquivo fonte. O resultado é um arquivo que contém o código fonte expandido.

COMPILAÇÃO

O próximo estágio chamado de “compilação propriamente dita” é responsável por traduzir o código fonte pré-processado em linguagem assembly (linguagem de máquina) para um processador específico.

ASSEMBLER

O estágio seguinte é chamado de assembler. Nesta etapa o GCC converte o código de máquina de um processador específico em um arquivo objeto.

Se neste código existirem chamadas externas de funções, o gcc deixa seus endereços indefinidos para serem preenchidos posteriormente pelo estágio de ligação.

LINKER

O último estágio chamado de ligação, ou linker, é responsável por ligar os arquivos objeto para criar um arquivo executável.

Ele faz isto preenchendo os endereços das funções indefinidas nos arquivos objeto com os endereços das bibliotecas externas do sistema operacional.

Isto é necessário porque os arquivos executáveis precisam de muitas funções externas do sistema e de bibliotecas do C para serem executados.

As bibliotecas podem ser ligadas ao executável preenchendo os endereços das bibliotecas nas chamadas externas ou de forma estática quando as funções das bibliotecas são copiadas para o executável.

No primeiro caso, o programa utilizará bibliotecas de forma compartilhada e ficará dependente delas para funcionar.

Este esquema economiza recursos, pois, uma biblioteca utilizada por muitos programas precisa ser carregada somente uma vez na memória. O tamanho do executável será pequeno.

Porém, se a biblioteca instalada for de uma versão diferente da que o executável necessita, o programa não será executado até que a versão apropriada da biblioteca seja instalada.

No segundo caso, o programa é independente, uma vez que as funções de que necessita estão no seu código.

Este esquema permite que quando houver uma mudança de versão de biblioteca o programa não será afetado. A desvantagem será o tamanho do executável e necessidades de mais recursos.

O GCC – GNU Compiler Collection

O GNU Compiler Collection – coleção de compiladores GNU – são compiladores completos da linguagem C ANSI com suporte para o K&R C, C++, Objective C, Java, e Fortran. O GCC oferece também diferentes níveis de checagem de erros de código-fonte e informações para depuração e otimizações do programa objeto.

Ele também suporta a moderna plataforma de processadores Intel IA-64. A versão 7.0 inclui novas APIs e bibliotecas C++. O manual também foi substancialmente reescrito e melhorado.

Pré-Processamento

O pré-processamento é responsável por expandir as macros e incluir os arquivos de cabeçalho no arquivo fonte. O resultado é um arquivo que contém o código fonte expandido.

O pré-processamento do simples exemplo abaixo irá gerar um arquivo em C com mais de 800 linhas de código expandido.

Veja como fica este simples programa estilo Hello World que Imprime Certificação Linux na tela:

#include <stdio.h>

int main()

{

        printf(“Certificação Linux!\n”);

};

A opção “-E” do GCC diz ao compilador para fazer apenas o pré-processamento do código fonte.

# gcc –E teste.c –o teste.i

Veja que o arquivo preprocessado teste.i gerou 841 linhas de código pré-processado:

# cat teste.i | wc

    841    2074   16879

Compilação no GCC

O próximo estágio chamado de “compilação propriamente dita” é responsável por traduzir o código-fonte pré-processado em linguagem assembly (linguagem de máquina) para um processador específico.

Para que você veja o resultado do estágio de compilação, a opção “-S” (maiúsculo) do GCC gera o código de máquina para um processador específico.

O código abaixo é o exemplo acima já pré-processado que foi compilado em assembly para o processador Intel Xeon:

# gcc –S teste.i  

# cat teste.s

.file “teste.c”

.section .rodata

.LC0:

.string “Certifica\303\247\303\243o Linux!”

.text

.globl main

.type main, @function

main:

.LFB0:

.cfi_startproc

pushq %rbp

.cfi_def_cfa_offset 16

.cfi_offset 6, -16

movq %rsp, %rbp

.cfi_def_cfa_register 6

movl $.LC0, %edi

call puts

popq %rbp

.cfi_def_cfa 7, 8

ret

.cfi_endproc

.LFE0:

.size main, .-main

.ident “GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)”

.section .note.GNU-stack,””,@progbits

Assembler do Código no gcc

O estágio seguinte é chamado de assembler. Nesta etapa o GCC converte o código de máquina de um processador específico em um arquivo objeto. Se neste código existirem chamadas externas de funções, o gcc deixa seus endereços indefinidos para serem preenchidos posteriormente pelo estágio de ligação.

O seguinte comando irá fazer a compilação do código assembly do exemplo anterior para o código de máquina:

# as teste.s -o teste.o

O comando as é um compilador assembler disponível no pacote GNU GCC.

O resultado será um arquivo “teste.o” que contém instruções de máquina do exemplo com a referência indefinida para a função externa printf.

A linha “call printf” do código assembly informa que esta função está definida em uma biblioteca e deverá ser chamada durante o processamento.

Linker do código com as bibliotecas

O último estágio chamado de ligação, ou linker,  é responsável por ligar os arquivos objeto para criar um arquivo executável.

Ele faz isto preenchendo os endereços das funções indefinidas nos arquivos objeto com os endereços das bibliotecas externas do sistema operacional.

Isto é necessário porque os arquivos executáveis precisam de muitas funções externas do sistema e de bibliotecas do C para serem executados.

As bibliotecas podem ser ligadas ao executável preenchendo os endereços das bibliotecas nas chamadas externas ou de forma estática quando as funções das bibliotecas são copiadas para o executável.

No primeiro caso, o programa utilizará bibliotecas de forma compartilhada e ficará dependente delas para funcionar.

Este esquema economiza recursos, pois uma biblioteca utilizada por muitos programas precisa ser carregada somente uma vez na memória. O tamanho do executável será pequeno.

No segundo caso, o programa é independente, uma vez que as funções que necessita estão no seu código.

Este esquema permite que quando houver uma mudança de versão de biblioteca o programa não será afetado. A desvantagem será o tamanho do executável e necessidades de mais recursos.

Internamente a etapa de ligação é bem complexa, mas o GCC faz isto de forma transparente através do comando:

# gcc teste.o –o teste

O resultado será um executável chamado teste.

# ./teste

Certificação Linux!

Este programa foi ligado às bibliotecas de forma compartilhada. O comando ldd informa quais são as bibliotecas ligadas ao executável:

# ldd teste

linux-vdso.so.1 =>  (0x00007ffd69cff000)

libc.so.6 => /lib64/libc.so.6 (0x00007f8ee7991000)

/lib64/ld-linux-x86-64.so.2 (0x00007f8ee7d5e000)

As bibliotecas libc.so.6 e ld-linux.so.2 são referenciadas dinamicamente pelo programa teste.

A opção “-static” do compilador copia as funções externas das bibliotecas para o executável:

# gcc –static teste.c –o teste1

O comando ldd informará que o executável teste1 não tem ligações dinâmicas:

# ldd teste1

not a dynamic executable

A diferença no tamanho dos executáveis é muito grande. O programa teste que utiliza ligações dinâmicas tem 8.279 bytes.

O teste1 tem 1.884.604 bytes. Portanto, somente utilize a cópia das bibliotecas para o executável se for estritamente necessário.

Compilando do jeito certo

Uma vez que o software foi baixado da Internet e extraído o conteúdo de seu pacote, é hora de compilar o código fonte.

É muito comum os desenvolvedores ao criar seu software e distribuir seu código fonte, incorporar dois arquivos especiais que facilitam muito a compilação dos programas: configure e Makefile.

configure

O “configure” é um script que o desenvolvedor cria para checar se a máquina tem todos os requisitos necessários para a compilação do software. Isto geralmente envolve checar se existe o compilador necessário e se todas as bibliotecas necessárias estão instaladas.

O configure também permite que o usuário habilite ou desabilite alguma função do software no momento da compilação.

Pode-se ver as opções que o configure permite com a opção “–help”:

$ ./configure --help

Uma vez que o configure é executado, ele faz toda a checagem e programas, bibliotecas e dependências para compilar o software.

$ ./configure

Ele criará um arquivo especial chamado Makefile, que contém as diretivas de compilação do software. 

Se houver alguma problema ou falta de alguma dependência, o configure irá alertar o usuário, para que esta dependência seja satisfeita, e então o configure possa ser executado novamente, até que o Makefile seja gerado.

~/httpd-2.4.41$ ./configure 
checking for chosen layout... Apache
checking for working mkdir -p... yes
configure: Configuring Apache Portable Runtime library...
checking for APR... no
configure: error: APR not found.  Please read the documentation.

Makefile

O Makefile é um arquivo na forma de um script, que contém os comandos para compilar o software, customizados para a máquina em questão, com as opções que o configure habilitou ou desabilitou.

Para que o software seja compilado, é necessário o utilitário make para ler o conteúdo do Makefile, e disparar o processo de compilação do software.

make

O utilitário make é necessário para compilarmos múltiplos arquivos de código fonte de um projeto. Ele utiliza um arquivo de descrição geralmente nomeado como Makefile. O conteúdo deste arquivo contém regras que definem as dependências entre arquivos fonte e os comandos necessários para a compilação.

A partir deste arquivo de descrição ele cria seqüências de comandos que são interpretados pelo shell. Geralmente o compilador gcc é invocado com diversas opções que completam as dependências de outros arquivos objetos e bibliotecas.

Mesmo os menores projetos de software contêm vários arquivos que tem uma interdependência e o comando make e o Makefile facilitam muito o processo de compilar software.

Para compilar o software, simplesmente digite make no diretório corrente do projeto de software:

$ make

Desta forma, o make irá ler o Makefile e fazer todo o processo de compilação do software.

É possível no momento da compilação acontecerem erros, principalmente de falta de bibliotecas ou problemas na versão das bibliotecas, que não foram previstos pelo desenvolvedor ao se criar o configure. 

Uma vez compilado o software, pode-se usar a diretiva “install” do make para instalar o software recém compilado nos diretórios apropriados no Linux:

$ make install

Feito isso, o software será devidamente instalado no sistema.

Os programas que são construídos desta forma geralmente foram empacotados usando um conjunto de programas referidos como autotools. Esta suíte inclui autoconf, automake e muitos outros programas, todos eles trabalham juntos para tornar a vida de um mantenedor de software significativamente mais fácil. O usuário final não vê essas ferramentas, mas eles eliminam a dor de configurar um processo de instalação que será executado de forma consistente em diferentes distribuições Linux.

Aprenda muito mais sobre Linux em nosso curso online. Você pode efetuar a matrícula aqui. Se você já tem uma conta, ou quer criar uma, basta entrar ou criar seu usuário aqui.

Gostou? Compartilhe