Linker scripts: controlando o layout de memória

1. Introdução ao Linker e ao Linker Script

O processo de compilação de um programa em C envolve quatro etapas principais: pré-processador, compilador, montador (assembler) e linker. Enquanto o compilador gera código objeto com endereços relativos, o linker é responsável por resolver símbolos e gerar o executável final com endereços absolutos. O linker script é o arquivo de configuração que controla exatamente como essa resolução ocorre.

Para desenvolvedores de sistemas embarcados, bootloaders ou kernels, o linker script é essencial. Ele permite definir onde cada segmento do programa será carregado na memória física, otimizar o uso de recursos limitados e criar segmentos personalizados para requisitos específicos de hardware.

2. Estrutura Básica de um Linker Script

Um linker script do GNU ld possui comandos globais e seções principais. Os comandos mais importantes são:

  • ENTRY: define o ponto de entrada do programa
  • OUTPUT_FORMAT: especifica o formato do arquivo de saída (ELF, binary, etc.)
  • OUTPUT_ARCH: define a arquitetura alvo
  • MEMORY: declara as regiões de memória disponíveis
  • SECTIONS: mapeia as seções de entrada para as regiões de memória

Exemplo mínimo:

ENTRY(_start)

OUTPUT_FORMAT(elf32-littlearm)
OUTPUT_ARCH(arm)

SECTIONS
{
    . = 0x00000000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) }
}

3. A Seção MEMORY: Definindo Regiões de Memória

A seção MEMORY declara regiões físicas disponíveis no hardware. Cada região possui nome, endereço inicial, tamanho e atributos de acesso.

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

Atributos de acesso:
- r (read) - região legível
- w (write) - região gravável
- x (execute) - região executável
- a (allocatable) - região alocável

Para um microcontrolador típico, a Flash armazena o código e constantes, enquanto a RAM contém dados variáveis e pilha.

4. A Seção SECTIONS: Controlando o Posicionamento dos Segmentos

A seção SECTIONS define como as seções de entrada (.text, .data, .bss) são organizadas nas regiões de memória. Comandos importantes incluem ALIGN, AT (para carregamento em endereço diferente) e KEEP (para evitar que o linker remova seções).

Exemplo prático posicionando .text na Flash e .data + .bss na RAM:

SECTIONS
{
    . = ORIGIN(FLASH);

    .text : {
        *(.text)
        *(.text.*)
        *(.rodata)
        *(.rodata.*)
        . = ALIGN(4);
        _etext = .;
    } > FLASH

    . = ORIGIN(RAM);

    .data : AT(LOADADDR(.text) + SIZEOF(.text)) {
        _sdata = .;
        *(.data)
        *(.data.*)
        . = ALIGN(4);
        _edata = .;
    } > RAM

    .bss : {
        _sbss = .;
        *(.bss)
        *(.bss.*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    } > RAM

    . = ALIGN(4);
    _end = .;
}

Note o uso de AT() para indicar que .data é carregado na Flash mas executado na RAM. O símbolo _etext marca o fim do código, e _sdata/_edata delimitam a seção de dados inicializados.

5. Inicialização de Dados e Seções Especiais

No código de inicialização (crt0), é necessário copiar .data da ROM para a RAM e zerar .bss. O linker script fornece os símbolos necessários:

extern uint32_t _sdata, _edata, _sbss, _ebss;
extern uint32_t _etext;

void _start(void) {
    // Copiar .data da Flash para RAM
    uint32_t *src = &_etext;
    uint32_t *dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;

    // Zerar .bss
    for (dst = &_sbss; dst < &_ebss; *dst++ = 0);

    // Chamar main()
    main();
}

Seções customizadas como .noinit (dados que persistem após reset) podem ser adicionadas:

.noinit (NOLOAD) : {
    *(.noinit)
    *(.noinit.*)
} > RAM

A diretiva NOLOAD impede que o linker tente inicializar essa seção.

6. Símbolos e Expressões no Linker Script

Símbolos podem ser declarados com PROVIDE (define se não existir) ou HIDDEN (não exportado). Expressões aritméticas permitem cálculos dinâmicos:

PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM));
PROVIDE(__heap_start = _end);
PROVIDE(__heap_size = __stack_top - __heap_start);

ASSERT(__heap_size > 0, "Heap size must be positive");

Funções especiais incluem:
- ADDR(section): endereço virtual da seção
- LOADADDR(section): endereço de carga da seção
- SIZEOF(section): tamanho da seção
- ABSOLUTE(expr): força expressão a ser absoluta

Exemplo para cálculo de heap:

__heap_start = _end;
__heap_end = __stack_top - 1024; /* reservar 1KB para pilha */
__heap_size = __heap_end - __heap_start;

7. Técnicas Avançadas e Casos de Uso

Overlays: permitem que múltiplas seções compartilhem o mesmo espaço de endereço, carregadas sob demanda:

OVERLAY : {
    .overlay1 { *(.overlay1) }
    .overlay2 { *(.overlay2) }
} > RAM

Múltiplas memórias: sistemas com TCM (Tightly Coupled Memory) e SDRAM:

MEMORY
{
    ITCM (rx)  : ORIGIN = 0x00000000, LENGTH = 16K
    DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 16K
    SDRAM (rwx): ORIGIN = 0x70000000, LENGTH = 64M
}

SECTIONS
{
    .text : { *(.text) } > ITCM
    .data : { *(.data) } > DTCM
    .heap : { *(.heap) } > SDRAM
}

Debugging: para inspecionar o layout final, use:

arm-none-eabi-objdump -h programa.elf
arm-none-eabi-readelf -S programa.elf

O mapa de símbolos gerado pelo linker (-Map=programa.map) mostra endereços e tamanhos detalhados.

Armadilhas comuns:
- Alinhamento incorreto: usar ALIGN(4) para alinhamento word
- Seções órfãs: verificar se todas as seções de entrada têm mapeamento
- Conflitos de símbolos: usar PROVIDE com cuidado

8. Conclusão e Boas Práticas

Os comandos essenciais do linker script incluem MEMORY, SECTIONS, ENTRY, PROVIDE e ASSERT. Para criar um linker script funcional, siga este checklist:

  1. Defina regiões de memória com MEMORY
  2. Mapeie .text e .rodata para memória executável
  3. Configure .data com AT() para cópia ROM→RAM
  4. Defina .bss sem inicialização
  5. Declare símbolos para o código de inicialização
  6. Inclua seções customizadas conforme necessário
  7. Use ASSERT para validar tamanhos
  8. Gere mapa de símbolos para verificação

A integração com ferramentas como Makefile ou CMake é direta:

# Makefile
LDFLAGS = -T linker.ld -Map=output.map

Dominar linker scripts é fundamental para projetos embarcados profissionais, permitindo controle preciso sobre o uso de memória e a inicialização do sistema.

Referências