Featured image of post Como programas escritos separadamente se tornam um só: Os segredos de Linkers e Loaders

Como programas escritos separadamente se tornam um só: Os segredos de Linkers e Loaders

Introdução: Os arquitetos invisíveis do software moderno

No desenvolvimento diário, compilamos código com comandos como:

1
gcc -O2 main.c utils.c math.c -o myprogram

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:

  1. Dois símbolos fortes iguais causam erro de compilação (multiple definition).
  2. Símbolo forte sempre tem precedência sobre fraco.
  3. 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:

1
callq 0x00000000  # Endereço fictício
$$\text{Valor} = S + A - P$$

($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().

comments powered by Disqus