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.
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
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
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)
| Dimensão | Está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 |
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
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
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
.a): o linker copia os .o necessários para o executável — autónomo, sem dependências em runtime..so/.dll): o executável referencia; o sistema operativo carrega em runtime — partilhada entre processos..so corrige todos os programas automaticamente.-fPIC obrigatório para compilar objectos que vão para uma .so — código independente da posição em memória.dlopen/dlsym permitem carregar bibliotecas em runtime — base de sistemas de plugins e hot-reloading.gcc -static.