GDB a ARM - několik triků.

1. WiFi debuger ESP32.

Tohle jsem našel náhodou na webu a přijde mi to jako docela vtipné řešení. Původní Blackmagic je připojen pomocí USB k počítači, pokud jej postavíme na ESP32, půjde to i bez drátů. Bylo sice potřeba udělat několik drobných úprav, ale vcelku to funguje.

2. Používání skriptů v jazyce python a debug stacku.

Tady bude těch triků vlastně povícero. Novější verze gdb umožňují spouštět python skripty a ty mohou definovat nové příkazy. Je to tedy spíš pro ty, kteří jsou zvyklí pracovat s gdb z příkazové řádky. Je to však docela užitečné - i když v pythonu jen spouštíme příkazy gdb, lze vrácená data dost dobře a jednoduše analyzovat. Nebo lze spustit příkazy gdb na základě toho, co se děje jinde v systému. Třeba podle toho jaký máme připojen adaptér.

Zde uvedu jednoduchý skript pro analýzu použití stacku. Protože píšu většinou v C++, kde je možné vytvářet i poměrně složité lokální objekty a ty se ukládají právě na stacku, může být tento dost obsazen a pro kontrolu je pak potřeba nějaký nástroj. Existují sice statické analyzátory, ale nejsou zas tak moc spolehlivé. Použitá metoda je primitivní:

A tuhle práci vykoná tento skript. Skript sice používá pro vyhledání použitého stacku metodu bisekce, protože je pro velký zásobník (>cca 8KiB) rychlejší, ale může ukázat nesmysl pokud struktury na zásobníku neinicializují data. Metodu výpisu do souboru a jeho následnou analýzu (method_file (adr_from, adr_to)) lze odkomentovat a použít místo method_bisect (adr_from, adr_to).

Příklad velmi zjednodušeného linker skriptu :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ENTRY(Reset_Handler)
MEMORY {
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
  RAM  (xrw) : ORIGIN = 0x20000000, LENGTH = 8K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* Define output sections */
SECTIONS {
  .text : {
    *(.text*)   /* .text* sections (code) */
    *(.rodata*) /* .rodata* sections (constants, strings, etc.) */
    _etext = .; /* define a global symbols at end of code */
  } >FLASH
  _sidata = .;
  .data : AT ( _sidata ) {
    . = ALIGN(4);
    _sdata = .; /* create a global symbol at data start */
    *(.data*)   /* .data* sections */
    . = ALIGN(4);
    _edata = .; /* define a global symbol at data end   */
  } >RAM
  .bss : {
    . = ALIGN(4);
    _sbss = .;  /* define a global symbol at bss start  */
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;  /* define a global symbol at bss end    */
  } >RAM
}

Příklad použití v kódu procesoru (startup.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extern unsigned int _estack;
extern unsigned int _sidata;
extern unsigned int _sdata;
extern unsigned int _edata;
extern unsigned int _sbss;
extern unsigned int _ebss;
static inline void fillStack (void) {
  register unsigned int *dst, *end;
  dst = &_ebss;
  end = &_estack;
  while (dst < end) *dst++ = 0xDEADBEEFU;
}
void Reset_Handler (void) {
  fillStack(); /* pokud neladíme stack, můžeme tt. řádek zakomentovat */
  /* Pak už to pokračuje asi takto ... */
  register unsigned int *src, *dst, *end;
  /* Zero fill the bss section */
  dst = &_sbss;
  end = &_ebss;
  while (dst < end) *dst++ = 0U;
  /* Copy data section from flash to RAM */
  src = &_sidata;
  dst = &_sdata;
  end = &_edata;
  while (dst < end) *dst++ = *src++;
  /*...*/
}

Pozn.: Pokud používáme heap (haldu), která bývá hned za .bss, bude nutné ještě najít podobnou metodou vrchol této haldy. Platí, že halda roste ve směru proti zásobníku až se nakonec potkají. Protože metody vytváření haldy jsou různé, nebudu se tím zabývat. Nakonec vždy se snažím pokud to jen trochu jde, použití haldy vyhnout, trochu se zde ztrácí determinizmus, přináší to víc problémů než užitku. Vytvářet lokální objekty na zásobníku sice také není bez problémů ale mám takový dojem, že cena, kterou je za to nutné zaplatit je menší. Uznávám však, že někdy se bez haldy obejít nelze.

3. Ladění aplikace v RAM.

Protože jádra CortexMx umožňují běh programu z RAM, nabízí se možnost ladit přímo v ní. Ani tak by nevadilo, že se FLASH opotřebovává, ale natažení programu do RAM pomocí SWD je mnohem rychlejší. A protože některé čipy mají RAM opravdu dost je nasnadě to zkusit. Co mi však dost vadilo bylo přepínání boot módu pomocí propojek (příp. options). Zde je trik, který umožní běh kompletní aplikace v RAM, přičemž ve FLASH zůstává jen malý kousek programu, který není potřeba přepisovat a bootuje se normálně z FLASH. Inicializační kód pro FLASH manipuluje s ukazatelem zásobníku a je proto potřeba napsat ho v assembleru :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  .syntax unified
  .cpu cortex-m3
  .fpu softvfp
  .thumb

  .global g_pfnVectors
  .global Reset_Handler
  .extern _sdata
  .extern _estack

  .section  .text.Reset_Handler
  .type Reset_Handler, %function
Reset_Handler:
  ldr   r0,user_code_begin  @ load sp and pc from user code (vector table)
  ldr   sp,[r0]             @ ldmia not support sp
  ldr   pc,[r0, #4]         @ jump to user

  .align 2
user_code_begin:
  .word _sdata
  .size Reset_Handler, .-Reset_Handler

  .section  .isr_vector,"a",%progbits
  .type g_pfnVectors, %object
g_pfnVectors:
  .word _estack
  .word Reset_Handler
  .size g_pfnVectors, .-g_pfnVectors

K tomu je použit velmi zjednodušený linker skript (definovat zde přesnou velikost RAM je zde docela zbytečné, ale neměla by přesahovat skutečnou délku):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* Entry Point */
ENTRY(Reset_Handler)

/* Specify the memory areas */
MEMORY {
  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 512K
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 8K
}

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);

/* Define output sections */
SECTIONS {
  .fixed :  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
  } >FLASH
  .relocate : {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data*)          /* .data* sections */
    _edata = .;        /* define a global symbol at data end */
  } >RAM
  /DISCARD/ : {
    *(.comment*)
    *(.ARM.*)
  }
  .ARM.attributes 0 : { *(.ARM.attributes) }
}

Což vygeneruje jen následujících 24 bytů (a to úplně stačí) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Disassembly of section .text:

08000000 <g_pfnVectors>:
 8000000: 00 20 00 20 09 00 00 08                             ... ....

08000008 <Reset_Handler>:
 8000008: 4802        ldr r0, [pc, #8]  ; (8000014 <user_code_begin>)
 800000a: f8d0 d000   ldr.w sp, [r0]
 800000e: f8d0 f004   ldr.w pc, [r0, #4]
 8000012: bf00        nop

08000014 <user_code_begin>:
 8000014: 20000000  .word 0x20000000

Vše vychází z toho, že FLASH začíná na 0x08000000 a RAM na 0x20000000. Uživatelský program je pak uložen kompletně od 0x20000000, a je nutné v něm nastavit na tuto hodnotu i počátek tabulky vektorů zápisem tt. hodnoty do registru SCB.VTOR ještě před povolením přerušení. Nebude to tedy fungovat na CortexM0. To by bylo asi vše.