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

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