Bibliotecas estáticas e dinâmicas
Esta aula explora as bibliotecas estáticas (.a) e dinâmicas (.so) na linguagem C, abordando suas diferenças, criação, uso e processo de linkedição. O aluno aprenderá a compilar e vincular bibliotecas, entenderá as vantagens de cada tipo e praticará com exercícios.
Nesta aula, vamos mergulhar no universo das bibliotecas em C, focando nas bibliotecas estáticas (.a) e dinâmicas (.so). Esses artefatos são fundamentais para organizar código reutilizável, modularizar projetos e facilitar a manutenção. Você vai entender as diferenças entre eles, como criar e usar cada tipo, e como o linkeditor (linker) trabalha com eles durante a compilação.
Dominar bibliotecas é essencial para qualquer programador C que deseja produzir software profissional, seja para sistemas embarcados, aplicações desktop ou servidores. Vamos explorar desde os conceitos básicos até a prática de criação e uso, com exemplos que você pode reproduzir em seu próprio ambiente de desenvolvimento.
Visão geral
Bibliotecas são coleções de funções e símbolos que podem ser reutilizados em diferentes programas. Elas evitam a duplicação de código e permitem que projetos grandes sejam divididos em partes menores e mais gerenciáveis. Em C, existem dois tipos principais: estáticas e dinâmicas.
Uma biblioteca estática é um arquivo com extensão .a (archive) que contém código objeto (arquivos .o) agrupados. Quando você linka um programa contra uma biblioteca estática, o código necessário é copiado diretamente para o executável final. Isso torna o programa autocontido, mas aumenta o tamanho do arquivo e dificulta a atualização da biblioteca sem recompilar o programa.
Já uma biblioteca dinâmica (ou compartilhada) tem extensão .so (shared object) no Linux, .dll no Windows, e .dylib no macOS. Ela é carregada em memória em tempo de execução, e vários programas podem compartilhar a mesma cópia da biblioteca. Isso economiza espaço em disco e memória, e permite atualizar a biblioteca sem recompilar os programas que a usam (desde que a interface permaneça compatível).
O processo de linkedição é o momento em que o compilador e o linkeditor combinam o código objeto do seu programa com as bibliotecas. Para bibliotecas estáticas, isso ocorre em tempo de compilação; para dinâmicas, em tempo de execução, com a ajuda do carregador dinâmico.
.a vs .so
A escolha entre biblioteca estática e dinâmica depende de vários fatores. Vamos comparar as principais características:
- Tamanho do executável: Com estática, o executável contém uma cópia do código da biblioteca, tornando-o maior. Com dinâmica, o executável é menor, pois o código fica em arquivo separado.
- Carregamento: Estática é carregada junto com o programa, dinâmica é carregada sob demanda, podendo até ser carregada em tempo de execução via
dlopen(). - Atualização: Para atualizar uma biblioteca estática, é preciso recompilar e relinkar o programa. Para dinâmica, basta substituir o arquivo
.so, desde que a interface seja preservada. - Dependências: Um programa com biblioteca estática não tem dependências externas, tornando-o mais portátil. Um programa com biblioteca dinâmica exige que a biblioteca esteja presente no sistema, o que pode gerar problemas de compatibilidade.
- Memória: Múltiplos programas usando a mesma biblioteca dinâmica compartilham a mesma região de memória (text), economizando RAM. Com estática, cada programa tem sua própria cópia.
- Desempenho: Bibliotecas estáticas podem ter desempenho ligeiramente melhor, pois não há overhead de resolução de símbolos em tempo de execução, mas na prática a diferença é mínima.
Em projetos pequenos ou quando você precisa de um executável único, estática é conveniente. Em sistemas onde várias aplicações usam funções comuns (como a libc), dinâmica é a escolha padrão.
Criando e usando
Vamos criar uma biblioteca simples com funções matemáticas para ilustrar o processo. Primeiro, criamos o arquivo de cabeçalho matematica.h:
#ifndef MATEMATICA_H
#define MATEMATICA_H
int soma(int a, int b);
int subtrai(int a, int b);
#endifAgora, o arquivo de implementação matematica.c:
#include "matematica.h"
int soma(int a, int b) {
return a + b;
}
int subtrai(int a, int b) {
return a - b;
}Para criar uma biblioteca estática, compilamos o arquivo .c para objeto e usamos ar para criar o arquivo .a:
gcc -c matematica.c -o matematica.o
ar rcs libmatematica.a matematica.oPara criar uma biblioteca dinâmica, usamos a flag -fPIC (Position Independent Code) e compilamos com -shared:
gcc -c -fPIC matematica.c -o matematica.o
gcc -shared -o libmatematica.so matematica.oPara usar em um programa, criamos main.c:
#include <stdio.h>
#include "matematica.h"
int main() {
int a = 10, b = 5;
printf("%d + %d = %d\n", a, b, soma(a, b));
printf("%d - %d = %d\n", a, b, subtrai(a, b));
return 0;
}Compilar com biblioteca estática:
gcc main.c -L. -lmatematica -o programa_estaticoA flag -L. indica o diretório atual para busca de bibliotecas, e -lmatematica procura por libmatematica.a (ou .so, se disponível).
Para compilar com biblioteca dinâmica, o comando é o mesmo, mas o sistema pode precisar saber onde encontrar a biblioteca em tempo de execução. Se a biblioteca não estiver em um diretório padrão, defina LD_LIBRARY_PATH:
gcc main.c -L. -lmatematica -o programa_dinamico
LD_LIBRARY_PATH=. ./programa_dinamicoVocê também pode usar ldd para ver as dependências dinâmicas do executável.
Linkedição
A linkedição é o processo de combinar código objeto e bibliotecas para gerar o executável. O linker resolve referências a símbolos (funções, variáveis) entre os arquivos. Para bibliotecas estáticas, o linker extrai apenas os objetos necessários do arquivo .a e os incorpora ao executável. Para dinâmicas, o linker registra a biblioteca como dependência e deixa a resolução para o carregador dinâmico.
Um ponto importante é a ordem das bibliotecas na linha de comando: se a biblioteca A depende de B, A deve vir antes de B. Isso porque o linker processa da esquerda para a direita, e se encontrar um símbolo não resolvido, ele procura nas bibliotecas seguintes. Se B viesse antes, o símbolo de A não seria encontrado a tempo, causando erro.
Exemplo: se você tem libA.a que usa libB.a, o comando deve ser:
gcc main.o -lA -lB -o programaPara bibliotecas dinâmicas, a ordem também importa, mas o carregador dinâmico resolve as dependências em tempo de execução, então a ordem é menos crítica, mas ainda é uma boa prática.
Outra ferramenta útil é nm para listar símbolos em um arquivo objeto ou biblioteca, e ldd para listar dependências dinâmicas de um executável.
Boas práticas
- Use bibliotecas dinâmicas para código que muda frequentemente ou é compartilhado entre várias aplicações.
- Use bibliotecas estáticas para distribuir executáveis autônomos, especialmente em sistemas embarcados.
- Mantenha a interface da biblioteca (cabeçalhos) estável para evitar quebrar programas que a usam.
- Ao criar bibliotecas dinâmicas, compile com
-fPICpara garantir que o código seja independente de posição. - Documente as funções e os cabeçalhos para facilitar o uso por outros desenvolvedores.
Referências
- GCC Link Options
- ar(1) - Linux manual page
- ld(1) - Linux manual page
- Program Library HOWTO
- ld.so(8) - Linux manual page
- Static vs. dynamic linkage (IBM)
Exercícios
- Crie uma biblioteca estática chamada
libcalculadora.acontendo as funçõesmultiplicaredividir(a divisão pode retornar float). Depois, compile um programa que a use e execute.
calculadora.h:#ifndef CALCULADORA_H
#define CALCULADORA_H
int multiplicar(int a, int b);
float dividir(int a, int b);
#endifcalculadora.c:#include "calculadora.h"
int multiplicar(int a, int b) {
return a * b;
}
float dividir(int a, int b) {
return (float)a / b;
}Compile e crie a biblioteca:gcc -c calculadora.c -o calculadora.o
ar rcs libcalculadora.a calculadora.oPrograma main.c:#include <stdio.h>
#include "calculadora.h"
int main() {
int x = 10, y = 4;
printf("%d * %d = %d\n", x, y, multiplicar(x, y));
printf("%d / %d = %.2f\n", x, y, dividir(x, y));
return 0;
}Compile e execute:gcc main.c -L. -lcalculadora -o prog
./proglibcalculadora.so). Compile o mesmo programa e execute com LD_LIBRARY_PATH.gcc -c -fPIC calculadora.c -o calculadora.o
gcc -shared -o libcalculadora.so calculadora.oCompile o programa:gcc main.c -L. -lcalculadora -o prog_din
LD_LIBRARY_PATH=. ./prog_dinnm para listar os símbolos exportados pela biblioteca estática que você criou e pela dinâmica. O que você observa?nm libcalculadora.a e nm -D libcalculadora.so. Na estática, você verá os símbolos (funções) com seus endereços. Na dinâmica, -D mostra os símbolos dinâmicos, que são aqueles exportados para outros programas. Ambos listam multiplicar e dividir, mas a dinâmica pode mostrar também símbolos de inicialização.dlopen e dlsym. Use a função soma da biblioteca libmatematica.so criada na aula.#include <stdio.h>
#include <dlfcn.h>
int main() {
void *handle = dlopen("./libmatematica.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Erro ao carregar: %s\n", dlerror());
return 1;
}
int (*soma)(int, int) = (int (*)(int, int)) dlsym(handle, "soma");
if (!soma) {
fprintf(stderr, "Símbolo não encontrado: %s\n", dlerror());
dlclose(handle);
return 1;
}
printf("3 + 4 = %d\n", soma(3, 4));
dlclose(handle);
return 0;
}Compile com -ldl: gcc prog.c -ldl -o progCom esses exercícios, você pratica a criação e o uso de bibliotecas estáticas e dinâmicas, além de explorar a linkedição em detalhes. Esses conceitos são fundamentais para projetos maiores e para entender como o sistema operacional gerencia código compartilhado.