Cross-compilation: toolchains e sysroots

1. Fundamentos da Cross-compilation

Cross-compilation é o processo de gerar código executável em uma plataforma (host) para ser executado em uma plataforma diferente (target). Diferentemente da compilação nativa, onde o código é compilado e executado no mesmo sistema, a compilação cruzada envolve arquiteturas distintas — como compilar para ARM a partir de um sistema x86_64.

A motivação principal reside na impossibilidade ou inconveniência de compilar diretamente no dispositivo alvo. Sistemas embarcados frequentemente possuem recursos limitados de CPU, memória e armazenamento, tornando inviável executar toolchains completas. Além disso, cenários como distribuição multiplataforma (Linux para ARM, RISC-V, MIPS) e manutenção de sistemas legados exigem a capacidade de gerar binários para múltiplas arquiteturas a partir de um único ambiente de desenvolvimento.

As diferenças fundamentais entre compilação nativa e cruzada incluem:
- Toolchain distinta: compilador, linker e assembler específicos para o target
- Sysroot separado: cabeçalhos e bibliotecas do sistema alvo
- Configuração explícita: flags de compilação e linkagem devem ser fornecidas manualmente

2. Anatomia de uma Toolchain

Uma toolchain de cross-compilation é composta por três componentes essenciais:
- Compilador (arm-linux-gnueabi-gcc, aarch64-linux-gnu-gcc): traduz código C para assembly da arquitetura alvo
- Assembler (arm-linux-gnueabi-as): converte assembly para código objeto
- Linker (arm-linux-gnueabi-ld): combina objetos em executáveis ou bibliotecas

Os prefixos de target seguem a convenção arch-vendor-os-abi:

arm-linux-gnueabi-     # ARM 32 bits, Linux, EABI com glibc
aarch64-linux-gnu-     # ARM 64 bits, Linux, glibc
riscv64-unknown-elf-   # RISC-V 64 bits, bare-metal (sem SO)
mipsel-linux-musl-     # MIPS little-endian, Linux, musl libc

Variantes importantes de toolchain incluem:
- Bare-metal vs. Linux: bare-metal não possui sistema operacional subjacente
- glibc vs. musl vs. uclibc: diferentes implementações da biblioteca C, com trade-offs entre compatibilidade e tamanho

3. Configuração e Uso de Toolchains

A instalação de toolchains pré-compiladas simplifica o processo. Provedores como Linaro e Bootlin oferecem toolchains prontas para ARM e AArch64. Ferramentas como crosstool-NG permitem construir toolchains customizadas.

Exemplo de instalação e uso com toolchain Linaro:

# Baixar e extrair toolchain ARM
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar xf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
export PATH=$PWD/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# Compilar um programa simples
arm-linux-gnueabihf-gcc -o hello_arm hello.c

Para compilação manual com flags do GCC:

# Compilação cruzada explícita
arm-linux-gnueabi-gcc -march=armv7-a -mfloat-abi=hard -mfpu=neon \
  --sysroot=/path/to/sysroot -o programa programa.c

Wrappers e scripts de ambiente são comuns em projetos maiores:

#!/bin/bash
export CC=arm-linux-gnueabi-gcc
export CXX=arm-linux-gnueabi-g++
export AR=arm-linux-gnueabi-ar
export LD=arm-linux-gnueabi-ld
export RANLIB=arm-linux-gnueabi-ranlib

4. Sysroots: O Sistema de Arquivos Alvo

O sysroot é uma réplica do sistema de arquivos do target, contendo cabeçalhos (/usr/include), bibliotecas compartilhadas (/usr/lib, /lib) e arquivos de configuração necessários para compilação. Sem ele, o compilador não teria acesso às definições e bibliotecas do sistema alvo.

Estrutura típica de um sysroot para Linux ARM:

sysroot/
├── usr/
│   ├── include/
│   │   ├── stdio.h
│   │   ├── stdlib.h
│   │   └── ...
│   └── lib/
│       ├── libc.so -> libc-2.31.so
│       ├── libc-2.31.so
│       ├── libm.so
│       └── ...
└── lib/
    ├── ld-linux-armhf.so.3
    └── ...

Geração de sysroot com debootstrap e qemu-debootstrap:

# Criar sysroot ARM via debootstrap com QEMU
sudo qemu-debootstrap --arch=armhf --variant=buildd \
  --include=libc6-dev bullseye /path/to/sysroot \
  http://deb.debian.org/debian

Alternativamente, usando rsync a partir de um dispositivo real:

# Copiar sysroot de dispositivo alvo
rsync -avz root@192.168.1.100:/usr/ /path/to/sysroot/usr/
rsync -avz root@192.168.1.100:/lib/ /path/to/sysroot/lib/
rsync -avz root@192.168.1.100:/etc/ /path/to/sysroot/etc/

5. Cross-compilação com Autotools e CMake

Autotools

O sistema Autotools oferece suporte nativo a cross-compilation através de variáveis:

# Configurar para cross-compilation ARM
./configure --host=arm-linux-gnueabihf \
            --build=x86_64-linux-gnu \
            --with-sysroot=/path/to/sysroot \
            PKG_CONFIG_SYSROOT_DIR=/path/to/sysroot

make

A variável --host especifica a plataforma onde o binário será executado, enquanto --build indica onde está sendo compilado.

CMake

CMake utiliza toolchain files para configurar cross-compilation:

# toolchain_arm.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Uso:

cmake -DCMAKE_TOOLCHAIN_FILE=toolchain_arm.cmake /path/to/source
make

Meson

Meson utiliza cross files:

# arm_cross.txt
[binaries]
c = 'arm-linux-gnueabihf-gcc'
cpp = 'arm-linux-gnueabihf-g++'
ar = 'arm-linux-gnueabihf-ar'
strip = 'arm-linux-gnueabihf-strip'
pkgconfig = 'arm-linux-gnueabihf-pkg-config'

[host_machine]
system = 'linux'
cpu_family = 'arm'
cpu = 'armv7hl'
endian = 'little'

[properties]
sys_root = '/path/to/sysroot'
pkg_config_libdir = '/path/to/sysroot/usr/lib/pkgconfig'

6. Resolução de Dependências e Bibliotecas

O pkg-config deve ser configurado para usar o sysroot:

export PKG_CONFIG_LIBDIR=/path/to/sysroot/usr/lib/pkgconfig
export PKG_CONFIG_SYSROOT_DIR=/path/to/sysroot

Ao compilar bibliotecas dependentes, a ordem de flags é crucial:

arm-linux-gnueabihf-gcc -o programa programa.c \
  -I/path/to/sysroot/usr/include \
  -L/path/to/sysroot/usr/lib \
  -lz -lpng \
  --sysroot=/path/to/sysroot

Bibliotecas estáticas (.a) e dinâmicas (.so) no sysroot:

# Verificar bibliotecas disponíveis no sysroot
ls /path/to/sysroot/usr/lib/*.a
ls /path/to/sysroot/usr/lib/*.so

# Forçar linkagem estática
arm-linux-gnueabihf-gcc -static -o programa programa.c

7. Depuração e Testes no Alvo

Depuração remota com GDB:

# No target (executar gdbserver)
gdbserver :2345 ./programa

# No host (conectar)
aarch64-linux-gnu-gdb programa
(gdb) target remote 192.168.1.100:2345
(gdb) continue

Execução de binários cruzados com QEMU:

# Executar binário ARM em x86_64
qemu-arm -L /path/to/sysroot ./programa

# Para AArch64
qemu-aarch64 -L /path/to/sysroot ./programa

Verificação de linked libraries:

# Listar bibliotecas dinâmicas necessárias
arm-linux-gnueabihf-readelf -d programa | grep NEEDED

# Verificar linked libraries (usando sysroot)
arm-linux-gnueabihf-ldd --root /path/to/sysroot programa

8. Boas Práticas e Armadilhas Comuns

Cuidados com endianness: arquiteturas como ARM e MIPS podem operar em little-endian ou big-endian. Verifique com:

echo | arm-linux-gnueabihf-gcc -dM -E - | grep __BYTE_ORDER__

ABI e variantes float: hard-float vs soft-float afetam a passagem de argumentos em ponto flutuante:

# Compilação hard-float (armhf)
arm-linux-gnueabihf-gcc -mfloat-abi=hard -mfpu=vfpv3-d16

# Compilação soft-float (armel)
arm-linux-gnueabi-gcc -mfloat-abi=soft

Reprodutibilidade: versionamento de toolchain e sysroot é crítico. Containers (Docker) oferecem ambientes reproduzíveis:

# Dockerfile para cross-compilation
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
    gcc-arm-linux-gnueabihf \
    libc6-dev-armhf-cross

Tamanho de tipos: sizeof(int) e sizeof(long) variam entre arquiteturas. Use tipos de tamanho fixo:

#include <stdint.h>
uint32_t valor;  // Garantido 32 bits em qualquer arquitetura

Referências