Esse post faz parte de uma série de posts sobre mistura de código C (avr-gcc) com código Assembly (AVRASM2). Se você ainda não leu os posts anteriores, recomendo que leia antes de prosseguir.
Contexto
No post anterior vimos que é possível chamar código assembly (feito com AVRASM2) a partir de codigo C (avr-gcc). Vimos também que existem algumas limitaçoes na estratégia usada, tivemos que ajustar a instrução .org
e isso significa que tínhamos que ajustar o código assembly toda vez que adicionávamos mais código C. Nesse post vamos ver uma outra abordagem em que isso não é mais necessário.
Olhando para o exemplo inicial
Esse será o código assembly que usaremos para ilustrar esse post.
.org 0x0000
_blinks:
call _clear
call _real_code
ret
_real_code:
ldi r23, 0xa
add r24, r23
ret
_clear:
clr r1
clr r25
ret
Esse código, depois de compilado e linkado com um código C produz esse disassembly:
00000080 <_binary_build_blink_call_asm_bin_start>:
80: 0e 94 08 00 call 0x10 ; 0x10 <__zero_reg__+0xf>
84: 0e 94 05 00 call 0xa ; 0xa <__zero_reg__+0x9>
88: 08 95 ret
8a: 7a e0 ldi r23, 0x0A ; 10
8c: 87 0f add r24, r23
8e: 08 95 ret
90: 11 24 eor r1, r1
92: 99 27 eor r25, r25
94: 08 95 ret
00000096 <main>:
96: 80 e0 ldi r24, 0x00 ; 0
98: 0e 94 40 00 call 0x80 ; 0x80 <_binary_build_blink_call_asm_bin_start>
Vamos analisar esse código mais de perto e ver o que está acontecendo. Vemos que nosso código assembly foi posicionado no endereço 0x0080
e que nossa funcção main()
faz uma chamada a esse endereço (call 0x80
).
Olhando as duas primeiras instruçoes de nossa rotina Assembly (_binary_build_blink_call_asm_bin_start
), vemos que as duas chamadas estão indo para endereços completamente errados (0x10
e 0xa
). É fácil perceber que os endereços corretos deveriam ser, respectivamente, 0x90
e 0x8a
. Até aqui nenhuma novidade em relação ao que já fizemos. Acontece que podemos mostrar ao compilador onde cada uma de nossas rotinas começa e fazemos isso atráves da tabela de símbolos.
Manipulando a tabela de símbolos
A tabela de símbolos diz ao compilador onde está cada parte do nosso código, no nosso caso, onde estão cada uma das rotinas assembly. Vamos voltar um pouco e olhar a tabela de símbolos do nosso código assembly compilado, recém convertido para ELF partir de um HEX. Se olharmos bem veremos que só temos os símbolos criados pelo avr-objcopy
quando fizemos a conversão.
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 g .text 00000000 _binary_build_blink_call_asm_bin_start
00000016 g .text 00000000 _binary_build_blink_call_asm_bin_end
00000016 g *ABS* 00000000 _binary_build_blink_call_asm_bin_size
E o disassembly:
00000000 <_binary_build_blink_call_asm_bin_start>:
0: 0e 94 08 00 call 0x10 ; 0x10 <_binary_build_blink_call_asm_bin_start+0x10>
4: 0e 94 05 00 call 0xa ; 0xa <_binary_build_blink_call_asm_bin_start+0xa>
8: 08 95 ret
a: 7a e0 ldi r23, 0x0A ; 10
c: 87 0f add r24, r23
e: 08 95 ret
10: 11 24 eor r1, r1
12: 99 27 eor r25, r25
14: 08 95 ret
(Lembrando que nesse disasembly as duas primeiras instruções estão corretas pois o código ainda não foi linkado com o código C)
Quando convertemos um HEX para ELF perdemos todas as labels (símbolos) originais do Assembly. Na verdade, só de compilar o Assembly as labels já são convertidas em endereços absolutos.
Acontece que o avrasm2
pode gerar, no momento da compilação, dois arquivos adicionais: Um tem todos os labels e seus endereços finais (.map, opção -m
) e o outro tem o código assembly final, ainda em formato de texto mas já com todos os endereços resolvidos (.lst, opção -l
). Olhando o .lst
vemos como ficou nossa rotina _blinks
:
.org 0x0000
_blinks:
000000 940e 0008 call _clear
000002 940e 0005 call _real_code
000004 9508 ret
_real_code:
000005 e07a ldi r23, 0xa
000006 0f87 add r24, r23
000007 9508 ret
_clear:
000008 2411 clr r1
000009 2799 clr r25
00000a 9508 ret
Podemos perceber que a linha do call
é codificada como 940e 0008
. A primeira parte é o código da instrução (940e
) e a segunda é o endereço para onde ela transfere o controle da execução (0008
).
No aquivo que contém todos as labels e seus respectivos endereços finais, temos o seguinte:
CSEG _blinks 00000000
CSEG _clear 00000008
CSEG _real_code 00000005
Aqui temos nossos três símbolos: _blinks
, _clear
e _real_code
. Olhando o disassembly do arquivo ELF vemos que a primeira instrução call
foi codificada como: 0e 94 08 00
, que é essencialmente a mesma coisa que tínhamos no nosso arquivo .lst
!
ELF:
00000000 <_blinks>:
0: 0e 94 08 00 call 0x10 ; 0x10 <_binary_build_blink_call_asm_bin_start+0x10>
.lst:
_blinks:
000000 940e 0008 call _clear
A única diferença entre eles parece ser a representação do bit mais significativo. No ELF a representação está com o byte menos significativo primeiro (mais à esquerda) e no .lst
está com byte menos signifcativo por último (mais à diretia). Isso significa que nossa rotina _clear
que no HEX estava no endereço 0x0008
está agora no ELF no endereço 0x10
.
Ainda não entendo porque o código da instrução menciona o endereço 0008
e o disassembly mostra call 0x10
(um é o dobro do outro!), mas percebi que a princípio os endereços sempre coincidem! Ou seja, no ELF os endereços são sempre o dobro dos respectivos endereços no HEX. Talvez isso tenha relação com como o ELF representa internamente as instruçoes. A instrução que vai para o AVR é mesmo 0e 94 08 00
, ou seja, o call
irá saltar para o endereço 0008
da memória flash do AVR, mas como estamos adicionando símbolos no ELF, precisamos obeceder o endereçamento que ele mostra.
Agora que sabemos onde estão nossas duas rotinas (_clear
e _real_code
) dentro do ELF podemos adicionar dois símoblos à tabela de símbolos. Como não encontrei nenhuma ferramenta que adicionasse símbolos a um ELF, escrevei meu pŕoprio código que faz isso, chamei a ferramenta de elf-add-symbol
. Nossa nova tabela de símbolos ficou assim (mais detalhe em como ela foi adicionada ao arquivo ELF: Automatizando todo o processo):
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 g .text 00000000 _blinks
00000010 g .text 00000000 _clear
0000000a g .text 00000000 _real_code
A tabela é simples. Temos o endereço do símbolo, a seção do ELF onde ele está, o tamanho do símbolo e o nome do símbolo. O g
e l
significam, respectivamente, Símbolo Global e Símbolo Local. Isso é importante pois apenas símbolos globais são enxergados no momento da link-edição.
Depois que fazemos isso, até o disassembly muda e fica mais simples de entender, pois conseguimos ver onde começa cada rotina, veja:
Disassembly of section .text:
00000000 <_blinks>:
0: 0e 94 08 00 call 0x10 ; 0x10 <_clear>
4: 0e 94 05 00 call 0xa ; 0xa <_real_code>
8: 08 95 ret
0000000a <_real_code>:
a: 7a e0 ldi r23, 0x0A ; 10
c: 87 0f add r24, r23
e: 08 95 ret
00000010 <_clear>:
10: 11 24 eor r1, r1
12: 99 27 eor r25, r25
14: 08 95 ret
Isso já ajuda, mas quando linkamos esse código Assembly com código C, mesmo tendo manipulado a tabela de símbolos (que já é um bom começo) ainda ficamos com endreços errados. Vejamos o disassembly após a link-edição:
00000080 <_blinks>:
80: 0e 94 08 00 call 0x10 ; 0x10 <__zero_reg__+0xf>
84: 0e 94 05 00 call 0xa ; 0xa <__zero_reg__+0x9>
88: 08 95 ret
0000008a <_real_code>:
8a: 7a e0 ldi r23, 0x0A ; 10
8c: 87 0f add r24, r23
8e: 08 95 ret
00000090 <_clear>:
90: 11 24 eor r1, r1
92: 99 27 eor r25, r25
94: 08 95 ret
00000096 <main>:
96: 80 e0 ldi r24, 0x00 ; 0
98: 0e 94 40 00 call 0x80 ; 0x80 <_blinks>
Perceba que todo nosso codigo Assembly foi posicionado no endereço 0x0080
e mesmo nossas duas rotinas auxiliares tendo sido posicionadas, respectivcamente, em 0x008a
e 0x0090
as duas linhas com as chamadas call
continuam achando que as rotinas estão em 0x10
e 0xa
. É aí que entra a tabela de realocação.
Isso acontece porque esse código assembly é apenas copiado para alguma posição dentro do binário final durante o processo de link-edição. Precisamos então, de alguma forma, dizer ao compilador que o endereço das rotinas _real_code
e _clear
irá mudar e por isso ele deve ajustar o endereço de chamada de quaisquer instruçoes que fizerem referências a essas rotinas.
Tabela de realocação
A Tabela de realocação existe exatamente para dizer ao compilador quais símbolos mudarão de lugar e quais instruçoes ele deve editar e trocar o endereço final.
Para entendermos a tabela de realocação precisamos voltar ao nosso disassembly inicial, antes de ser link-editado ao código C.
Disassembly of section .text:
00000000 <_blinks>:
0: 0e 94 08 00 call 0x10 ; 0x10 <_clear>
4: 0e 94 05 00 call 0xa ; 0xa <_real_code>
8: 08 95 ret
0000000a <_real_code>:
a: 7a e0 ldi r23, 0x0A ; 10
c: 87 0f add r24, r23
e: 08 95 ret
00000010 <_clear>:
10: 11 24 eor r1, r1
12: 99 27 eor r25, r25
14: 08 95 ret
(Usando a mesma ferramenta que escrevi para manipular a tabela de símbolos podemos construir a tabela de realocação)
Vejamos a tabela em detalhes (mais detalhes em como ela foi adicionada: Automatizando todo o processo):
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000000 R_AVR_CALL _clear
00000004 R_AVR_CALL _real_code
A tabela funciona da segunte forma: Cada seção do ELF pode ter sua tabela de realocação. Nesse caso, essa tabela de realocação “pertence” à secão .text
, ou seja, ela faz referência apenas a símbolos que existem na seção .text
, que é onde estão as instruçoes do nosso código. O campo OFFSET
indica o endereço da instrução que deverá ser editada (veremos isso em detalhe mais adiante). O campo TYPE
indica o tipo de realocação, confesso que olhei esse valor (R_AVR_CALL
) em um ELF gerado pelo avr-gcc (mais sobre isso: Engenharia reversa para descobrir o valor do R_AVR_CALL). O campo VALUE
indica qual símbolo será realocado.
Agora vamos analisar cada uma das linhas da tabela de realocação:
00000000 R_AVR_CALL _clear
Essa linha nos diz que a instrução que está na posição 0x0000
(call 0x10
) está fazendo uma chamada a um rotina de nome _clear
e que essa rotina estará em algum lugar no binário final. Seja qual for esse lugar, essa instrução call
deve ser editada e o valor 0x10
deve ser trocado pelo endereço final da rotina _clear
.
O mesmo acontece pra a outra linha:
00000004 R_AVR_CALL _real_code
Aqui é exatamente a mesma coisa, mas a instrução que será editada é o call 0xa
e o 0xa
será trocado pelo endereço final da rotina _real_code
.
Agora que temos um ELF com tabela de símbolos e tabela de realocação estamos prontos para re-linkar com o código C. Fazendo isso temos o seguinte dissasembly:
00000080 <_blinks>:
80: 0e 94 48 00 call 0x90 ; 0x90 <_clear>
84: 0e 94 45 00 call 0x8a ; 0x8a <_real_code>
88: 08 95 ret
0000008a <_real_code>:
8a: 7a e0 ldi r23, 0x0A ; 10
8c: 87 0f add r24, r23
8e: 08 95 ret
00000090 <_clear>:
90: 11 24 eor r1, r1
92: 99 27 eor r25, r25
94: 08 95 ret
E agora temos nosso código assembly com o endereços dos calls corretamente ajustados!
Um detalhe importante é perceber que a instrução foi mesmo editada. Olhando a primeira instrução call
ela está codificada como 0e 94 48 00
(antes era 0e 94 08 00
, lembra?) e como os endereços no ELF são sempre o dobro dos endereços no HEX podemos conferir que 0x90
(endereço da rotina _clear
no ELF) é exatamente o dobro de 0x48
, que é o endereço que está codificado na instrução!!
Esse código funciona quando gravado na memória flash do micro controlador!
Automatizando todo o processo
É claro que o que fizemos aqui foi uma análise manual de como construir todo o aparato necessário para que possamos realocar rotinas que estão espalhadas pelo nosso código Assembly legado, mas quando estamos lidando com um projeto grande precisamos fazer isso de forma automatizada. Para isso eu escrevi um script que me ajuda a manipular a tabela de símbolos e a tabela de realocação.
Primeiro escrei um script python que funciona da segunte maneira:
Dado o conteudo do arquivo de mapa (.map
produzido pelo avrasm2
) e a saída do disassembly do ELF ele consegue encontrar o novo endereço dos símbolos dentro do ELF e também quais instruçoes possuem desvio para endereços absolutos e, portanto, precisarão ser editadas. Usando esse script com o código que analisamos nese post, temos a seguinte saída:
$ avr-objdump -d blink_call.asm.elf \
| python2 extract-symbols-metadata.py blink_call.asm.map
_blinks 0x0000
_clear 0x10 0x0
_real_code 0xa 0x4
Olhando bem para essa saída ela representa exatamente nossa tabela de realocação. Essa saida é estruturada da segunte forma:
<nome_do_símbolo> <endereço_do_símbolo> <endereço_das_instruçoes_que_usam_esse_símbolo>
Agora o que precisamos fazer é transformar essa saída em uma tabela de realocação, dentro o ELF. Para isso usamos a ferramenta elf-add-symbol
. Assumindo que gravamos esse conteudo em blink_call.asm.symtab
podemos fazer o seguinte:
cat blink_call.asm.symtab | ./elf-add-symbol blink_call.asm.elf
Essa chamada modifica o arquivo blink_call.asm.elf
adicionando a tabela de símbolos e a tabela de realocação! E então estamos prontos para link-editar nosso ELF com nosso código C.
Engenharia reversa para descobrir o valor do R_AVR_CALL
A tabela de realocação tem a uma estrutura espefícia. Um dos campos dessa estrutura é o r_info
. Esse campo diz duas coisas: Qual o símbolo está sendo realocado (8 bits mais significativos) e qual o tipo de realocação será feita (8 bits menos significativos). Quando escrevi o elf-add-symbol
, na biblioteca que usei (ELFIO) só existiam constantes para os tipos de realocação do ELF32 para arquitetura x86 então, de alguma forma, eu precisava descobrir qual o valor eu deveria colocar nesse campo para a realocação de símbolos para AVR.
O que fiz foi compilar um arquivo assembly com o avr-gcc
e usando a ferramenta avr-readelf
consegui ver o seguinte:
Relocation section '.rela.text' at offset 0x100 contains 2 entries:
Offset Info Type Sym.Value Sym. Name + Addend
00000000 00000112 R_AVR_CALL 00000000 .text + a
00000004 00000112 R_AVR_CALL 00000000 .text + c
Peguei o valor 0x112
(campo Info
) e usei a macro ELF32_R_TYPE()
da própria lib ELFIO. O retorno dessa chamada foi 0x12
que é 18
em decimal. Por isso no código do elf-add-symbol
temos a linha #define R_AVR_CALL 18
.
Próximo post: Chamando código novo C (avr-gcc) a partir de código legado Assembly (avrasm2)