Posts com Tag ‘ambiente’

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 para o mesmo. A escolha de ambos é feita quase que em conjunto, já que as ferramentas de desenvolvimento devem dar suporte para o 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 e gerentes, já que quem deve assumir os custos são esses últimos.

Objetivos

As questões levantadas anteriormente são melhor entendidas e respondidas se o desenvolvedor possuir o conhecimento do funcionamento dessas ferramentas de desenvolvimento. Visando esse conhecimento, esse post explica o processo de build de um ambiente open-source, usando GNU Development Tools, para sistemas embarcados sem sistema operacional 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, core do microcontrolador, quanto o sistema operacional que oferece o ambiente de runtime para o software final. Quando é utilizado um RTOS, geralmente os arquivos-objeto do mesmo são utilizados na geração da imagem final do firmware que deve ser gravada na memória não volátil do projeto. 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 computador Desktop, 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 que utiliza a arquitetura x86 e que gera binários para um target que faz uso da arquitetura ARM9, 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 maneira sequencial, de forma que a saída de uma é usada como a entrada da próxima, motivo pelo qual o nome toolchain foi escolhido. A composição do GNU cross-toolchain é dada por:

– GCC (GNU C/C++ Compiler)

É o compilador responsável por converter arquivos-fonte escritos utilizando 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. Para um sistema bare-metal, o mesmo não possui referências não-resolvidas e já possui endereço absoluto 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 criação de uma instância de um processo no sistema operacional, esta por meio de um linker 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 ld, as, objdump, objcopy, nm, readelf, strip, strings, gprof, etc.

O GNU Linker, ld, é responsável por combinar diversos arquivos-objeto em um maior arquivo relocatable, um shared object ou uma imagem executável final. 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 Assembler, as, é 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, dietlibc, 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, duas implementações são as mais utilizadas: a glibc para plataforma PC Desktop e uClibc para sistemas embarcados (Embedded Linux).

Essa biblioteca é conhecida como runtime library, já que dá suporte à execução de uma aplicação em C/C++. A mesma é 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, as mesmas 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 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.

Figura 1 - Estrutura da biblioteca C

Figura 1 – Estrutura da biblioteca C

Figura 2 - "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 make, autotools, etc.

– GDB (GNU Debugger)

É o software depurador da 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 os mesmos interagem entre si?

Processo de build

Agora que sabemos qual é a composição do GNU 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 que possuo o código da minha aplicação? Qual é a sequência de uso dessas ferramentas? O fluxo de build de uma aplicação é mostrado na Figura 3.

Figura 3 – Processo de build em sistemas embarcados

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 em específico da memória do mesmo. Dependendo de qual microcontrolador é utilizado, o mesmo pode ser escrito tanto em Assembly quanto em C.

– Arquivo de startup

Quando é escrito um código usando uma linguagem de alto nível, tais como C e C++, deve ser oferecido um ambiente de execução para que tais linguagens possam ser acomodadas. 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, seguem algumas responsabilidades desse arquivo de inicialização:

1) Inicializar a região de dados inicializados (data): copiar dos dados armazenados em ROM para 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 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 e;

7) Executar a função global main().

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

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 executable, shared 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, compilação Assembly e link-edição. O mesmo chama os aplicativos necessários para cada fase. O executável gcc, na etapa de pré-processamento, analisa 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. Na sequência o executável as é chamado, o qual converte o código em Assembly gerado anteriormente e o converte para 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.

$ cross-gcc main.c startup.o vetor_de_interrupcoes.o -o main

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 opções de compilação do gcc, como:

– opção de compilação -E: a saída do pré-processador é gerada ao invés de um arquivo-objeto;

– opção de compilação -S: a saída é o arquivo C convertido em Assembly;

– opção de compilação -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.

Conclusões

Saber lidar com uma ferramenta como o GCC faz bem à saúde! 🙂

Anúncios