Introdução: Os arquitetos invisíveis do software moderno
No desenvolvimento diário, compilamos código com comandos como:
| |
Frequentemente chamamos esse processo de “compilação”, mas o compilador realiza apenas metade da jornada. A outra metade—costurar dezenas de arquivos-objeto e bibliotecas, unificar endereços e carregar o binário na memória para a CPU executar—é realizada pelo Linker (Ligador) e pelo Loader (Carregador).
flowchart TD
subgraph BuildPipeline ["Do código-fonte à execução"]
SourceA["main.c"] -->|Compilador| ObjA["main.o (Objeto realocável)"]
SourceB["utils.c"] -->|Compilador| ObjB["utils.o (Objeto realocável)"]
Lib["libc.so / libm.a\n(Bibliotecas)"]
ObjA --> Linker["★ Linker (ld)\n[Resolução de símbolos & Realocação]"]
ObjB --> Linker
Lib --> Linker
Linker --> Exec["myprogram (Binário executável ELF)"]
end
Exec -->|execve| Loader["★ Loader (Kernel & ld-linux.so)\n[Mapeamento em memória & Ligação dinâmica]"]
Loader --> CPU["Execução pela CPU (_start -> main)"]
style BuildPipeline fill:#f9f9f9,stroke:#333,stroke-width:2px
style Linker fill:#ffe6e6,stroke:#d9534f,stroke-width:2px
style Loader fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
Sem o linker, seríamos forçados a escrever milhões de linhas de código em um único arquivo gigantesco.
1. O formato ELF: Anatomia de um arquivo-objeto
Arquivos intermediários (.o) são arquivos-objeto realocáveis no formato ELF (Executable and Linkable Format):
.text: Instruções em linguagem de máquina..rodata: Constantes e cadeias de caracteres somente-leitura..data: Variáveis globais e estáticas inicializadas..bss: Variáveis globais não-inicializadas. Ocupa zero bytes no disco! O espaço é alocado e zerado apenas em tempo de execução..symtab: Tabela de símbolos (índice de funções e variáveis)..rela.text: Registros de realocação com endereços provisórios a serem corrigidos.
2. Resolução de símbolos (Symbol Resolution)
O linker emparelha chamadas de funções e variáveis externas com suas definições concretas.
Símbolos Fortes vs. Fracos:
- Forte: Funções e variáveis globais inicializadas (
int x = 1;). - Fraco: Variáveis globais não-inicializadas (
int x;).
Regras:
- Dois símbolos fortes iguais causam erro de compilação (
multiple definition). - Símbolo forte sempre tem precedência sobre fraco.
- Entre múltiplos símbolos fracos, o linker escolhe um arbitrariamente (risco grave de corrupção de memória).
Ordem na linha de comando: O linker varre arquivos da esquerda para a direita. Por isso, -lm deve ficar após main.c.
3. Realocação de endereços (Relocation)
O compilador usa zeros como marcadores temporários para chamadas externas:
| |
($S$: endereço do símbolo, $A$: addend, $P$: posição da instrução).
4. Ligação dinâmica: Código Independente de Posição (PIC), GOT e PLT
Para não duplicar bibliotecas na memória, surgiram as bibliotecas compartilhadas (.so).
Elas utilizam Código Independente de Posição (PIC), viabilizado por:
- GOT (Global Offset Table): Armazena endereços reais de memória.
- PLT (Procedure Linkage Table): Tabela de saltos na seção de código.
Com Lazy Binding (Ligação Tardia), o linker dinâmico (ld-linux.so) calcula o endereço da função somente na primeira vez em que ela é chamada.
5. O Loader: De _start até main()
Ao executar ./myprogram, a chamada execve mapeia os segmentos ELF na memória virtual (mmap), prepara a pilha (argc, argv, envp) e salta para o ponto de entrada _start.
A biblioteca de tempo de execução executa os construtores globais em .init_array antes de chamar a função main().
