dimanche 1 mars 2020

ARDUINO : au fond de la pile


ARDUINO : au fond de la pile


Nous allons parler dans cet article de la gestion de la pile (stack en anglais) d'un ATMEGA328p ou d'un ATMEGA2560.
Ces explications sont parfaitement applicables à d'autres microcontrôleurs. Les informaticiens ne sont pas de grands génies. Il leur arrive rarement de réinventer la poudre ou l'eau tiède.
Certaines différences peuvent exister, par exemple pour un ESP8266, un ESP32, ou STM32, la taille d'une adresse mémoire est simplement différente : 32 bits au lieu de 16.

Le but de cete article est également de fournir des outils de diagnostic de la pile. Il est en effet possible de mesurer à un instant T la place disponible sur la pile avec des outils relativement simples.

1. La pile

Lors de l'exécution d'un logiciel, la pile est utilisée pour empiler les adresses de retour des fonctions (lors du retour d'une fonction il vaut mieux en effet savoir d'où l'appel a été fait), ainsi que les variables locales (appelées aussi variables automatiques).
La pile est gérée grâce à un registre spécial : le pointeur de pile.

Lorsque l'on appelle une fonction l'adresse de retour est placée sur la pile et le pointeur de pile est décrémenté de 2 octets (la taille d'une adresse ATMEGA).
Si la fonction appelée déclare une variable locale le pointeur de pile est décrémenté de la taille de la variable. Dans certains cas, le compilateur utilisera un registre, plus rapide.
Si cette fonction appelle une autre fonction, le pointeur de pile est encore décrémenté, de la même manière.

Si la valeur du pointeur de pile descend trop bas il y a un risque d'aller corrompre les adresses haute de la zone des variables globales. Si le logiciel utilise des objets String ou l'allocation dynamique, les adresses hautes du tas seront corrompues.

2. Les espaces mémoire

Les différents espaces mémoire d'un ATMEGA328p peuvent être représentés ainsi :

Le contenu de ces différentes zones est le suivant :
  • STARTUP : code de démarrage
  • CODE : code de l'application, y compris celui des librairies
  • LOAD_DATA : les valeurs des variables globales et statiques initialisées
  • REGISTERS : registres du microcontrôleur (GPIOs, TIMERS, etc.)
  • DATA : variables globales et statiques initialisées
  • BSS : variables globales et statiques non initialisées
  • STACK : la pile
Les notions de variables globales, statiques, initialisées ou non, sont décrites dans le paragraphe suivant.

Le tas (zone mémoire réservé à l'allocation mémoire dynamique) n'est pas représenté. Il a déjà été décrit dans l'article précédent :
https://riton-duino.blogspot.com/2020/02/arduino-la-fragmentation-memoire.html

Ici nous allons parler de la pile. Le tas, existant ou pas, ne modifie en rien ces explications.

2.1. Les variables

Nous allons commencer par faire la distinction entre les différents types de variables :

int globalData;
int globalInitialized = 1000;

void setup()
{
  Serial.begin(115200);
}

void loop()
{
  int localData;
  int localInitialized = 1000;
  static int staticData;
  static int staticInitialized = 1000;

  Serial.print("globalData :"); Serial.println(globalData);
  Serial.print("globalInitialized :"); Serial.println(globalInitialized);
  Serial.print("localData :"); Serial.println(localData);
  Serial.print("localInitialized :"); Serial.println(localInitialized);
  Serial.print("staticData :"); Serial.println(staticData);
  Serial.print("staticInitialised :"); Serial.println(staticInitialized);
  delay(1000);
}


globalData est une variable globale, accessible depuis n'importe quelle fonction. Toutes les variables globales sont initialisées à ZÉRO au démarrage.
Elle aura donc la valeur ZÉRO dès le départ, y compris avant l'entrée dans la fonction setup().

globalInitialized est une autre variable globale, initialisée à la déclaration.
Elle aura la valeur 1000 dès le départ, y compris avant l'entrée dans la fonction setup().

localData est une variable locale, créée sur la pile, accessible seulement depuis la fonction dans laquelle elle est déclarée. Elle a une valeur indéterminée en entrant dans la fonction. Ce n'est pas parce que par chance elle vaut ZÉRO la première fois qu'elle va conserver cette valeur dans le temps.
Une variable locale est détruite en sortant de la fonction.

globalInitialized est une autre variable locale. Elle est initialisée à la valeur 1000 en entrant dans la fonction, et ceci à chaque fois que cette fonction est appelée.

staticData est une variable statique, Elle a les mêmes propriétés qu'une variable globale, sauf qu'elle est accessible uniquement depuis la fonction dans laquelle elle est déclarée.

staticInitialized est une autre variable statique, initialisée à la déclaration. Elle a les mêmes propriétés qu'une variable globale initialisée à la déclaration, sauf qu'elle est accessible uniquement depuis la fonction dans laquelle elle est déclarée.

2.2. Le startup

Ce morceau de code est celui qui est exécuté avant l'entrée dans l'application. Ses différents rôles sont :
  • initialiser la pile
  • initialiser à ZÉRO la zone BSS (variables globales)
  • initialiser la zone DATA (variables globales initialisées)
  • appeler les constructeurs d'objets statiques
  • appeler main()
La zone BSS est initialisée à ZÉRO par une fonction __clear_bss du startup. Comme cette opération est faite automatiquement, il est inutile de le faire manuellement :

int  data = 0;   // initialisation à ZERO inutile

La zone DATA est initialisée par une fonction __do_copy_data du startup.
Cette opération est réalisée par recopie de la zone LOAD_DATA en mémoire FLASH vers la zone DATA en RAM.
La zone DATA et la zone LOAD_DATA ont donc la même taille.

2.3. L'application

Le point d'entrée de l'application s'appelle main(). Voici son contenu dans la librairie standard ARDUINO :

int main(void)
{
    init();
    initVariant();
#if defined(USBCON)
    USBDevice.attach();
#endif
    setup();
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
    return 0;
}

On retouve les points d'entrée d'un sketch : setup(), appelé une seule fois, et loop(), appelé en boucle.

2.4. Les variables locales

Comme on l'a vu plus haut les variables locales sont déclarées sur la pile. Lorsque l'on utilise trop de variables locales à travers des appels de fonctions imbriqués (une fonction appelle une autre fonction qui en appelle une autre) il y risque de débordement de pile.

Comment mesurer tout ça et évaluer le risque ?

2.5. La mesure de la pile

Le code suivant comporte quelques fonctions utiles :

#define GLOBAL_SIZE        1211
char global[GLOBAL_SIZE + 1];
int  data = 1000;

extern char *__bss_end;
extern char *__bss_start;
extern char *__data_end;
extern char *__data_start;
extern char *__data_load_start;
extern char *__data_load_end;

uint16_t data_start;
uint16_t data_end;
uint16_t data_load_start;
uint16_t data_load_end;
uint16_t bss_start;
uint16_t bss_end;
uint16_t stack_addr;
int stack_size;

#define fillStack(addr) for (char *p = (char *)&__bss_end+1; p < addr ; p++) *p = 'U';

#define LOCAL_SIZE        100

void __attribute__ ((noinline)) func2(void)
{
  char local[LOCAL_SIZE + 1];
  memset(local, 'C', LOCAL_SIZE);
  local[LOCAL_SIZE] = 0;
  Serial.print(F("func2 STACK       = 0X0")); Serial.println((uint16_t)local, HEX);
}

void __attribute__ ((noinline)) func1(void)
{
  char local[LOCAL_SIZE + 1];
  memset(local, 'B', LOCAL_SIZE);
  local[LOCAL_SIZE] = 0;
  Serial.print(F("func1 STACK       = 0X0")); Serial.println((uint16_t)local, HEX);
  func2();
}

int getFreeStack(void)
{
  for (char *p = (char *)bss_end+1 ; p < RAMEND ; p++) {
    if (*p != 'U') {
      return p - bss_end+1;
    }
  }
  return 0;
}

unsigned char memByteRam(const void* x) {return *(char*)x;}
unsigned char memBytePgm(const void* x) {return pgm_read_byte(x);}

void dump(Print& out, void const*at, int sz, unsigned char (*memByte)(const void*)) {
  while(sz>0) {
    out.print("0x");
    out.print((unsigned long)at < 0x10 ? "000" : (unsigned long)at<0x100 ? "00" : (unsigned long)at<0x1000 ? "0" : "");
    out.print((unsigned long)at,HEX);
    out.print(": ");
    for(int c=0;c<16;c++) {
      if (c==8) out.write(' ');
      if (sz-c>0) {
        // Because ISO C forbids `void*` arithmetic, we have to do some funky casting
        void *memAddress = (void *)((int)at + c);
        unsigned char v = memByte(memAddress);

        out.write(v>=32/*&&v<='z'*/?v:'.');
      } else out.write(' ');
    }
    out.write(' ');
    for (int c=0; c<16 && sz; c++, sz--) {
      // Because ISO C forbids `void*` arithmetic, we have to do some funky casting
      unsigned char v=memByte(at);
      at = (void *)((int)at + 1);

      if (c==8) out.write(' ');
      out.print(v<16?"0":"");
      out.print(v,HEX);
      // out.write(v==0x97?'=':' ');
      out.write(' ');
    }
    out.println();
  }
}

void dumpRam(Print& out, void const*at,int sz) {return dump(out,at,sz,memByteRam);}
void dumpPgm(Print& out, void const*at,int sz) {return dump(out,at,sz,memBytePgm);}

void setup()
{
  char foo = 0xff;
  data_load_start = (uint16_t)&__data_load_start;
  data_load_end = (uint16_t)&__data_load_end;
  data_start = (uint16_t)&__data_start;
  data_end = (uint16_t)&__data_end;
  bss_start = (uint16_t)&__bss_start;
  bss_end = (uint16_t)&__bss_end;
  stack_addr = &foo;
  stack_size = stack_addr - bss_end+1;
  fillStack(stack_addr);
  Serial.begin(115200);
  memset(global, 'A', GLOBAL_SIZE);
  Serial.print(F("global ")); Serial.println(global);
  Serial.print(F("data ")); Serial.println(data);
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  dumpRam(Serial, bss_end+1, stack_size);
  Serial.print(("fill stack        : 0X0")); Serial.print((bss_end)+1, HEX); Serial.print(F(" - 0X0"));   Serial.println(stack_addr, HEX);
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  Serial.print(F("RAMEND            = 0X0")); Serial.println(RAMEND, HEX);
  Serial.print(F("TOP OF STACK      = 0X0")); Serial.println(stack_addr, HEX);
  Serial.print(F("STACK POINTER     = 0X0")); Serial.println(SP, HEX);
  Serial.print(F("STACK SIZE        = ")); Serial.println(stack_size);
  Serial.print(F("__data_start      = 0X0")); Serial.println(data_start, HEX);
  Serial.print(F("__data_end        = 0X0")); Serial.println(data_end, HEX);
  Serial.print(F("__data_load_start = 0X0")); Serial.println(data_load_start, HEX);
  Serial.print(F("__data_load_end   = 0X0")); Serial.println(data_load_end, HEX);
  Serial.print(F("__bss_start       = 0X0")); Serial.println(bss_start, HEX);
  Serial.print(F("__bss_end         = 0X0")); Serial.println(bss_end, HEX);
  Serial.print(F("__bss_size        = ")); Serial.println(bss_end - bss_start);
  func1();
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  dumpRam(Serial, bss_end+1, stack_size);
}

void loop()
{
}


Tout d'abord il y a un certain nombre de variables externes :

extern char *__bss_end;
extern char *__bss_start;
extern char *__data_end;
extern char *__data_start;
extern char *__data_load_start;
extern char *__data_load_end;


Celles-ci sont créées par l'éditeur de liens (le linker en anglais) lors de la fabrication de l'exécutable. Elles représentent les adresses de début et de fin des trois zones BSS, DATA et DATA_LOAD.

uint16_t data_start;
uint16_t data_end;
uint16_t data_load_start;
uint16_t data_load_end;
uint16_t bss_start;
uint16_t bss_end;
uint16_t stack_addr;
int stack_size;


Ces quelques autres variables sont une recopie des précédentes, car il est plus facile de faire des calculs à partir de variables entières.

La macro fillStack() permet de remplir la pile de caractères 'U' (caractère '\x55'). Au fur et à mesure de l'utilisation de la pile d'autres caractères vont venir remplacer ceux-ci. Lorsque l'on désirera mesurer jusqu'à quelle adresse la pile a été utilisée, il suffira de compter le nombre d'octets 'U'.

getFreeStack() retourne le nombre d'octets non utilisés sur la pile. Plus ce nombre sera important plus la pile sera considérée comme étant en bon état de santé.

dump() permet d'afficher le contenu d'une zone mémoire :

0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 

Sur chaque ligne 16 octets sont affichés. L'adresse est affichée, puis 16 caractères en mode texte puis les 16 mêmes caractères en hexadécimal.

Pourquoi les fonctions func1 et func2 ont-elles été déclarées noinline ?
Parce que sinon, le compilateur, comme elles sont appelées une seule fois,  inclurait leur code directement dans la fonction appelante, ce qui n'est pas voulu dans cet exercice.

A l'exécution ce sketch affiche beaucoup d'informations :

global AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
data 1000
### STACK DUMP
free_stack        = 506 
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x070B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x071B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x072B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x073B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x074B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x075B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x076B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x077B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x078B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x079B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x080B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x081B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x082B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x083B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x084B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x085B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x086B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x087B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x088B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x089B: UU.⸮.⸮.. >?.B.... 55 55 04 89 01 BC 06 0E  29 2E 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 
0x08BB: ⸮...⸮.⸮. ..⸮.⸮.⸮. DF 00 02 00 3E 02 A9 00  00 01 FA 02 A9 04 89 01 
0x08CB: ⸮..'(. . ⸮.⸮..57. BC 06 0E 00 04 01 30 04  89 00 C1 06 0E 21 25 08 
0x08DB: .....#⸮. .⸮.⸮.".. 20 00 01 00 00 23 A3 00  01 DA 08 E6 E6 16 00 0F 
0x08EB: .`.⸮... .......⸮ 03 A3 08 FA 00 00 7F 00  00 00 03 05 11 01 05 B7 
0x08FB: ⸮.                FF 00 
fill stack        : 0X06AB - 0X08FB
free_stack        = 500
RAMEND            = 0X08FF
TOP OF STACK      = 0X08FB
STACK POINTER     = 0X08FA
STACK SIZE        = 594
__data_start      = 0X0100
__data_end        = 0X0138
__data_load_start = 0X0DBE
__data_load_end   = 0X0DF6
__bss_start       = 0X0138
__bss_end         = 0X06AA
__bss_size        = 1394
func1 STACK       = 0X0892
func2 STACK       = 0X0829

### STACK DUMP
free_stack        = 300
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x070B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x071B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x072B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x073B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x074B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x075B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x076B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x077B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x078B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x079B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07CB: UUUUUUUU UU.⸮.⸮.. 55 55 55 55 55 55 55 55  55 55 04 89 01 BC 06 0E 
0x07DB: ...9.... #⸮..⸮... 0B 0C 08 39 00 03 00 00  23 A3 00 01 D9 00 02 08 
0x07EB: ....@.⸮. .....⸮U 1A 00 03 01 40 07 F9 08  17 7F 00 00 00 02 A9 55 
0x07FB: UUUUU.⸮. ⸮....... 55 55 55 55 55 04 89 01  BC 06 0E 0D 0E 01 0A 00 
0x080B: ...#⸮..⸮ .......@ 02 00 00 23 A3 00 01 DB  00 01 01 14 00 02 01 40 
0x081B: ....... .⸮.(.⸮CC 00 03 00 00 7F 00 00 00  02 CD 08 28 03 F2 43 43 
0x082B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x083B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x084B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x085B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x086B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x087B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x088B: CC..⸮..B BBBBBBBB 43 43 00 08 91 04 1E 42  42 42 42 42 42 42 42 42 
0x089B: BB.⸮.⸮.. ?..B.... 42 42 04 89 01 BC 06 0E  2A 2F 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 
0x08BB: ⸮...⸮.⸮. ..,.⸮.⸮. DF 00 02 00 3E 02 A9 00  00 01 2C 02 A9 04 89 01 
0x08CB: ⸮..(). . ⸮.⸮..68. BC 06 0E 01 05 01 30 04  89 00 C1 06 0E 22 26 08 
0x08DB: .....#⸮. .⸮.⸮.".. 20 00 01 00 00 23 A3 00  01 D7 08 E6 E6 16 00 0F 
0x08EB: .`.⸮... .......y 03 A3 08 FA 00 00 7F 00  00 00 03 05 11 01 06 79 
0x08FB: ⸮.                FF 00 


Nous allons expliquer tout cela.

Nous allons commencer à la ligne suivante :

fill stack        : 0X06AB - 0X08FB

La pile est emplie de caractères 'U' de 0X06AB à 0X08FB.

RAMEND            = 0X08FF

Il s'agit de l'adresse haute de la mémoire RAM. L'adresse de début (voir plus bas __data_start vaut 0x0100. La quantité de mémoire est donc de :

0x900 - 0x100 : 0x800 = 2048 octets, pile la taille de mémoire RAM de l'ATMEGA328p.

TOP OF STACK      = 0X08FB

Il s'agit de l'adresse de la première variable locale du sketch : foo
Il est normal que l'adresse soit proche de RAMEND, étant donné que le pointeur de pile est presque à son maximum au démarrage.

STACK POINTER     = 0X08FA

Il s'agit de l'adresse stockée dans le pointeur de pile.
Il est normal que sa valeur soit inférieure d'un octet à celle de l'adresse de la variable foo puisque la variable foo a une taille d'un octet. Après le déclaration de foo le pointeur de pile a été décrémenté de 1 octet.

STACK SIZE        = 594

Cette valeur est obtenue en soustrayant l'adresse RAMEND l'adresse de la variable foo. On obtient donc la taille de la pile puisque foo a été déclarée sur la pile.
Attention : le compilateur ordonne les variables sur la pile comme bon lui semble. S'il y avait plusieurs variables locales, leurs emplacements en mémoire seraient forcément voisins mais dans un ordre indéterminé.

__data_start      = 0X0100

Cette adresse est le début de la zone DATA.

__data_end        = 0X0138

Cette adresse est la fin de la zone DATA.

__data_load_start = 0X0DBE

Cette adresse est le début de la zone LOAD_DATA en mémoire FLASH.

__data_load_end   = 0X0DF6

Cette adresse est la fin de la zone LOAD_DATA.

Au démarrage LOAD_DATA sera recopiée dans DATA. Elles doivent donc avoir la même taille.

__data_end - __data_start = __data_load_end - __data_load_start

Est-ce vrai ? Oui, 56 octets dans les deux cas.

A quoi correspondent ces 56 octets ?
Premièrement à notre variable data :

int  data = 1000;

Le reste est occupé par d'autres variables globales ou statiques initialisées à la déclaration, dans la librairie ARDUINO.

__bss_start       = 0X0138

Cette adresse est le début de la zone BSS, les variables globales.

__bss_end         = 0X06AA

Cette adresse est la fin de la zone BSS.

__bss_size        = 1394

Cette valeur représente la quantité de variables globales. En sommes-nous responsables ?

Non, puisque nous avons déclaré :
  • un tableau global de 1211 octets
  • 8 entiers de 2 octets chacun
  • donc un total de 1227 octets
Il y a donc 1394 - 1227 = 167 octets déclarés quelque part dans la librairie ARDUINO.

func1 STACK       = 0X0892

Il s'agit de l'adresse d'une variable locale de 100 octets déclarée dans func1.

func2 STACK       = 0X0829

Il s'agit de l'adresse d'une variable locale de 100 octets déclarée dans func2.

Pourquoi est-elle beaucoup plus basse que la précédente ?
parce que func1 appelle func2 donc les variables locales s'empilent.

Il y a 105 octets de différence, ce qui correpond à la somme de :
  • la taille de la variable locale
  • l'adresse de retour de la fonction
  • quelques registres que le compilateur a décidé d'empiler par comodité
Il nous reste deux blocs marqués ### STACK DUMP à examiner.

Le premier est affiché juste après avoir affiché notre variable data.

### STACK DUMP
free_stack        = 506
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55  
...
0x089B: UU.⸮.⸮.. >?.B.... 55 55 04 89 01 BC 06 0E  29 2E 08 38 00 01 00 00  

Une quantité de pile de 506 octets n'a pas été utilisée.
On voit plus bas dans le dump qu'à l'adresse 0x089D le 'U' a disparu.

Tout le reste a déjà été exploité par les différents appels de fonctions, en particulier Serial.begin(), Serial.print() et Serial.println() qui ont déclaré et utilisé des variables locales entre cet emplacement et la fin de la mémoire RAM.

On peut donc dire que 0x08FB - 0x089D = 94 octets ont été utilisés depuis le début de l'exécution, ce qui est assez conséquent. Serial.print() est gourmand.

Le deuxième dump est affiché juste après avoir appelé la fonction func1 (qui elle-même appelle func2). On devrait observer les traces des deux fonctions.

### STACK DUMP
free_stack        = 300
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
...
0x07CB: UUUUUUUU UU.⸮.⸮.. 55 55 55 55 55 55 55 55  55 55 04 89 01 BC 06 0E  

Une quantité de pile de 300 octets n'a pas été utilisée.
On voit plus bas dans le dump qu'à l'adresse 0x07D5 le 'U' a disparu.

On voit également les traces des variables locales des fonctions func1 et func2.
func1 a rempli un buffer de caractères 'B'.
func2 a rempli un buffer de caractères 'C'

0x088B: CC..⸮..B BBBBBBBB 43 43 00 08 91 04 1E 42  42 42 42 42 42 42 42 42 
0x089B: BB.⸮.⸮.. ?..B.... 42 42 04 89 01 BC 06 0E  2A 2F 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 


0x081B: ....... .⸮.(.⸮CC 00 03 00 00 7F 00 00 00  02 CD 08 28 03 F2 43 43 
0x082B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x083B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 


Et les traces sont encore visibles aux adresses affichées précédemment :

func1 STACK       = 0X0892
func2 STACK       = 0X0829

Pourquoi les traces du travail de func1 ont-elles en partie disparu ?
Parce que depuis la sortie de func1, d'autres fonctions, en particulier Serial.print() et Serial.println() ont déclaré des variables au même endroit.

Après avoir examiné toutes ces données on peut conclure :

STACK SIZE        = 594
free_stack        = 300 au minimum

La pile a été utilisée à environ 50%, ce qui est un signe de parfaite santé pour notre application.

3. Conclusion

Comme on vient de le voir, le compilateur et l'éditeur de liens mettent à notre disposition toutes les variables nécessaire au diagnostic mémoire d'une application.
Il n'y a qu'à les utiliser.


Cordialement
Henri

1 commentaire:

  1. Bravo pour cet article même s'il est très dur à digerer.
    Je ne dirai qu'une chose ... encore, encore.
    J'ai beaucoup appris de vous sur la STM32 mais j'ai encore tellement à apprendre.
    Merci Henri

    RépondreSupprimer