Desmistificando toolchains em Linux embarcado

- por Sergio Prado

Categorias: Linux Tags: , , ,

Toolchain e cross-compiling toolchain são nomes que podem confundir e até assustar algumas pessoas. Já vi gente confundindo toolchain com buildsystem e compilador. Os conceitos estão relacionados, mas não são a mesma coisa. É por isso que resolvi escrever este artigo.

TOOLCHAIN?

Ao pé da letra, e traduzindo literalmente, toolchain é uma “corrente de ferramentas”. Na prática, é um conjunto de ferramentas de compilação.

Você se lembra do processo de compilação de um código em C?

É mais ou menos assim:

  1. Pré-processador: trata todas as diretivas de pré-processamento e gera um código C intermediário.
  2. Compilador: converte este código C intermediário em um código-fonte assembly.
  3. Assembler: converte o código-fonte assembly em arquivo objeto.
  4. Linker: Converte um ou mais arquivos objeto no binário final (firmware, aplicação, etc).

Cada uma destas etapas é executada por uma ferramenta, todas elas fazendo parte do toolchain. Perceba como as ferramentas estão interligadas e são executadas uma após a outra. Entendeu agora o porque do “chain” no nome?

TIPOS DE TOOLCHAIN

Existem basicamente dois tipos de toolchain:

  1. Native toolchain
  2. Cross-compiling toolchain

O conceito é simples. Dá uma olhada na imagem abaixo:


O bloco de cima é sua máquina de desenvolvimento e o bloco de baixo é a arquitetura-alvo (hardware, kit de desenvolvimento, etc). As cores representam a arquitetura: azul para x86 e laranja para ARM.

Portanto, você irá usar um native toolchain quando quiser compilar uma aplicação para a mesma arquitetura da sua máquina de desenvolvimento. Na prática, esta aplicação irá rodar na sua máquina de desenvolvimento (x86 no nosso exemplo).

Já um cross-compiling toolchain vai gerar um binário para uma arquitetura diferente da sua máquina de desenvolvimento. No exemplo acima, a máquina de desenvolvimento é x86 e a arquitetura-alvo é ARM.

PORQUE USAR UM CROSS-COMPILING TOOLCHAIN?

Por que não usamos um native toolchain para desenvolver em Linux embarcado? Simplesmente porque na grande maioria dos casos não dá.

Para isso, precisaríamos colocar todo o nosso toolchain dentro do dispositivo. Um toolchain ocupa bastante espaço, de 50MB a 100MB. E se a sua memória flash tiver só 64M? Um toolchain precisa também de capacidade de processamento. Já pensou compilar o kernel dentro do seu kit de desenvolvimento? Iríamos tomar muito mais café neste caso! :)

É por isso que precisamos de um cross-compiling toolchain para desenvolvimento em Linux embarcado.

MISTURANDO NOMES

Use o nome que achar mais bonito: native toolchain ou toolchain nativo, cross-compiling toolchain, toolchain de cross-compilação ou toolchain de compilação-cruzada.

A partir de agora, e visando a lei do menor esforço :=), usarei apenas toolchain quando quiser expressar cross-compiling toolchain, ou serei mais explícito quando necessário.

OS COMPONENTES DO TOOLCHAIN

Um toolchain para desenvolvimento em Linux (embarcado ou não) possui 5 componentes principais:

1. Compilador GCC

O famoso compilador GNU C, compatível também com diversas outras linguagens como C++ e Java, e capaz de gerar código para diversas arquiteturas incluindo ARM, AVR, Blackfin, MIPS, PowerPC e x86.

2. Binutils

Um conjunto de ferramentas para manipular binários para arquiteturas específicas. Por exemplo, possui o “as” para ler um código-fonte assembly e gerar um arquivo-objeto e o “ld” para linkar um ou mais arquivos-objeto em um executável.

3. Biblioteca C padrão

A principal função da biblioteca C padrão é fazer a interface com o kernel através de chamadas do sistema (System Calls).

Sabe quando você usa as funções open() ou write() no código C? A implementação desta função esta na biblioteca C, que gera uma chamada de sistema para o kernel. 

E quando você tenta compilar este código, o toolchain irá linkar o código da biblioteca com o código da sua aplicação (estaticamente ou dinamicamente). Por este motivo, o toolchain contém a biblioteca C padrão compilada para sua arquitetura-alvo.

E é por este mesmo motivo que, em Linux embarcado, nem toda aplicação compilada para ARM e linkada dinamicamente vai rodar em qualquer dispositivo ARM. Neste caso, a biblioteca C padrão do dispositivo precisa ser a mesma do toolchain. Se você quiser que a aplicação rode em qualquer dispositivo ARM compatível, compile estaticamente.

Em Linux, existem diversas implementações de biblioteca C, dentre elas a glibc (padrão em sistemas desktop) e a uClibc (padrão em Linux embarcado). Apesar de apresentarem a mesma interface (API) com as aplicações, a uClibc é otimizada para gerar um código menor e a glibc é otimizada para ter melhor performance:


Veja que um simples printf() compilado estaticamente com a glibc gerou um código de 472K, enquanto que na uClibc o código ficou com apenas 18K!

4. Kernel Headers

Vimos que um toolchain contém (compilada) a biblioteca C padrão do sistema. Isso significa que, quando o toolchain é gerado, ele precisa compilar a biblioteca e transformá-la em arquivo-objeto (*.so ou *.a) para poder ser usada (linkada) pelas aplicações.

Mas para compilar a biblioteca C padrão, existe uma dependência adicional: os headers do kernel! Por quê?


A biblioteca C padrão conversa com o kernel através de chamadas de sistema. Mas como a biblioteca C sabe quais são as chamadas de sistema disponíveis? Estão nos headers do kernel!

Definição das chamadas de sistema para uma arquitetura ARM dentro dos fontes do kernel, em “arch/arm/include/asm/unistd.h“:

1
2
3
4
5
6
7
8
9
10
...
#define __NR_SYSCALL_BASE       0
 
#define __NR_exit               (__NR_SYSCALL_BASE+  1)
#define __NR_fork               (__NR_SYSCALL_BASE+  2)
#define __NR_read               (__NR_SYSCALL_BASE+  3)
#define __NR_write              (__NR_SYSCALL_BASE+  4)
#define __NR_open               (__NR_SYSCALL_BASE+  5)
#define __NR_close              (__NR_SYSCALL_BASE+  6)
...

Além disso, a biblioteca C padrão precisa ter acesso às constantes e estruturas de dados do kernel. Exemplos:

1
2
3
4
/* em linux/fcntl.h */
...
#define O_RDWR          00000002
...
1
2
3
4
5
6
/* em asm/stat.h */
...
struct stat {
        unsigned long  st_dev;
        unsigned long  st_ino;
...

Portanto, e por estes motivos, a biblioteca C depende dos headers do kernel. Isso significa que um toolchain depende também da versão do kernel do Linux. 

Neste caso, se você usar um toolchain com os headers do kernel 2.6.32, terá problemas para gerar uma aplicação para ser executada no kernel 2.6.33? Não necessariamente! Em 99,9999% dos casos, irá funcionar normalmente. Isso porque os desenvolvedores do kernel são bem conservadores. As chamadas do sistema nunca são modificadas. Pode-se incluir alguma nova chamada, mas as existentes continuam sempre as mesmas. Isso garante compatibilidade de aplicações e bibliotecas com diferentes versões do kernel do Linux.

5. GDB

O GDB é o debugger padrão em sistemas Linux. Como ele também depende das bibliotecas do sistema, faz parte do toolchain. Mas como não precisamos dele para compilar as aplicações, é apenas um elemento opcional.

NA PRÁTICA

Na prática, um toolchain nativo é composto pelas ferramentas de compilação que usamos na máquina de desenvolvimento, como o gcc, as, ld, strip e gdb.

Já um cross-compiling toolchain acrescenta normalmente um prefixo ao nome destas ferramentas para indicar a arquitetura e outras informações adicionais. Por exemplo, o compilador gcc de um toolchain para sistemas Linux de arquitetura ARM pode se chamar “arm-linux-gcc“. Veja a listagem completa do diretório de ferramentas de um toolchain para ARM:

$ ls
arm-linux-addr2line  arm-linux-c++      arm-linux-cpp      arm-linux-gcc        arm-linux-gcov   arm-linux-ld.bfd    arm-linux-nm       arm-linux-ranlib   arm-linux-strings
arm-linux-ar         arm-linux-cc       arm-linux-elfedit  arm-linux-gcc-4.3.5  arm-linux-gprof  arm-linux-ldconfig  arm-linux-objcopy  arm-linux-readelf  arm-linux-strip
arm-linux-as         arm-linux-c++filt  arm-linux-g++      arm-linux-gccbug     arm-linux-ld     arm-linux-ldd       arm-linux-objdump  arm-linux-size

É isso aí. Espero ter desmistificado um pouco o conceito de toolchain para Linux embarcado.

No próximo artigo, iremos aprender a gerar nosso próprio toolchain e usá-lo para cross-compilar qualquer aplicação para Linux embarcado.

Até lá!

Um abraço,

Sergio Prado

  •   Mais um excelent post! Obrigado por compartilhar Sérgio!

      Abraços e sucesso!

      Marcelo Jo

  • Alberto Fabiano

    Da série, não confunda "conhaque de alcatrão" com "catraca de canhão", este foi um artigo simples e direto.
    Maestro, uma nota: "sensacional"! :-)

  • Marcos de Lima Carlos

    Poxa… tá aí uma coisa que eu nunca tinha visto alguém falar de forma tão clara! Show! Parabéns.

  • Klaus da Cruz

    Um execelente Post Sergio…. Está de uma forma bem clara a passagem da informação..
    Congratularions..

  • Boa explicação, mas para SW embarcado precisa em geral também do método de gravação. Em geral JTAG + software adequado. Já que a explicação é boa, vale a pena acrescentar isso :)

  • José Antônio

    Olá Sérgio!
    Ótimo artigo, mas acho que ficaria melhor se incluísse o que
    seria o buildsystem e o porquê de tanta gente confundir este
    termo com toolchain.
     
    Atenciosamente,

    • Boa observação José!

      Um buildsystem (ou sistema de build) é uma ferramenta para gerar uma distribuição Linux completa, que além de ser capaz de gerar o toolchain, também é capaz de gerar o bootloader, o kernel e o sistema de arquivos! Ou seja, o toolchain é apenas um dos componentes do buildsystem.

      Um abraço!

  • Leo

    Voce explica de forma muito clara, parabens pelo artigo.
    Gostaria de deixar a sugestao que seja feito um artigo para quem quer se aventurar em portar dispositivos winCe para android, assim como faz o pessoal da xda-developers.

    • Obrigado Leandro!

      Sugestão anotada.

      Um abraço.

  • Leandro Martins

    Sérgio,  Muito obrigado pela sua contribuição.
    Sua linguagem clara e objetiva, de quem tem interesse em ensinar e paixão pela coisa deixa tudo mais facil de ser entendido.
    Estou me divertindo bastante.
    Porém, estou esperando um artigo que vc disse que seria o próximo. :-(
    No próximo artigo, vou falar sobre como cross-compilar qualquer aplicação open source para o Android. Você poderá inclusive testar esta aplicação cross-compilada no seu smartphone, se tiver acesso de root. Até lá!
    Um abraço,
    Sergio Prado
    Abraços e parabéns pelo trabalho.

    • Opa Leandro!

      Não esqueci deste artigo não! :)

      Quando comecei a escrevê-lo, percebi que seria legal ter um artigo sobre toolchains antes de falar sobre como cross-compilar para o Android. Mas ele deve sair em breve!

      Um abraço.

  • Thiago

    excelente e muito esclarecedor artigo. parabens.

  • william gueiros

    parabens pelo post muito profissional…
    e aprendi bastante com o mesmo

  • Perdomo

    Olá Sergio. Estou estudando o funcionamento do OpenWrt para meu projeto final de curso e seu artigo foi muito útil para meu aprendizado.
    Obrigado.

  • Rafael

    Muito bom post. Como sempre, escrito de forma clara e objetiva. Estou começando agora a estudar Linux embarcado e seguidamente consulto o blog.
    Parabéns e muito obrigado!

  • Olá Sérgio,
    Parabéns pelo post.. No trabalho o pessoal costumava chamar de toolchain todo o buildsystem, seu post esclareceu muitos pontos.
    Gostaria de saber se você me indica algum conteúdo sobre  ramdisk. Preciso gerar um ramdisk para o mini2440 e não sei qual o melhor caminho.
    Mais uma vez, parabéns!

  • Leonardo

    Sergio,
       Parabéns pelo Otimo Artigo.

  • Felipe

    Cara perfeito o teu post!!! Muito Obrigado!

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.