paulo1205
(usa Ubuntu)
Enviado em 07/11/2016 - 17:09h
Usar biblioteca e cabeçalhos tem como objetivo poupar tempo, evitando ter de refazer esforço que já foi feito.
O meio para a concretização desse objetivo é a compilação em separado das partes que compõem um programa. Você só compila partes novas ou partes que sofreram alterações. Depois de compiladas, essas partes são ligadas (
linked ) com outras partes já compiladas.
Para tanto, a técnica que você, programador, utiliza é de informar ao compilador os nomes e os tipos de dados dos objetos (variáveis e funções) cuja implementação está fora da parte do programa que está sendo compilada naquele momento. Sabendo o nome e o tipo, o compilador saberá como usar (e como não deixar usar) tais objetos, mesmo sem conhecer detalhes de sua implementação.
Um dos usos de cabeçalhos (
headers ), que geralmente (mas não necessariamente) residem em arquivos (que, no C, têm sufixo “.h”; em C++ o uso de sufixo nos cabeçalhos padronizados foi suprimido justamente para evitar confusões com cabeçalhos do C), é justamente informar ao compilador os nomes e os tipos de objetos cuja implementação pode estar num outro local. Esse local não é o próprio cabeçalho: no cabeçalho fica apenas a informação de nome e forma.
O produto final da compilação não é um programa executável, mas sim arquivos contendo o que se chama
código objeto . O código objeto já tem um bocado de código nativo do processador, mas os endereços das funções e de variáveis globais ainda não estão completamente determinados: as referências a elas continuam amarradas apenas pelos seus nomes, e essa indeterminação vale tanto para as variáveis e funções que você define quanto para aquelas que você invoca a partir de outras fontes. Os endereços finais desses objetos só são completamente determinados pelo programa que faz a “ligação” (
link ) de um ou mais arquivos de código objeto (por isso o nome do programa que executa essa função é
linker ).
Além dos arquivos que você compilou, podem entrar na composição do executável final outros arquivos com código objeto que acompanham o compilador ou o próprio sistema operacional. Frequentemente, os compiladores e sistemas trazem uma coleção de códigos objetos empacotada em apenas um ou num pequeno número de arquivos, e tais coleções recebem o nome de bibliotecas. Bibliotecas podem — e geralmente devem, pelo menos aquelas que são mais importantes — ser passadas ao linker juntamente com os arquivos de código objeto produzidos por você, a fim de serem ligadas no mesmo arquivo executável(*).
Bibliotecas acabam trabalhando em conjunto com cabeçalhos: os cabeçalhos apresentam ao compilador quais são os símbolos e quais formas eles têm, e as bibliotecas trazem a implementação funcional desses símbolos.
Geralmente, cada biblioteca que você utiliza traz junto consigo um ou mais cabeçalhos. Por exemplo, a biblioteca padrão do C apresenta vários cabeçalhos que declaram os objetos nela contidos, classificados por assunto. O programa abaixo ilustra isso.
// Divide o intervalo [0; 2*PI) em um número aleatório de faixas, e imprime o cosseno de cada faixa
#include <stddef.h> // definições de uso geral (e.g. NULL)
#include <stdio.h> // operações de entrada e saída (e.g. printf())
#include <stdlib.h> // funções de uso geral (e.g. RAND_MAX, srand() e rand())
#include <math.h> // matemática de ponto flutuante (e.g. M_PI e cos())
#include <time.h> // funções ligadas a relógio e tempos (e.g. time())
int main(void){
int n, i;
srand(time(NULL));
n=1+(int)(359.0*rand()/RAND_MAX);
for(i=0; i<n; i++)
printf("cos(%d*PI/%d)=%0.15f\n", 2*i, n, cos(2*i*M_PI/n));
return 0;
}
Todos os símbolos acima, exceto
main () e as variáveis locais nela contidas, são definidos pelo padrão como parte da biblioteca padrão do C. Alguns dos símbolos são constantes que existem na forma de macros (e.g.
NULL ,
RAND_MAX e
M_PI ), e só são conhecidos pelo compilador por nome numa etapa bem inicial da compilação, chamada preprocessamento, tendo seu valor imediatamente substituído durante a compilação. As variáveis locais automáticas dentro de
main () também são resolvidas durante a compilação, mas os demais símbolos globais, incluindo
main , são levados como símbolos não-resolvidos (
unresolved ) para o código objeto, e só assumem valores fixos após o linker ser invocado.
Se você salvar o programa acima como
prog.c e o compilar com a seguinte linha de comando
gcc prog.c -o prog -lm já percebe logo de cara que uma parte das funções da biblioteca padrão não reside junto com as demais. No mundo UNIX, por razões históricas, as funções matemáticas de ponto flutuante residem numa biblioteca separada, a chamada
libm . Para invocá-la através do GCC, você tem de especificar a opção
-lm , que informa ao linker a necessidade de incluir a biblioteca matemática.
Outra coisa que você pode notar é que o GCC cuidou de chamar o linker para você, e ainda cuidou de invocar a biblioteca que contém as demais funções usadas pelo seu programa. Isso é uma conveniência oferecida pelo GCC, que quebra o galho muitas vezes com programas pequenos do dia-a-dia.
No entanto, quando você tem programas de maior porte, você geralmente vai trabalhar com ele dividindo-o em vários módulos menores, e vai querer usar compilação separada de cada um desses módulos. Para comunicar um módulo com outros, você provavelmente vai criar seus próprios cabeçalhos. Além disso, você pode querer ter mais controle sobre as opções passadas ao linker, até para poder fazer a ligação dos módulos já compilados de uma maneira ordenada.
---
Para dar uma ideia de como a coisa realmente funciona, vou mostrar o que a interface conveniente do GCC faz por trás das cortinas.
A primeira coisa é gerar um arquivo com o código objeto do
prog.c . Para você fazer isso manualmente, você pode fazer o seguinte.
gcc -c prog.c
Iso vai produzir um arquivo
prog.o (ou
prog.OBJ , se você estiver no Windows). Esse arquivo contém código objeto do seu programa. Note que não foi necessário especificar a biblioteca matemática agora, porque o linker não foi invocado: a única coisa que nós fizemos foi gerar o código objeto do seu programa.
Como o seu programa só tem um arquivo, não temos mais código objeto para gerar. Então podemos chamar o linker.
A pergunta é: o que tem de ser ligado junto com o seu programa para que ele funcione?
A resposta é um pouco complicada, e pode variar de acordo com cada tipo de sistema operacional ou ambiente de execução. No entanto, pelo menos uma parte da resposta é óbvia: você vai ter de incluir todas as bibliotecas que possuírem as implementações das funções usadas pelo seu programa. No caso do Linux, você já sabe que as funções matemáticas de ponto flutuante estão na
libm . O restante da biblioteca padrão do C tem suas funções (ou pelo menos a maioria delas) na
libc .
Mas existem mais dependências, pelo menos no caso do Linux.
O padrão do C e de sua biblioteca especifica ações que devem ser tomadas antes de um programa em C começar a executar e depois que ele sinaliza que quer terminar a execução (i.e. quando se chama
exit () ou se executa o comando
return dentro de
main ()). No caso do Linux e alguns outros sistemas
à la UNIX, essas coisas residem em arquivos objetos separados da
libc , mas tais arquivos têm de entrar na composição do executável, logo têm de ser informados ao linker.
No fim das contas, para invocar o linker a fim de produzir o executável para o programa acima usando bibliotecas dinâmicas, eis o que teríamos de fazer (numa máquina com Ubuntu 14.04).
ld \
-dynamic-linker /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \
/usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o \
prog.o -lc -lm \
/usr/lib/x86_64-linux-gnu/crtn.o \
-o prog
Você precisa informar ao linker:
• como chamar o programa que faz a resolução final dos últimos símbolos em tempo de execução (“
-dynamic-linker /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ”);
• a lista dos arquivos de código objeto que inicializam o programa, antes de
main () ser invocada (“
/usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o ”);
• o código objeto do seu programa (no seu caso, apenas “
prog.o ”, mas poderia ser uma lista com múltiplos arquivos);
• as bibliotecas das quais ele depende (“
-lc -lm ”);
• o código objeto que faz a finalização após o encerramento de
main () (“
/usr/lib/x86_64-linux-gnu/crtn.o ”)); e
• por fim, o nome do executável a ser produzido como saída do processo (“
-o prog ”).
Já uma versão estática (todas as bibliotecas embutidas dentro do executável), seria produzida com o seguinte comando.
ld -static \
/usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o \
/usr/lib/gcc/x86_64-linux-gnu/4.8/crtbeginT.o \
prog.o -lc -lm \
-L /usr/lib/gcc/x86_64-linux-gnu/4.8 -lgcc -lgcc_eh -lc \
/usr/lib/gcc/x86_64-linux-gnu/4.8/crtend.o \
/usr/lib/x86_64-linux-gnu/crtn.o \
-o prog
Em relação à versão dinâmica, você pode notar que saiu a indicação do programa que carrega as bibliotecas compartilhadas, mas entraram outras coisas:
• por conta da unificação de funções de I/O entre C e C++, você acaba sendo obrigado a incluir objetos e bibliotecas do C++ (
crtbeginT.o ,
libgcc ,
libgcc_eh e
crtend.o );
• a libc (“
-lc ”) foi listada duas vezes, porque a ordem das bibliotecas importa para o comando
ld , de modo que toda vez que algum objeto faz referência a um símbolo definido em outro módulo, você deve incluir novamente o objeto em que o símbolo é definido.
_________________________________
(*) Em alguns sistemas, incluindo Linux e Windows, a produção do executável pode não ser realmente a
última etapa do processo. Nesses sistemas, com o intuito de economizar espaço em disco e espaço na memória RAM, os programas podem compartilhar trechos de código executável. Tal compartilhamento se faz sobretudo com bibliotecas do sistema. Para usar as bibliotecas compartilhadas, o linker embute no executável instruções que informam ao sistema que a ligação final de alguns dos símbolos deve ser feita no momento em que o programa for executado, e aponta os nomes dos arquivos em que tais símbolos residem. Na hora em que o executável é chamado, o sistema localiza tais instruções e verifica se os arquivos referidos já foram carregados para a memória por algum outro programa. Se não tiverem sido, o sistema primeiro os carrega, e mapeia os endereços em que residem cada um dos símbolos. Dali para frente, cada novo programa que fizer referência a essas bibliotecas vai aproveitar o mapeamento já feito, resolvendo a referência aos símbolos mapeados no momento em que esses programa forem executados.