GNU Cross-toolchain - Processo de build

Introdução

 

O início da carreira de um desenvolvedor de software embarcado é marcado com cicatrizes de guerra. Uma delas é a definição de um microcontrolador para um novo projeto e outra é a escolha do ambiente de desenvolvimento. A escolha de ambos é feita quase que em conjunto, já que as ferramentas de desenvolvimento devem dar suporte ao microcontrolador especificado. Então…qual microcontrolador usar? Qual compilador deve ser escolhido? Qual é a melhor IDE? Qual a ferramenta de depuração que melhor atende o projeto? Essas são questões que atormentam os desenvolvedores, já que eles devem justificar e, às vezes, assumir os custos do projeto.

 

 

Objetivos

 

As questões levantadas anteriormente são melhor entendidas e respondidas se o desenvolvedor possuir o conhecimento básico e comum do funcionamento dessas ferramentas de desenvolvimento. Visando esse conhecimento, este post explica o processo de build de um ambiente de desenvolvimento usando GNU Development Tools, para sistemas embarcados sem sistema operacional, conhecidos como bare-metal, e que usem as linguagens C, C++ e Assembly.

 

 

Composição do ambiente de desenvolvimento

 

Um ambiente de desenvolvimento é composto de uma máquina host, onde o desenvolvimento de uma aplicação é realizado, e um dispositivo target, onde o binário da aplicação é executado. O desenvolvimento de uma aplicação que deve ser executada no próprio ambiente de desenvolvimento é chamado de desenvolvimento nativo, ao passo que o desenvolvimento de uma aplicação que deve ser executada numa plataforma diferente é chamado de desenvolvimento cross-platform.

 

Mas o que é uma plataforma diferente? Deve ser considerado tanto a arquitetura utilizada, ou seja, o core do microcontrolador/microprocessador, quanto o sistema operacional que oferece o ambiente de runtime para a aplicação final. Quando é utilizado um RTOS, geralmente os seus arquivos-fonte ou arquivos-objeto são utilizados para a geração da imagem final do firmware que deve ser gravado em memória não-volátil. Portanto, o RTOS, quando utilizado dessa maneira, não influencia na distinção da plataforma. No entanto, quando um sistema operacional é utilizado, tal como o Linux ou o Windows, o mesmo é tomado como parâmetro da plataforma.

 

O desenvolvimento nativo, quando realizado num PC, por sua natureza, pode esconder alguns aspectos do processo de build de um software, ao passo que o desenvolvimento cross-platform para sistemas embarcados obriga o desenvolvedor a conhecer o hardware utilizado e fornecer instruções mais detalhadas às ferramentas de desenvolvimento.

 

Como exemplos de desenvolvimento cross-platform considere:

  • Um host com um microprocessador x86 e que gera binários para um target com a arquitetura ARM Cortex A8, independente dos sistemas operacionais que são utilizados por ambas as partes;
  • Um host e um target que possuem a mesma arquitetura (x86, por exemplo), mas os sistemas operacionais que são utilizados em ambos ambientes são diferentes (Windows e Linux, por exemplo).

 

Em grande parte dos sistemas embarcados atuais não são utilizados sistemas operacionais, o que torna a distinção de plataformas a cargo somente das arquiteturas em questão.

 

Mas como são gerados os binários para o ambiente target no ambiente host?

 

 

Composição do GNU Toolchain

 

Dado que deve ser realizado um desenvolvimento cross-platform, é necessário fazer uso de um conjunto de ferramentas específico para isso, ao qual é dado o nome de cross-toolchain. Tais ferramentas trabalham em conjunto, de forma sequencial e de modo que a saída de uma é usada como a entrada da próxima, motivo pelo qual o nome toolchain foi dado. A composição do GNU cross-toolchain é dada por:

 

GCC (GNU C/C++ Compiler)

 

É o compilador responsável por converter arquivos-fonte escritos em linguagens de alto nível, tais como C, C++, Java e outras, em arquivos-objeto que contêm tanto código de máquina quanto dados de programa. Ele oferece muitas opções de linha de comando e extensões de sintaxe, além de proporcionar um front-end para o GNU Linker, ld.

 

O gcc gera os arquivos binários no formato ELF, mas suporta outros formatos, como, por exemplo, COFF. Cada arquivo-objeto contém, em forma de seções, as informações pertinentes ao correspondente arquivo-fonte. A divisão ocorre da seguinte forma: todos os blocos de código são agrupados na seção .text; todas as variáveis globais inicializadas e seus valores iniciais são reunidos na seção .data; e as variáveis globais não-inicializadas são agrupadas na seção .bss. Outras seções são criadas de acordo com as estruturas e linguagem utilizados, assunto sobre o qual pretendo escrever um novo post futuramente.

 

Basicamente, o gcc pode gerar os três tipos de arquivos-objeto estipulados pelo padrão ELF:

  • Relocatable file: é um arquivo-objeto que contém seções de código e de dados que pode ser usado para link-edição em conjunto com outros arquivos-objeto, a fim de que ou uma imagem executável ou um shared object seja criado. Possui referências não-resolvidas, não podendo ser executado;
  • Executable file: é um arquivo-objeto pronto para execução, que também contém seções de código e de dados. Num sistema bare-metal, o arquivo não possui referências não-resolvidas, possuindo somente endereços absolutos de execução;
  • Shared object file: é um arquivo-objeto tal como um arquivo relocatable, no entanto este pode ser utilizado ou para a criação de novos arquivos-objeto ou para a execução de uma instância de um processo no sistema operacional, esta gerenciada por um linker/loader dinâmico. Portanto, tal arquivo não tem utilidade para um sistema bare-metal.

 

GNU Binutils

 

É um conjunto de programas para criação, manipulação e análise de arquivos binários. Desse pacote de ferramentas pode-se citar ldasobjdumpobjcopynmreadelfstripstringsgprof, etc.

 

O GNU Linkerld, é responsável por combinar diversos arquivos-objeto num único arquivo, podendo esse ser relocatable, shared object ou executable. Tal processo é guiado por um arquivo especial, o liker command file, o qual instrui o linker em como combinar os arquivos-objeto e em que posições de memória inserir tanto o código quanto os dados da imagem final.

 

O GNU Assembleras, é o Assembler responsável por gerar um arquivo-objeto relocatable a partir de um arquivo-fonte em Assembly.

 

Biblioteca C/C++

 

É a biblioteca padrão das linguagens C/C++, cuja escolha deve ser feita no momento da geração do cross-toolchain, já que o compilador gcc é compilado contra uma biblioteca C específica. Existem diversas implementações da biblioteca C, tais como glibc, uClibc, EGLIBC, diet libc, Newlib e klibc. Para sistemas bare-metal, a biblioteca mais utilizada é a Newlib, ao passo que, para sistemas que usam o Linux como sistema operacional, três implementações são as mais utilizadas: a glibc para plataforma PC Desktop; e uClibc e EGLIBC para sistemas embarcados (Embedded Linux).

 

Essa biblioteca é conhecida como runtime library, já que dá suporte à execução de uma aplicação em C/C++. Ela é implementada em dois grandes módulos: um que é a implementação do padrão ANSI C e outro que é dependente do sistema operacional utilizado.

 

Dentre as funções que são independentes de SO, que são parte do padrão ANSI C, pode-se citar funções de manipulação de strings, de cópia de memória, de comparação de blocos de memória, etc.

 

Já as funções que são dependentes do SO utilizado são chamadas de system calls. Essas funções são interfaces que, nos casos do sistema respeitar o padrão POSIX.1 (também conhecido como IEEE 1003.1), são implementadas pelo sistema operacional utilizado. As funções de gerenciamento de sistemas de arquivos, I/O, gerenciamento de memória, gerenciamento de processos, etc, são exemplos de funções que fazem parte desse grupo (write, read, open, close, sbrk, fork, getpid, execve, etc.).

 

E quando não é utilizado um sistema operacional? Como não existe um módulo em específico que implementa tais funções, elas devem ser implementadas pelo desenvolvedor. A biblioteca Newlib oferece, para algumas dessas funções, stubs, os quais são implementações mínimas necessárias para que uma aplicação possa ser simplesmente link-editada com a biblioteca libc.a. Para outras, como sbrk, uma implementação mínima e funcional é entregue.

 

Essas funções de suporte podem ser reimplementadas pelo desenvolvedor a fim de que essa interface seja realizada por meio de um agente de debug, instalado na máquina host. Esse tipo de implementação é chamado de semihosting, e é mostrada na Figura 1. Além disso, tais funções podem ser implementadas de acordo com o hardware disponível, como, por exemplo, envio/recepção de dados pela porta serial, como mostrado na Figura 2.

 

Estrutura da biblioteca C
Figura 1 – Estrutura da biblioteca C
“Retargeting” da biblioteca C
Figura 2 – “Retargeting” da biblioteca C

 

Um simples RTOS pode oferecer implementações de parte da biblioteca de runtime, quando necessário, tal como o gerenciamento de memória. O FreeRTOS, por exemplo, possui uma implementação desse tipo.

 

GNU Build System

 

São ferramentas utilizadas para gerenciar pacotes de código-fonte e facilitar o processo de compilação de uma aplicação, tais como GNU Make, Autotools, etc.

 

GDB (GNU Debugger)

 

É o software depurador do toolchain GNU, que é constituído de um front-end instalado na máquina host, o qual comunica-se com um back-end no sistema target por meio um canal de comunicação tal como serial ou Ethernet.

 

Dado que os módulos que compõem um cross-toolchain foram resumidamente apresentados, como eles interagem entre si?

 

 

Processo de build

 

Agora que sabemos qual é a composição do GNU cross-toolchain, vem a pergunta? Como criar uma imagem executável que possa ser gravada na memória não-volátil (ROM, Flash, etc) de um sistema embarcado, dado o código da aplicação? Qual é a sequência de uso dessas ferramentas? O fluxo de build de uma aplicação é mostrado na Figura 3.

 

Processo de build em sistemas embarcados
Figura 3 – Processo de build em sistemas embarcados. Fonte: Livro "Real-Time Concepts for Embedded Systems", capítulo 2

 

Em linhas gerais, o desenvolvedor precisa ter em mãos os seguintes arquivos para gerar a imagem executável final:

 

Aplicação

 

Consiste dos arquivos nas linguagens C, C++ ou Assembly (.c, .cpp, .asm, .s, .h, .hpp, etc), os quais devem ser traduzidos para a linguagem de máquina correspondente (.o).

 

Bibliotecas

 

Caso haja necessidade, o desenvolvedor pode oferecer bibliotecas estáticas (.a) para serem compiladas na imagem final. Bibliotecas dinâmicas não podem ser utilizadas, visto que não é utilizado um sistema operacional embarcado que ofereça um linker dinâmico.

 

Vetor de interrupções

 

Como está sendo desenvolvido um software para um microcontrolador, deve-se inicializar o seu vetor de interrupções, o qual é uma sequência de código que deve ser inserida numa região específica da memória do dispositivo. Dependendo de qual microcontrolador é utilizado, esse código pode ser escrito tanto em Assembly quanto em C.

 

Arquivo de startup

 

Quando é escrito um código usando uma linguagem de alto nível, tal como C e C++, deve ser oferecido um ambiente de execução para que essa linguagem possa ser acomodada. Cada linguagem de alto nível possui seu próprio conjunto de suposições com relação ao ambiente de execução. Para que essas expectativas sejam atendidas, é necessário desenvolver um arquivo de inicialização do sistema, o famigerado arquivo de startup, cujas responsabilidades são:

 

  1. Inicializar a região de dados inicializados, .data: copiar os dados armazenados em ROM para a RAM;
  2. Inicializar a região de dados não-inicializados, .bss: zerar o conteúdo dessa região;
  3. Alocar espaço para a pilha (stack) e inicializar seu conteúdo;
  4. Inicializar o valor do registrador de stack pointer do microcontrolador;
  5. Alocar espaço para a região de heap;
  6. Executar o método construtor e inicializar a vtable de cada instância global (para C++ somente) e;
  7. Executar a função global main().

 

Deve ser lembrado que o vetor de interrupções, em alguns casos, pode ser inicializado pelo código de startup, em seu primeiro passo. No caso de microcontroladores baseados no core ARM Cortex-M3/4, o vetor de interrupções é um simples array em C, cujo arquivo binário é alocado pelo linker na posição correta, e que inicializa a pilha principal do core.

 

De forma genérica, a Figura 3 detalha a sequência dos passos necessários para a geração de um binário, seja ele executableshared object ou relocatable. Dentro do ambiente GNU, existe uma aplicação muito útil, uma espécie de canivete suíço, que realiza todas as etapas desse processo: gcc (GNU C/C++ Compiler).

 

A aplicação gcc atua como um front-end para todas as etapas de build: pré-processamento, compilação C, assembler e link-edição. Esse binário chama os aplicativos necessários para cada fase. Na etapa de pré-processamento, são analisados os arquivos-fonte e os arquivos-cabeçalho por meio do executável cpp. Assim, todos os arquivos-cabeçalho são agrupados, todas as macros são expandidas e todas as diretivas para o pré-processador são processadas. Em seguida, a compilação dos arquivos C, efetivamente, é realizada pelo executável cc1, o qual produz o código Assembly correspondente ao módulo compilado. Na sequência, o executável as é chamado, o qual converte o código em Assembly, gerado anteriormente, em arquivo-objeto, contendo código de máquina. Por fim, os arquivos-objeto são agrupados e uma imagem executável é gerada pelo linker, papel realizado pelo executável collect2.

 

O simples comando abaixo gera todos os passos listados acima:

 

Tanto os executáveis cc1 e collect2 são parte da distribuição do GCC. Já o executável as faz parte do GNU Binutils.

 

Pode-se visualizar a saída de cada etapa de compilação por meio de flgas de compilação do gcc, como:

  • -E: a saída do pré-processador é gerada ao invés de um arquivo-objeto;
  • -S: a saída é o arquivo C convertido em Assembly;
  • -c: a saída do compilador C, um arquivo-objeto, é gerada;
  • sem as opções acima: a link-edição é executada.

 

Se desejar, o desenvolvedor pode fazer uso de utilitários de build, tal como make, o qual necessita de um arquivo makefile para orquestrar a geração do binário final, tendo como entrada todos os arquivos-fonte do projeto e suas bibliotecas.

 

 

Conclusões

 

Entender o funcionamento das ferramentas de desenvolvimento, sejam elas GNU ou não, pode ajudar na escolha da melhor alternativa de toolchain para um projeto. E saber lidar com uma ferramenta como o GCC pode lhes livrar de enormes dores de cabeça, não concordam? Vocês leitores usam tais ferramentas em seus projetos? Ou preferem as proprietárias?

 

 

Referências

 

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0205f/Bgbjjgij.html

http://infocenter.arm.com/help/topic/com.arm.doc.dui0471c/DUI0471C_developing_for_arm_processors.pdf

Especificação ELF

Newlib

Padrão POSIX.1

Livro "Real-Time Concepts for Embedded Systems", Qing Li and Carolyn Yao, CMP Books© 2003

 

Artigos da série GNU ARM Cross-toolchain:

 

GNU ARM Cross-toolchain - Compilação e OpenOCD

GNU ARM Cross-toolchain – Configurando stack e heap

GNU ARM Cross-toolchain – OpenOCD + GDB

GNU ARM Cross-toolchain - FreeRTOS + GCC + STM32F4Discovery - Parte 1

GNU ARM Cross-toolchain - FreeRTOS + GCC + STM32F4Discovery - Parte 2

GNU ARM Cross-toolchain – Eclipse + FreeRTOS + GCC - Parte 1

GNU ARM Cross-toolchain – Eclipse + FreeRTOS + GCC - Parte 2