Décorticage d'un simple firmware

Compilation

Dans la partie précédente, nous avons vu que le programme suivant, qui permet d'allumer la LED de la carte:

#include <avr/io.h>

int main(void)
{
    // Configure le bit 5 du registre DDRB en sortie
    DDRB |= (1 << 5);
    PORTB |= (1 << 5);

    // Boucle infinie
    while (1);
}

Compilons ce programme à l'aide de:

avr-gcc -mmcu=atmega328p -Os -o main.elf main.c

À l'aide de la commande:

$ avr-size main.elf
text    data     bss     dec     hex filename
138       0       0     138      8a main.elf

On peut constater que le code binaire du programme produit occupera 138 octets de mémoire flash. La commande:

$ avr-objcopy -O binary main.elf main.bin

Produit d'ailleurs bien un fichier binaire de 138 octets.

Désassemblage

Désassemblons maintenant le fichier binaire produit:

$ avr-objdump -d main.elf

Vecteurs d'interruption

On constate tout d'abord que le code commence par __vectors à l'adresse 0x0 de la mémoire flash:

| 00000000 <__vectors>:
| 0:	0c 94 34 00 	jmp	0x68	; 0x68 <__ctors_end>
| 4:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 8:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 10:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 14:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 18:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 1c:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| 20:	0c 94 3e 00 	jmp	0x7c	; 0x7c <__bad_interrupt>
| ...

Cette zone est appelée la table des vecteurs d'interruption. Elle contient les adresses des fonctions à appeler lorsqu'une interruption se produit. La toute première correspond au reset du microcontrôleur, dans ce cas, le microcontrôleur saute sur __ctors_end l'adresse 0x68.

À noter que, par défaut, toutes les autres interruptions sont redirigées vers la fonction __bad_interrupt dont le code consiste en un saut en 0x0:

| 0000007c <__bad_interrupt>:
| 7c:	0c 94 00 00 	jmp	0	; 0x0 <__vectors>

Initialisations

Plus bas, on peut lire le code suivant:

| 00000068 <__ctors_end>:
| 68:	11 24       	eor	r1, r1
| 6a:	1f be       	out	0x3f, r1	; 63
| 6c:	cf ef       	ldi	r28, 0xFF	; 255
| 6e:	d8 e0       	ldi	r29, 0x08	; 8
| 70:	de bf       	out	0x3e, r29	; 62
| 72:	cd bf       	out	0x3d, r28	; 61
| 74:	0e 94 40 00 	call	0x80	; 0x80 <main>
| 78:	0c 94 43 00 	jmp	0x86	; 0x86 <_exit>

Ce code:

  • Désactive les interruptions en mettant le registre SREG (0x3f) à 0 (cf page 11 de la datasheet),
  • Assigne SPH (0x3e) à 0x08 et SPL (0x3d) à 0xFF (cf page 14 de la datasheet), ces registres (Stack Pointer High et Stack Pointer Low) définissent l'adresse de la pile.
    • La pile est donc définie à l'adresse 0x08FF, soit la fin de la mémoire RAM.
  • Appelle la fonction main à l'adresse 0x80,
  • Saute sur _exit à l'adresse 0x86.

Le désassemblage de la fonction main donne:

| 00000080 <main>:
| 80:	25 9a       	sbi	0x04, 5	; 4
| 82:	2d 9a       	sbi	0x05, 5	; 5
| 84:	ff cf       	rjmp	.-2      	; 0x84 <main+0x4>

Les instructions sbi permettent de mettre à 1 un bit d'un registre. 0x04 et 0x05 sont les adresses des registres PORTB et DDRB (cf page 72 de la datasheet).

Nos opérateurs de masque binaire C ont été traduit de cette façon en assembleur!

Fin

Enfin, la routine _exit qui est appellée à la fin du programme désactive les interruptions et rentre dans une boucle infinie:

| 00000086 <_exit>:
| 86:	f8 94       	cli
|
| 00000088 <__stop_program>:
| 88:	ff cf       	rjmp	.-2      	; 0x88 <__stop_program>

Dans la partie suivant,
nous aborderons plus en détails le fonctionnement des broches, mais aussi des bus de communication