C Linker Distribuição
C

Bibliotecas: Estáticas vs Dinâmicas

Quando o teu programa usa código de terceiros — funções matemáticas da libc, compressão com zlib, criptografia com OpenSSL — esse código chega sob a forma de uma biblioteca. A decisão entre biblioteca estática e dinâmica determina se o código da biblioteca é copiado para dentro do teu executável ou apenas referenciado por ele. Esta decisão afecta o tamanho do binário, a gestão de actualizações de segurança, e o processo de distribuição — os mesmos trade-offs que encontras quando decides entre um JAR fat/thin ou um instalador nativo em Java.

O Que é uma Biblioteca

Uma biblioteca é um conjunto de ficheiros objecto .o empacotados num único ficheiro, disponibilizando funções reutilizáveis para outros programas. A libc — a biblioteca standard de C — é ela própria uma biblioteca que fornece printf, malloc, fopen e centenas de outras funções que todos os programas C usam.

// Extensões por plataforma:

// Linux:
// .a   — biblioteca estática (archive)
// .so  — biblioteca dinâmica (shared object)

// Windows:
// .lib — biblioteca estática
// .dll — biblioteca dinâmica (dynamic-link library)

// macOS:
// .a   — biblioteca estática
// .dylib — biblioteca dinâmica (dynamic library)

// Exemplos reais no sistema Linux:
// /usr/lib/x86_64-linux-gnu/libm.a       — libc matemática, versão estática
// /usr/lib/x86_64-linux-gnu/libm.so.6    — libc matemática, versão dinâmica
// /usr/lib/x86_64-linux-gnu/libssl.a     — OpenSSL, versão estática
// /usr/lib/x86_64-linux-gnu/libssl.so.3  — OpenSSL, versão dinâmica

Bibliotecas Estáticas (.a / .lib)

Uma biblioteca estática é um simples arquivo — um contentor de ficheiros .o criado com o comando ar. Quando o linker usa uma biblioteca estática, copia os ficheiros .o necessários directamente para dentro do executável final. O executável resultante é autónomo — não tem dependências externas em runtime.

// Criar uma biblioteca estática:

// 1. Compilar os ficheiros .c para .o (sem linking)
gcc -O2 -Wall -c account.c  -o account.o
gcc -O2 -Wall -c database.c -o database.o
gcc -O2 -Wall -c utils.c    -o utils.o

// 2. Empacotar os .o num arquivo .a com ar (archiver)
ar rcs libaccount.a account.o database.o utils.o
//  r = inserir/substituir ficheiros no arquivo
//  c = criar o arquivo se não existir
//  s = gerar índice de símbolos (necessário para o linker)

// 3. Inspecionar o conteúdo do arquivo:
ar t libaccount.a       // listar ficheiros: account.o database.o utils.o
nm libaccount.a         // listar todos os símbolos exportados

// Usar a biblioteca estática no linking:
gcc main.o -L. -laccount -o programa
//          ^^ procurar bibliotecas no directório actual
//             ^^^^^^^^^ ligar libaccount.a (convenção: -l remove o "lib" e a extensão)

// Equivalente explícito:
gcc main.o libaccount.a -o programa

// O linker copia apenas os .o que contêm símbolos usados:
// Se main.o usa account_create() e account_deposit() (definidos em account.o)
// mas não usa nada de utils.o — utils.o NÃO é copiado para o executável
// A biblioteca estática é um menu — o linker escolhe apenas o que precisa
// Verificar que o executável é autónomo (sem dependências dinâmicas):
gcc main.o -static -laccount -lm -lc -o programa_estatico

ldd programa_estatico
// → not a dynamic executable   ← autónomo, sem dependências

ldd programa_dinamico           // versão dinâmica para comparação
// linux-vdso.so.1 (0x00007ffd...)
// libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6
// libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6

// Comparação de tamanho:
ls -lh programa_estatico programa_dinamico
// programa_estatico:  2.1 MB  ← libc e libm embutidas
// programa_dinamico:  18 KB   ← apenas o código do programa

Bibliotecas Dinâmicas (.so / .dll)

Uma biblioteca dinâmica não é copiada para o executável — o executável apenas guarda uma referência ao nome da biblioteca e aos símbolos que usa. O sistema operativo carrega a biblioteca em memória em runtime, quando o programa inicia (ou quando a função é chamada, no caso de carregamento explícito). Múltiplos programas que usam a mesma biblioteca partilham uma única cópia em memória.

// Criar uma biblioteca dinâmica:

// -fPIC = Position Independent Code — obrigatório para .so
// O código gerado usa endereçamento relativo — pode ser carregado em qualquer endereço
gcc -O2 -Wall -fPIC -c account.c  -o account.o
gcc -O2 -Wall -fPIC -c database.c -o database.o

// Criar o .so com -shared:
gcc -shared account.o database.o -o libaccount.so.1.0.0
//  ^^^^^^^ produz biblioteca dinâmica
//                                   ^^^^^^^^^^^^^^^ nome com versão major.minor.patch

// Convenção de nomes em Linux:
// libNOME.so.MAJOR.MINOR.PATCH    — nome real com versão completa
// libNOME.so.MAJOR                — soname (link simbólico) — usado pelo linker em runtime
// libNOME.so                      — link de desenvolvimento — usado pelo linker em compile time

// Criar os links simbólicos:
ldconfig -n .                           // cria libaccount.so.1 → libaccount.so.1.0.0
ln -s libaccount.so.1 libaccount.so     // link de desenvolvimento

// Ligar ao executável usando a biblioteca dinâmica:
gcc main.o -L. -laccount -Wl,-rpath,/usr/local/lib -o programa
//                        ^^^^^^^^^^^^^^^^^^^^^^^ onde procurar a .so em runtime

// Instalar a biblioteca no sistema:
sudo cp libaccount.so.1.0.0 /usr/local/lib/
sudo ldconfig                    // actualiza cache do dynamic linker
// Carregamento explícito em runtime — dlopen/dlsym (Linux) / LoadLibrary (Windows):
// Permite carregar uma biblioteca em runtime sem a conhecer em compile time
// Base de sistemas de plugins

#include <dlfcn.h>

int main() {
    // Carregar a biblioteca em runtime:
    void* handle = dlopen("libaccount.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "Erro: %s\n", dlerror());
        return 1;
    }

    // Obter ponteiro para uma função pelo nome:
    typedef Account* (*CreateFn)(int, double);
    CreateFn account_create = (CreateFn) dlsym(handle, "account_create");

    const char* error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym erro: %s\n", error);
        dlclose(handle);
        return 1;
    }

    // Usar a função carregada dinamicamente:
    Account* acc = account_create(99, 1500.0);

    // Fechar a biblioteca quando já não é necessária:
    dlclose(handle);
    return 0;
}

// Este padrão é a base de:
// - Sistemas de plugins (carregar extensões sem recompilar)
// - Loaders de drivers (OpenGL, Vulkan, ODBC)
// - Hot-reloading em desenvolvimento (recarregar código modificado sem reiniciar)

Comparação Directa

DimensãoEstática (.a)Dinâmica (.so / .dll)
Tamanho do executável Grande — código da biblioteca embutido Pequeno — apenas referências
Dependências em runtime Nenhuma — executável autónomo Requer bibliotecas instaladas no sistema
Actualizações de segurança ❌ Requer recompilação e redistribuição do executável ✅ Actualizar a .so corrige todos os programas que a usam
Partilha em memória Cada processo tem a sua cópia Uma cópia partilhada por todos os processos
Compatibilidade de versão ✅ Sem conflitos — cada executável usa a versão com que foi compilado ❌ DLL Hell — versões incompatíveis no mesmo sistema
Tempo de startup Ligeiramente mais rápido — sem resolução de símbolos Ligeiramente mais lento — dynamic linker resolve símbolos no arranque
Plugins / extensões ❌ Impossível — o código está fixo no executável ✅ dlopen/dlsym permitem carregar código em runtime
Caso de uso típico Distribuição de binários standalone, containers, firmware Sistemas operativos, aplicações desktop, servidores com plugins

DLL Hell — o Problema de Versões Dinâmicas

O maior problema das bibliotecas dinâmicas é a gestão de versões — quando diferentes programas no mesmo sistema requerem versões incompatíveis da mesma biblioteca. Este problema, chamado DLL Hell no ecossistema Windows, é a razão directa pela qual Docker existe e pela qual o modelo fat JAR se tornou popular em Java.

// O problema DLL Hell:

// Sistema tem: libssl.so.1.0.2 (versão antiga)
// Programa A requer: libssl.so.1.0.2 — funciona ✅
// Programa B requer: libssl.so.3 (nova API incompatível) — falha ❌

// Soluções ao longo da história:

// ── Linux: soname e versionamento semântico ──────────────────────────────────
// libssl.so.1.0.2 e libssl.so.3 podem coexistir — nomes diferentes
// O soname (libssl.so.MAJOR) é o que o executável referencia
// Múltiplas versões MAJOR podem coexistir no mesmo sistema
ls /usr/lib/x86_64-linux-gnu/libssl*
// libssl.so.1.1      → libssl.so.1.1.1n
// libssl.so.3        → libssl.so.3.0.2
// Ambas coexistem — programas ligam à versão que precisam

// ── Windows: application-local DLLs ──────────────────────────────────────────
// Colocar a .dll na mesma pasta que o executável
// O Windows procura DLLs primeiro na pasta do executável — isolamento por programa

// ── Containers Docker — isolamento total ─────────────────────────────────────
// Cada container tem o seu próprio sistema de ficheiros — as suas próprias .so
// Programa A em container com libssl 1.0.2
// Programa B em container com libssl 3.0
// Sem conflito — sistemas de ficheiros completamente isolados

// ── Static linking — eliminar a dependência ──────────────────────────────────
// Embutir a biblioteca no executável — sem dependências externas
// Custo: executável maior, actualizações de segurança requerem redistribuição

A Analogia com o Ecossistema Java

Os mesmos trade-offs de estático vs dinâmico existem em Java, mas com uma camada de abstracção diferente. Em vez de .a e .so, o ecossistema Java usa JARs, fat JARs, e instaladores nativos — os mesmos conceitos com nomes diferentes.

// Correspondência C ↔ Java:

// ── Biblioteca estática (.a) ↔ Fat JAR / Uber JAR ────────────────────────────
// C: o código da biblioteca é copiado para o executável
// Java: todas as dependências (JARs de terceiros) são descomprimidas e
//       reempacotadas num único JAR autónomo

// Maven Shade Plugin — cria fat JAR:
// <plugin>
//   <groupId>org.apache.maven.plugins</groupId>
//   <artifactId>maven-shade-plugin</artifactId>
// </plugin>
// resultado: app-1.0-shaded.jar (contém Spring Boot, Jackson, Hibernate, ...)
// tamanho típico: 50-100 MB — todas as dependências embutidas
// vantagem: java -jar app.jar funciona em qualquer máquina com JVM

// Spring Boot fat JAR:
java -jar app.jar    // autónomo — sem classpath manual, sem dependências externas
                     // análogo a: executável C compilado com -static

// ── Biblioteca dinâmica (.so) ↔ Thin JAR + classpath ─────────────────────────
// C: o executável referencia bibliotecas instaladas no sistema
// Java: o JAR referencia dependências no classpath — resolvidas em runtime

java -cp app.jar:lib/spring-6.0.jar:lib/jackson-2.15.jar com.randomt.Main
// análogo a: gcc main.o -lspring -ljackson -o programa (com as .so no sistema)

// ── Plugins dlopen ↔ ClassLoader dinâmico ────────────────────────────────────
// C: dlopen("plugin.so") carrega código em runtime
// Java: URLClassLoader carrega JARs em runtime — base de sistemas de plugins

URLClassLoader loader = new URLClassLoader(
    new URL[]{ new URL("file:plugin.jar") },
    getClass().getClassLoader()
);
Class<?> pluginClass = loader.loadClass("com.plugin.MyPlugin");
// Análogo a: dlopen("plugin.so") + dlsym(handle, "create_plugin")

// ── DLL Hell ↔ Dependency Hell (conflitos de versão Maven) ───────────────────
// C: programa A precisa libssl.so.1, programa B precisa libssl.so.3 — conflito
// Java: módulo A precisa jackson-2.12, módulo B precisa jackson-2.15 — conflito
//       Maven usa a versão "mais próxima da raiz" da árvore de dependências
//       resultado pode não satisfazer ambos os módulos
// Solução Java: fat JAR isola dependências (como containers isolam .so)

// ── GraalVM Native Image — volta às raízes de C ──────────────────────────────
// Compila Java + todas as dependências para um executável nativo
// Análogo ao -static de C: tudo embutido, sem JVM em runtime
// Resultado: binário de ~20-50 MB, startup em <100ms (vs 2-5s do fat JAR)
// Limitação: reflexão e carregamento dinâmico de classes requer configuração extra

Criação e Distribuição Prática

O processo completo de criar e distribuir uma biblioteca C — desde os ficheiros fonte até à instalação no sistema — segue uma estrutura convencional que o ecossistema de pacotes Linux (apt, rpm) automatiza.

// Estrutura de um projecto de biblioteca C para distribuição:

// libaccount/
// ├── include/
// │   └── account.h        ← headers públicos (instalados em /usr/include/)
// ├── src/
// │   ├── account.c
// │   ├── database.c
// │   └── utils.c
// ├── tests/
// │   └── test_account.c
// └── Makefile

# Makefile para biblioteca com instalação:
PREFIX      = /usr/local
INCLUDEDIR  = $(PREFIX)/include
LIBDIR      = $(PREFIX)/lib

LIB_NAME    = account
LIB_MAJOR   = 1
LIB_MINOR   = 0
LIB_PATCH   = 0

STATIC_LIB  = lib$(LIB_NAME).a
SHARED_LIB  = lib$(LIB_NAME).so.$(LIB_MAJOR).$(LIB_MINOR).$(LIB_PATCH)
SONAME      = lib$(LIB_NAME).so.$(LIB_MAJOR)

SRCS        = src/account.c src/database.c src/utils.c
OBJS_STATIC = $(SRCS:.c=_s.o)   # objectos para biblioteca estática
OBJS_SHARED = $(SRCS:.c=_d.o)   # objectos para biblioteca dinâmica (-fPIC)

# Compilar objectos para biblioteca estática (sem -fPIC)
%_s.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Compilar objectos para biblioteca dinâmica (com -fPIC)
%_d.o: %.c
	$(CC) $(CFLAGS) -fPIC -c $< -o $@

# Criar biblioteca estática
$(STATIC_LIB): $(OBJS_STATIC)
	ar rcs $@ $^

# Criar biblioteca dinâmica
$(SHARED_LIB): $(OBJS_SHARED)
	$(CC) -shared -Wl,-soname,$(SONAME) $^ -o $@

# Instalar no sistema
install: $(STATIC_LIB) $(SHARED_LIB)
	install -d $(INCLUDEDIR) $(LIBDIR)
	install -m 644 include/account.h $(INCLUDEDIR)/
	install -m 644 $(STATIC_LIB) $(LIBDIR)/
	install -m 755 $(SHARED_LIB) $(LIBDIR)/
	ln -sf $(SHARED_LIB) $(LIBDIR)/$(SONAME)
	ln -sf $(SONAME) $(LIBDIR)/lib$(LIB_NAME).so
	ldconfig    # actualiza cache do dynamic linker

# Desinstalar
uninstall:
	rm -f $(INCLUDEDIR)/account.h
	rm -f $(LIBDIR)/$(STATIC_LIB)
	rm -f $(LIBDIR)/$(SHARED_LIB)
	rm -f $(LIBDIR)/$(SONAME)
	rm -f $(LIBDIR)/lib$(LIB_NAME).so
	ldconfig

Checklist Bibliotecas Estáticas vs Dinâmicas