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);

#endif

Agora, 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.o

Para 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.o

Para 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_estatico

A 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_dinamico

Você 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 programa

Para 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 -fPIC para 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

Exercícios

  1. Crie uma biblioteca estática chamada libcalculadora.a contendo as funções multiplicar e dividir (a divisão pode retornar float). Depois, compile um programa que a use e execute.

✓ Resposta: Crie calculadora.h:
#ifndef CALCULADORA_H
#define CALCULADORA_H

int multiplicar(int a, int b);
float dividir(int a, int b);

#endif
calculadora.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.o
Programa 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
./prog
  • Transforme a biblioteca do exercício 1 em dinâmica (libcalculadora.so). Compile o mesmo programa e execute com LD_LIBRARY_PATH.
  • ✓ Resposta: Compile com PIC e shared:
    gcc -c -fPIC calculadora.c -o calculadora.o
    gcc -shared -o libcalculadora.so calculadora.o
    Compile o programa:
    gcc main.c -L. -lcalculadora -o prog_din
    LD_LIBRARY_PATH=. ./prog_din
  • Explique a diferença entre linkedição estática e dinâmica em termos de tempo de resolução de símbolos.
  • ✓ Resposta: Na linkedição estática, a resolução de símbolos ocorre em tempo de compilação, quando o linker copia o código da biblioteca para o executável. Na dinâmica, a resolução é adiada para o tempo de execução, quando o carregador dinâmico carrega a biblioteca na memória e resolve as referências.
  • Use o comando nm para listar os símbolos exportados pela biblioteca estática que você criou e pela dinâmica. O que você observa?
  • ✓ Resposta: Execute 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.
  • Escreva um programa que carregue uma biblioteca dinâmica em tempo de execução usando dlopen e dlsym. Use a função soma da biblioteca libmatematica.so criada na aula.
  • ✓ Resposta:
    #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 prog

    Com 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.