Mostrando postagens com marcador ELF. Mostrar todas as postagens
Mostrando postagens com marcador ELF. Mostrar todas as postagens

sexta-feira, 13 de novembro de 2015

Tornando o programa mais portável com linker script

Até agora, nosso programa tinha particularidades para o processador da placa STM32F0Discovery. As principais delas dizem respeito a mapeamento e tamanho das memórias flash e ram. A forma mais prática de se fazer isso é separando as particularidades de cada processador em um arquivo do linker: o linker script. Vamos começar com algo simples:


  1. /* Definindo as areas de memoria
  2. */
  3. MEMORY
  4. {
  5. FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  6. RAM : ORIGIN = 0x20000000, LENGTH = 8K
  7. }
  8. /* Definindo o endereco do inicio
  9. * da pilha de sistema
  10. */
  11. _stack_init = ORIGIN(RAM)+LENGTH(RAM);
  12. /* Definindo as sessoes
  13. * por enquanto temos apenas uma
  14. * sessao de interesse: a de código
  15. */
  16. SECTIONS
  17. {
  18. .text : { *(.text) } > FLASH
  19. .data : { *(.data) } > RAM
  20. }

O linker script é um arquivo texto onde definimos as particularidades que desejamos para o processo do linker. Primeiramente faremos um arquivo chamado stm32f0discovery.ld que, como o nome diz, será particular para o processador STM32F05xx da placa STM32F0Discovery. Começamos pelo comando MEMORYEsse comando dá ao linker as características dos tipos de memória que o nosso processador tem. No nosso caso, o processador STM32F05xx possui 64k KBytes de memória flash iniciando no endereço 0x08000000 e 8 KBytes de memória ram iniciando no endereço 0x20000000. Pronto: já definimos o tipo, tamanho e endereço de nossa memória física. Com isso, na linha 14, criamos um símbolo que possui, automaticamente, o endereço da pilha de sistema.

O próximo comando faz o mapeamento entre as sessões internas (definidas no programas) com as sessões externas nas áreas e endereços físicos da(s) memória(s) do processador. Temos, como padrão, as seguintes sessões:

  • .text : contém as instruções do programa a serem executadas;
  • .data : contém as áreas de memórias pré-inicializadas com valores;
  • .bss : contém as áreas de memórias não inicializadas com valores.

Por enquanto, nos preocuparemos com a sessão .text. A sessão .data está presente por ser obrigatória no processo do linker mas nós a utilizaremos efetivamente mais adiante. No nosso arquivo, começamos esse mapeamento através do comando SECTIONS. De forma simples, mapeamos a sessão .text externa para a memória flash e a sessão externa .data para a memória ram. Como o processo do linker pode unir muitos programas em um único módulo objeto (aliás essa é sua função principal), determinamos que todas as sessões .text internas (dos programas) serão alocadas na memória flash e todas as sessões .data internas para a memória ram. Isso se dá pelo caractere coringa '*' presente no comando. Em suma:

.text : { *(.text) } > FLASH

.text : a sessão .text do módulo objeto de saída
... } engloba
*(.text) todas as sessões .text dos arquivos objetos dos programas individuais
> FLASH  e residirá na memória flash

Raciocínio semelhante é usado para a sessão .data.

Completando o processo, faremos as alterações necessárias no nosso programa e no makefile.


  1. // Segundo programa para ARM Cortex-M0 configurado para linker script
  2. .thumb // Define código como THUMB
  3. .globl _start // Necessario para o linker
  4. .globl _reset_handler // visto externamente
  5. .text // Inicio da área de código
  6. _start:
  7. // Inicio do Vector Table
  8. .word _stack_init // Inicio da pilha
  9. .word _reset_handler // Endereco do Reset Handler
  10. // define o label como função para que o linker
  11. // resolva como código THUMB
  12. .weak _reset_handler // pode ser redefinido em outro modulo
  13. .type _reset_handler, function
  14. // tabela de bytes para somar 1
  15. tbl:
  16. .byte 3,7,9,0
  17. .align
  18. _reset_handler: // inicio do tratamento do reset
  19. ldr r0,=tbl // carrega r0 com o endereco da tabela
  20. mov r1,#1 // move o valor 1 para r1
  21. segue:
  22. ldrb r2,[r0] // carrega em r2 o byte enderecado por r0
  23. cmp r2,#0 // se r2 for zero,
  24. beq _stop // encerra
  25. add r3, r2, r1 // soma r1 e r2 guardando em r3
  26. add r0,#1 // avanca na tabela
  27. b segue // continua
  28. _stop:
  29. b . // loop infinito

Primeiramente, rebatizamos nosso função de tratamento de reset como _reset_handler (linha 25) e o definimos como global (linha 5) e, com a diretiva .weak (linha 18), é possível que outro módulo externo ao nosso redefina essa função. O inicio da pilha do sistema (_stack_init) agora é uma referência resolvida externamente no linker script e não mais inicializada dentro do programa.

Quanto ao makefile, retiramos os passos de geração do arquivo binário que não usaremos por agora. Além disso, retiramos o parâmetro -Ttext=0x00000000 do passo do linker substituindo-o pelo parâmetro informando nosso arquivo de linker script: -T stm32f0discovery.ld.


  1. rst_0.elf: rst_0.o
  2. @echo&echo&echo&echo&echo&echo ///////////// Executando o linker
  3. @echo
  4. arm-none-eabi-ld -o rst_0.elf rst_0.o -T stm32f0discovery.ld
  5. @echo&echo&echo&echo&echo&echo ///////////// Mostrando os simbolos apos o linker
  6. @echo
  7. arm-none-eabi-nm rst_0.elf
  8. @echo&echo&echo&echo&echo&echo ///////////// Dump das sessoes do programa
  9. @echo
  10. arm-none-eabi-objdump -h rst_0.elf
  11. rst_0.o: rst_0.s
  12. @echo&echo&echo&echo&echo&echo ///////////// Montando o programa assembly
  13. @echo
  14. arm-none-eabi-as -mcpu=cortex-m0 -g -o rst_0.o rst_0.s
  15. clean:
  16. @echo&echo&echo&echo&echo&echo ///////////// Eliminando os arquivos de saida
  17. @echo
  18. rm *.o
  19. rm *.elf

Pronto. Nosso processo de geração do módulo objeto pode ser executado:


  1. T:\arm_03>make
  2. ///////////// Montando o programa assembly
  3. arm-none-eabi-as -mcpu=cortex-m0 -g -o rst_0.o rst_0.s
  4. ///////////// Executando o linker
  5. arm-none-eabi-ld -o rst_0.elf rst_0.o -T stm32f0discovery.ld
  6. ///////////// Mostrando os simbolos apos o linker
  7. arm-none-eabi-nm rst_0.elf
  8. 0800000c W _reset_handler
  9. 20002000 A _stack_init
  10. 08000000 T _start
  11. 0800001c t _stop
  12. 08000010 t segue
  13. 08000008 t tbl
  14. ///////////// Dump das sessoes do programa
  15. arm-none-eabi-objdump -h rst_0.elf
  16. rst_0.elf: file format elf32-littlearm
  17. Sections:
  18. Idx Name Size VMA LMA File off Algn
  19. 0 .text 00000024 08000000 08000000 00008000 2**2
  20. CONTENTS, ALLOC, LOAD, READONLY, CODE
  21. 1 .ARM.attributes 00000021 00000000 00000000 00008024 2**0
  22. CONTENTS, READONLY
  23. 2 .debug_line 00000042 00000000 00000000 00008045 2**0
  24. CONTENTS, READONLY, DEBUGGING
  25. 3 .debug_info 0000003a 00000000 00000000 00008087 2**0
  26. CONTENTS, READONLY, DEBUGGING
  27. 4 .debug_abbrev 00000014 00000000 00000000 000080c1 2**0
  28. CONTENTS, READONLY, DEBUGGING
  29. 5 .debug_aranges 00000020 00000000 00000000 000080d8 2**3
  30. CONTENTS, READONLY, DEBUGGING
  31. T:\arm_03>


Vemos duas informações importantes: na linha 27 o símbolo _stack_init foi gerado pelo linker e com conteúdo correto: 0x20002000. Nas linhas 44 e 45 temos as informações por ora relevantes da nossa sessão .text do nosso módulo objeto: ela possui um tamanho de 0x24 bytes, e possui os endereços de memória VMA e LMA iguais a 0x08000000 que é o nosso endereço da memória flash. Por enquanto é o suficiente. No próximo post quando falarmos da utilização da memória ram, descreveremos as diferenças e usos dos endereços VMA e LMA.

Agora é só depurarmos da mesma forma como feito no post anterior. Boa sorte.


REFERÊNCIAS
The GNU linker - http://www.eecs.umich.edu/courses/eecs373/readings/Linker.pdf
Using AS - http://www.eecs.umich.edu/courses/eecs373/readings/Assembler.pdf

quarta-feira, 4 de novembro de 2015

Primeiro programa para a placa STM32F0Discovery

No último post, vimos a arquitetura do processador ARM Cortex, da qual destacamos o Vector Table no endereço 0x00000000. A primeira entrada aponta para o início da pilha e, a partir da segunda, temos os endereços das funções de exceção. A primeira delas é a exceção de RESET do processador. Ela é a que usaremos neste primeiro programa. Eis o código:

  1. // Primeiro programa para ARM Cortex-M0
  2. .code 16 // Define código como THUMB
  3. .globl _start // Necessario para o linker
  4. .equ STACK_INIT,0x020002000
  5. .text // Inicio da área de código
  6. _start:
  7. // Inicio do Vector Table
  8. .word STACK_INIT // Inicio da pilha
  9. .word reset_handler // Endereco do Reset Handler
  10. // define o label como função para que o linker
  11. // resolva como código THUMB
  12. .type reset_handler, function
  13. reset_handler: // inicio do tratamento do reset
  14. mov r0, #3 // carrega r0 com o valor 3
  15. mov r1, #4 // carrega r1 com o valor 4
  16. add r2, r1, r0 // soma r0 e r1 guardando em r2
  17. _stop: b . // loop infinito

Vamos aos detalhes.

Como visto em um post anterior, usaremos o conjunto de ferramentas ou toolchain da GNU portanto sua sintaxe deve ser respeitada. Há muita documentação na internet do GNU Assembly e algumas delas estão nas referências. Primeiramente, um código fonte assembly tem vários tipos de instruções:

  • Instruções de máquina do dispositivo final sendo, no nosso caso: processador ARM Cortex M0;
  • Instruções para o próprio montador (assembler);
  • Instruções para o linker;
  • Comentários.
Os comentários são iniciados por @ ou por // onde o montador ignora o resto da linha. Pode ser também por um bloco delimitado por /* e */ podendo ser de mais de uma linha inclusive.
As instruções de máquina são específicas, no nosso caso, para o processador ARM Cortex M0, se resumindo a 56 instruções as quais podem ser vistas na figura abaixo:



O formato geral da sintaxe desse montador é a seguinte:


Onde label: é um rótulo para facilitar a identificação do endereço da instrução; mnemonic é ó códico mneumonico representando a instrução conforme as tabelas acima; operad1,operad2,... são os argumentos da instrução e dependente em número e formato da instrução propriamente dita.

Finalmente, temos as instruções para o montador e para o linker, ou diretivas. Elas começam com ponto e serão vistas e explicadas ao longo do seu uso.

Voltemos então ao programa.

Na linha 3, temos a primeira diretiva: .code 16  a qual diz ao montador para usar o conjunto de instruções de 16 bits Thumb.
Na linha 4, a diretiva .globl _start diz ao linker para considerar o símbolo _start como global. Isso é necessário pois o linker obriga a existência de um símbolo _start que será definido adiante.
Na linha 6, a diretiva .equ STACK_INIT,0x020002000 defini para o montador o símbolo STACK_INIT atribuindo a ele o valor 0x020002000 que, no nosso caso, é o endereço inicial da pilha.
Na linha 8, a diretiva .text diz ao linker que, nesse endereço, inicia-se o código executável do programa o qual reside na memória flash. Em contrapartida a essa diretiva, há a diretiva .data que diz ao linker qual é o inicio da área de memória volátil para uso do programa.
Na linha 9, finalmente, o símbolo _start é definido no formato de 'label:' e refere-se ao inicio do código propriamente dito.

Uma vez definido o tipo de instrução e o endereço inicial do código, podemos começar a definir o Vector Table. Até aqui, nenhuma posição de memória foi consumida.

Na linha 13, a diretiva .word ocupa uma palavra de 32 bits e define seu conteúdo como, no caso, o valor definido no símbolo STACK_INIT. A primeira posição do Vector Table foi preenchido com o endereço inicial da pilha.
Na linha 14, a nova diretiva .word define o endereço da função de tratamento da exceção de RESET, no caso, o símbolo reset_handler que será definido adiante. O uso de símbolos permite a flexibilidade de se postergar a codificação de funções ou mesmo relocá-las de posição sem dano ao funcionamento do código.
Na linha 19, temos a diretiva .type reset_handler, function necessária para dizer ao ao linker que o endereço referenciado pelo símbolo reset_handler é uma função, ou melhor, um código Thumb. Efetivamente, essa diretiva liga o bit menos significativo do endereço referenciado por esse símbolo sempre que ele for utilizado.
A partir da linha 21, está definida a função de tratamento da exceção de RESET a partir da definição do símbolo reset_handler como label. Essa função é simples contendo apenas 4 instruções:

  • Linha 23, carrega o valor 0x03 no registrador 0;
  • Linha 24, carrega o valor 0x04 no registrador 1;
  • Linha 25, soma os valores contidos nos registradores 0 e 1 guardando o resultado no registrador 2;
  • Linha 27, entra em loop infinito desviando para o endereço da própria instrução (representado como argumento ponto).
Para evitar uma mensagem de warning no montador, o programa deve encerrar com uma linha em branco.

Uma pausa antes de começar a gerar os códigos.

O processador ARM Cortex possui registradores os quais- são áreas de 32 bits dentro do processador e são usadas para controle da aplicação, do estado do processador e para a aplicação. São eles:


Os registradores que usamos: R0, R1 e R2, são de uso geral para aplicações assim como os registradores R3 a R12. O registrador R13 contem o ponteiro pra última posição da pilha utilizada. No nosso caso, ao iniciar o processador, esse registrador terá o valor definido por STACK_INIT, ou 0x20002000. O registrador R14 possui o endereço de retorno quando o processador desvia para executar funções. Ao iniciar, possui o valor 0xffffffff. O registrador R15 ou program counter, possui o endereço da próxima instrução a ser executada. Ao iniciar, é carregado com o endereço apontado pelo conteúdo da função de tratamento de RESET do Vector Table. No nosso caso, ao iniciar, possui o endereço definido por reset_handler. Os demais registradores serão vistos em posts futuros.

Vamos executar o nosso código.

Primeiramente, salvemo o programa como rst_0.s. A extensão .s representa um programa assembly.

Para compilá-lo, executemos o comando abaixo em uma janela de linha de comando do MS-Windows:

arm-none-eabi-as -a -o rst_0.o rst_0.s

O comando arm-none-eabi-as é o assembler do nosso ferramental instalado. Como parâmetros, estamos passando -a que indica ao assembler para gerar a listagem do que ele está montando; -o rst_0.o para gerar como saída o arquivo rst_0.o e, rst_0.s indica o arquivo fonte: no nosso caso, o nosso programa. A compilação gera uma saída como a da figura abaixo:



Como pedimos para gerar listagem da montagem, algumas informações estão presentes na figura: o símbolo STACK_INIT está definido como valor o absoluto 20002000. O símbolo _start está definido para o endereço de inicio ou zero. O símbolo reset_handler está definido com o valor 00000008 que é endereço da função de tratamento de RESET que codificamos.
Podemos ver na listagem de montagem que o Vector Table está preenchido para a sua primeira entrada que é o ponteiro inicial da pilha e este está no formato little-endian (que é o default do montador). Já o endereço da função de tratamento de RESET não está preenchida e o será quando executarmos o linker com o comando abaixo:

arm-none-eabi-ld -Ttext=0x00000000 -o rst_0.elf rst_0.o

O linker é o comando arm-none-eabi-ld e recebe como parâmetros -Ttext que resolve a diretiva .text do nosso programa para o endereço 0x00000000. O parâmetro seguinte: -o rst_0.elf indica ao linker para gerar um arquivo de saída no formato elf chamado rst_0.elf. Finalmente o último parâmetro é o arquivo de entrada para o linker que é o arquivo gerado na nossa compilação: rst_0.o.

O formato elf (Executable and Linking ou Extensible Linking Format) é um formato que abriga o programa executável além dos símbolos usados em seu código fonte úteis para o processo de depuração.

Se tudo correr sem problemas, o comando acima não gera relatório.

Para vermos vermos os símbolos resolvidos pelo linker, usamos o comando:

arm-none-eabi-nm rst_0.elf

A saída é a seguinte, onde podemos ver os símbolos de nosso programas:


Para executarmos o programa no microcontrolador apenas nos interessa o código binário gerado, não os símbolos. Para tanto precisamos extrair do arquivo rst_o.elf o código binário. Fararemos isso com o comando abaixo:

arm-none-eabi-objcopy -O binary rst_0.elf rst_0.bin

O comando arm-none-eabi-objcopy é um utilitário de cópia de objetos e estamos solicitando que o formato de saída seja binário e gere, a partir do arquivo rst_0.elf, o arquivo rst_0.bin. Esse comando também não gera geratório de saída. Utilizando um editor hexadecimal para observar o arquivo de saída termos:


Podemos ver, no endereço zero, o endereço inicial da pilha (0x20002000) no formato little-endian (0x00200020). Já na no endereço 0x04, temos o endereço do reset_handler já com o bit menos significativo ligado (0x00000009) no formato little-endian também (0x09000000) indicando que é um código Thumb. A partir do endereço 0x08 temos a sequência 0x032004210A18FEE7 que é o código objeto da função reset_handler que codificamos.

Vamos agora gerar uma imagem completa da memória flash do microcontrolador presente na placa STM32F0Discovery que tem o tamanho de 16 KBytes. Para tal, usaremos dois comandos. O primeiro gera uma imagem zerada de 16 Kbytes e o segundo copia nosso arquivo binário para a posição zero dessa imagem.

dd if=/dev/zero of=flash.bin bs=4096 count=16

O comando acima copia o arquivo default de zeros binários /dev/zero para o arquivo de saída flash.bin com 16 blocos de 4096 bytes cada. Teremos um arquivo de imagem da memória flash com 64 KBytes zerados.

dd if=rst_0.bin of=flash.bin bs=4096 conv=notrunc

Finalmente, copiamos o conteúdo do arquivo binário rst_0.bin para a imagem flash com o comando acima. Será copiado um bloco de 4096 bytes e o arquivo de saída não será truncado. O resultado está abaixo:


Pronto, já temos um arquivo de imagem de memória flash para carregar no microcontrolador. No software STM32 ST-Link Utility conectemos com o microcontrolador clicando no botão .

Na sequência, escolher a função: file -> open file  e escolher nosso arquivo flash.bin.


Pronto, a imagem foi carregada no utilitário:


Para carregar na memória flash do microcontrolador o arquivo aberto, usar o botão de programação: .


O endereço de carga vem preenchido com o endereço da flash que, no caso, é 0x08000000. Já vimos anteriormente que o endereço da memória flash desse microcontrolador é mapeada nesse endereço e também no endereço 0x00000000. Podemos manter preenchido ou mudá-lo. Para efetivar a carga, clicar no botão start.

Podemos ver nosso programa mostrado na tela. Como o utilitário converte o formato little-endian para big-endian para facilitar a nossa compreensão dos endereços, o código da função reset_handler a partir da posição 0x08000008 parece estar com os bytes invertidos. Basta mudarmos o display para 8 bits e tudo fica como realmente está na memória.


Vamos executar o código. Entremos na opção Target -> MCU Core. Teremos algo como a imagem abaixo:

Os registradores estão, aparentemente, com valores aleatórios e o processador esta em estado Halted (parado). Como o inicio da execução se dá com o RESET, cliquemos no botão System Reset.



O estado do processador muda para Running (executando) e não é possível ver o valor de qualquer registrador, Para tal, precisamos parar o processador clicando no botão Halt.


O processador volta ao estado Halted e podemos ver seus registradores. Como programado, o registrador R0 possui o valor 3, o R1 o valor 4 e o registrador R3 a soma dos dois, ou seja, 3+4=7. Notamos também que o registrador R13 aponta para o inicio da pilha conforme programamos. Notamos ainda que, o registrador R15 ou PC (program counter) possui o valor 0x0e que é o endereço da próxima execução ou o símbolo _stop do nosso programa (o loop infinito). Os demais registradores R3 a R12 e R14 possuem o valor inicial 0xffffffff.

Com isso concluímos a codificação e execução do nosso primeiro programa para o microcontrolador STM32F051R8T6 que equipa a placa STM32F0Discovery. Apesar de simples e de não ter uma utilidade prática, ele serviu para assimilar muitos conceitos.

No próximo post simplificaremos o processo de geração de objetos e imagem através do comando MAKE.

REFERÊNCIAS
http://tigcc.ticalc.org/doc/gnuasm.html
http://www.coranac.com/tonc/text/asm.htm
"Cortex-M0 Devices Generic User Guide", ARM
"The Definitive Guide to the ARM Cortex-M0",Joseph Yiu
https://pt.wikipedia.org/wiki/ELF