General confusion/problems with LLDB debugging Cortex M0

I was worried my other post on the subject was getting off-topic. I’ve been trying to use lldb to debug embedded ARM targets like the Cortex M0. I’ve had some success.

TL;dr:

  • Can I use lldb to debug tiny MCUs like a Feather M0 (ARM Cortex M0)? It seems to work, but has many issues.
  • What does gdb-remote do that platform connect doesn’t?
  • Why does LLDB think there’s a bad CPU type in the executable when I try to run?
  • How do I tell LLDB to reset the processor and stop at the first instruction?
  • thread step-over is very unreliable, am I missing something?
  • Why aren’t my breakpoints being hit?
  • Does lldb know when code is Thumb vs ARM?

Details

I can connect and inspect a little bit:

$ lldb /var/folders/16/jfb809_s2fz01ql7k494f6pc0000gn/T/arduino_build_98039/Blink.ino.elf
(lldb) target create "/var/folders/16/jfb809_s2fz01ql7k494f6pc0000gn/T/arduino_build_98039/Blink.ino.elf"
Current executable set to '/var/folders/16/jfb809_s2fz01ql7k494f6pc0000gn/T/arduino_build_98039/Blink.ino.elf' (arm).
(lldb) target list
Current targets:
* target #0: /var/folders/16/jfb809_s2fz01ql7k494f6pc0000gn/T/arduino_build_98039/Blink.ino.elf ( arch=arm-*-*-eabi, platform=host )
(lldb) platform select remote-gdb-server
  Platform: remote-gdb-server
 Connected: no
(lldb) platform connect connect://localhost:3333
  Platform: remote-gdb-server
  Hostname: (null)
 Connected: yes
(lldb) list
(lldb) l
   44  	  TinyUSB_Device_Init(0);
   45  	#elif defined(USBCON)
   46  	  USBDevice.init();
   47  	  USBDevice.attach();
   48  	#endif
   49  	
   50  	  setup();
   51  	
   52  	  for (;;)
   53  	  {
(lldb) c
error: Process must be launched.

But as you can see, lldb’s state isn’t quite complete (i.e. it thinks there’s no running process). Then I discovered gdb-remote, which seems to behave differently from platform connect:

(lldb) platform disconnect 
Disconnected from "remote-gdb-server"
(lldb) gdb-remote 3333
Process 1 stopped
* thread #1, stop reason = signal SIGINT
    frame #0: 0x00002280 Blink.ino.elf`micros at delay.c:54:13
   51  	    ticks=ticks2;
   52  	    pend=pend2;
   53  	    count=count2;
-> 54  	    ticks2  = SysTick->VAL;
   55  	    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
(lldb) n
Process 1 stopped
* thread #1, stop reason = step over
    frame #0: 0x00002282 Blink.ino.elf`micros at delay.c:55:21
   52  	    pend=pend2;
   53  	    count=count2;
   54  	    ticks2  = SysTick->VAL;
-> 55  	    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
   58  	

I was able to connect to the target, see the current line of code, step to the next.

Getting overly curious, I tried platform process list, but it showed all the processes on the host Mac.

So I tried l:

(lldb) l
   59  	  return ((count+pend) * 1000) + (((SysTick->LOAD  - ticks)*(1048576/(VARIANT_MCK/1000000)))>>20) ;
   60  	  // this is an optimization to turn a runtime division into two compile-time divisions and
   61  	  // a runtime multiplication and shift, saving a few cycles
   62  	}
   63  	
   64  	#ifdef __SAMD51__
   65  	/*
(lldb) l
   66  	 * On SAMD51, use the (32bit) cycle count maintained by the DWT unit,
   67  	 * and count exact number of cycles elapsed, rather than guessing how
   68  	 * many cycles a loop takes, which is dangerous in the presence of
   69  	 * cache.  The overhead of the call and internal code is "about" 20
   70  	 * cycles.  (at 120MHz, that's about 1/6 us)
   71  	 */
   72  	void delayMicroseconds(unsigned int us)
(lldb) l
   73  	{
   74  	  uint32_t start, elapsed;
   75  	  uint32_t count;
   76  	
   77  	  if (us == 0)
   78  	    return;
   79  	
(lldb) l
   80  	  count = us * (VARIANT_MCK / 1000000) - 20;  // convert us to cycles.
   81  	  start = DWT->CYCCNT;  //CYCCNT is 32bits, takes 37s or so to wrap.
   82  	  while (1) {
   83  	    elapsed = DWT->CYCCNT - start;
   84  	    if (elapsed >= count)
   85  	      return;
   86  	  }

Okay, cool, I can list code (although sometimes I get lldb into a state where l does nothing).

Control-C stops it. c continues. bt shows a stack trace. up goes up a frame. r prompts me to kill and re-run the process, then chokes with “Bad CPU type in executable”:

(lldb) r
There is a running process, kill it and restart?: [Y/n] y
Process 1 exited with status = 6 (0x00000006) unexpected response to k packet: OK
error: Bad CPU type in executable

Meanwhile openocd shows:

Info : dropped 'gdb' connection

Stepping

So I reconnect, and the processor is still halted where it was before

(lldb) gdb-remote 3333
Process 1 stopped
* thread #1, stop reason = signal SIGTRAP
    frame #0: 0x0000229a Blink.ino.elf`micros at delay.c:59:17
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
   58  	
-> 59  	  return ((count+pend) * 1000) + (((SysTick->LOAD  - ticks)*(1048576/(VARIANT_MCK/1000000)))>>20) ;
   60  	  // this is an optimization to turn a runtime division into two compile-time divisions and
   61  	  // a runtime multiplication and shift, saving a few cycles
   62  	}
(lldb) bt
* thread #1, stop reason = signal SIGTRAP
  * frame #0: 0x0000229a Blink.ino.elf`micros at delay.c:59:17
    frame #1: 0x000022e0 Blink.ino.elf`delay(ms=180) at delay.c:103:23
    frame #2: 0x0000212a Blink.ino.elf`::loop() at Blink.ino:36:8
    frame #3: 0x00002eda Blink.ino.elf`main at main.cpp:54:9
    frame #4: 0x00002218 Blink.ino.elf`Reset_Handler at cortex_handlers.c:496:3

Okay, let’s get up to my main code:

(lldb) up
frame #1: 0x000022e0 Blink.ino.elf`delay(ms=180) at delay.c:103:23
   100 	  while (ms > 0)
   101 	  {
   102 	    yield();
-> 103 	    while (ms > 0 && (micros() - start) >= 1000)
   104 	    {
   105 	      ms--;
   106 	      start += 1000;
(lldb) up
frame #2: 0x0000212a Blink.ino.elf`::loop() at Blink.ino:36:8
   33  	  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
   34  	  delay(1000);                       // wait for a second
   35  	  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
-> 36  	  delay(1000);                       // wait for a second
   37  	}

Let’s step over this call:

(lldb) n
Process 1 stopped
* thread #1, stop reason = step over
    frame #0: 0x00002282 Blink.ino.elf`micros at delay.c:55:21
   52  	    pend=pend2;
   53  	    count=count2;
   54  	    ticks2  = SysTick->VAL;
-> 55  	    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
   58  	

Hmm, no, that’s not where it should be. Let’s try that again:

(lldb) up
frame #1: 0x000022e0 Blink.ino.elf`delay(ms=859) at delay.c:103:23
   100 	  while (ms > 0)
   101 	  {
   102 	    yield();
-> 103 	    while (ms > 0 && (micros() - start) >= 1000)
   104 	    {
   105 	      ms--;
   106 	      start += 1000;
(lldb) up
frame #2: 0x0000212a Blink.ino.elf`::loop() at Blink.ino:36:8
   33  	  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
   34  	  delay(1000);                       // wait for a second
   35  	  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
-> 36  	  delay(1000);                       // wait for a second
   37  	}
(lldb) n
(lldb) 

Hmm. The code is running again. I’ve experienced this sort of thing in Xcode targeting a Swift app on my local Mac a lot: I’ll try to step over code, and it will just continue running.

Let’s try again:

Control-C
Process 1 stopped
* thread #1, stop reason = signal SIGINT
    frame #0: 0x00002280 Blink.ino.elf`micros at delay.c:54:13
   51  	    ticks=ticks2;
   52  	    pend=pend2;
   53  	    count=count2;
-> 54  	    ticks2  = SysTick->VAL;
   55  	    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
(lldb) up
frame #1: 0x000022e0 Blink.ino.elf`delay(ms=921) at delay.c:103:23
   100 	  while (ms > 0)
   101 	  {
   102 	    yield();
-> 103 	    while (ms > 0 && (micros() - start) >= 1000)
   104 	    {
   105 	      ms--;
   106 	      start += 1000;
(lldb) up
frame #2: 0x0000211c Blink.ino.elf`::loop() at Blink.ino:34:8
   31  	// the loop function runs over and over again forever
   32  	void loop() {
   33  	  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
-> 34  	  delay(1000);                       // wait for a second
   35  	  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
   36  	  delay(1000);                       // wait for a second
   37  	}
(lldb) n
Process 1 stopped
* thread #1, stop reason = step over
    frame #0: 0x00002282 Blink.ino.elf`micros at delay.c:55:21
   52  	    pend=pend2;
   53  	    count=count2;
   54  	    ticks2  = SysTick->VAL;
-> 55  	    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
   56  	    count2  = _ulTickCount ;
   57  	  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));
   58  	

This time n stopped, but not on line 35. Instead, it stopped somewhere down inside the call on line 34. This is what usually happens when I try to step over the current source line.

Breakpoints

Okay, let’s try some breakpoints:

frame #1: 0x0000212a Blink.ino.elf`::loop() at Blink.ino:36:8
   33  	  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
   34  	  delay(1000);                       // wait for a second
   35  	  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
-> 36  	  delay(1000);                       // wait for a second
   37  	}
(lldb) breakpoint list
No breakpoints currently set.
(lldb) breakpoint set --file Blink.ino --line 34
Breakpoint 1: where = Blink.ino.elf`::loop() + 2 at Blink.ino:34:8, address = 0x0000210a
(lldb) breakpoint list
Current breakpoints:
1: file = 'Blink.ino', line = 34, exact_match = 0, locations = 1, resolved = 1, hit count = 0
  1.1: where = Blink.ino.elf`::loop() + 2 at Blink.ino:34:8, address = 0x0000210a, resolved, hit count = 0 
(lldb) c
Process 1 resuming

LED resumes blinking, breakpoint is ignored.

I realize there could be issues in OpenOCD or how I’m invoking that. It does seem to recognize the hardware:

$ openocd -f interface/cmsis-dap.cfg -f target/at91samdXX.cfg -c "init;reset init"
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd". To override use 'transport select <transport>'.
Warn : could not read product string for device 0x2222:0x0043: Pipe error
Info : CMSIS-DAP: SWD  Supported
Info : CMSIS-DAP: FW Version = 0254.2
Info : CMSIS-DAP: Serial# = 310436023538a0ab0533363639323946a5a5a5a597969908
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 400 kHz
Info : SWD DPIDR 0x0bc11477
Info : at91samd.cpu: hardware has 4 breakpoints, 2 watchpoints
Info : starting gdb server for at91samd.cpu on 3333
Info : Listening on port 3333 for gdb connections
target halted due to debug-request, current mode: Thread 
xPSR: 0x41000000 pc: 0x00000294 msp: 0x20002de0
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : accepting 'gdb' connection on tcp/3333
Info : SAMD MCU: SAMD21G18A (256KB Flash, 32KB RAM)
Warn : Prefer GDB command "target extended-remote 3333" instead of "target remote 3333"
Info : SWD DPIDR 0x0bc11477
Error: Failed to read memory at 0xff4f7400

ARM vs Thumb?

Hmm, after disconnecting and reconnecting (also resetting the MCU and restarting openocd):

(lldb) gdb-remote 3333
Process 1 stopped
* thread #1, stop reason = signal SIGINT
    frame #0: 0x00000294 Blink.ino.elf
->  0x294: stmdami r5!, {r2, r5, r8, r11, lr}
    0x298: addmi  r11, r1, #112, #10
    0x29c: blmi   0x9342cc
    0x2a0: andhs  r1, r0, #196, #28

If I step over this instruction:

(lldb) thread step-inst-over
Process 1 stopped
* thread #1, stop reason = instruction step over
    frame #0: 0x00000296 Blink.ino.elf
->  0x296: ldrblt r4, [r0, #-0x825]!
    0x29a: andle  r4, r10, r1, lsl #5
    0x29e: vdivne.f64 d20, d4, d20
    0x2a2: adcmi  r2, r3, #0, #4

Notice that the PC was 0x294, and now it’s 0x296 (two bytes later, implying Thumb instructions). But LLDB is showing 4-byte (ARM) instructions in the disassembly.

Anyway, I’m running out of steam for now.

This is about what I’d expect. I know we (Linaro) did some work with OpenOCD and lldb a while back but we didn’t get to the point of calling it supported.

There is some noise around Rust and Cortex-M debug but I don’t know the status.

In general it can depend a lot on what the debug server sends us as some awareness is in the client some in the server, and servers outside of lldb-server (OpenOCD being one) are rarelty tested with lldb.

(lldb) help platform connect
Select the current platform by providing a connection URL.

Which is great if you already know what a platform is :slight_smile: A platform is essentially a server that can spawn other servers on demand. So you connect and say you’d like to load a program, it will start a process in “gdbserver mode” that debugs that process, and you connect to that.

I think gdb has a mode like this but I wouldn’t bet on connecting lldb to a gdbserver in platform mode working.

So that command is for connecting to an lldb-server running in platform mode.

$ ./bin/lldb-server --help
Usage:
  ./bin/lldb-server v[ersion]
  ./bin/lldb-server g[dbserver] [options]
  ./bin/lldb-server p[latform] [options]

gdb-remote is for connecting to a server in “gdbserver mode” meaning it’s connected to a process.

Platform:

lldb-server platform
    -> lldb-server gdbserver
        -> process under debug

gdb-remote:

lldb-server gdbserver
    -> process under debug
1 Like

Only a guess but it could be trying to load the program file locally instead of loading it onto the remote server.

If the program file is loaded into the debug server first, then the debugger is connected to it, things can get confused if the debugger assumes it can just fork a new process like it could locally.

I don’t think you can. Do you know of a GDB way to do that? Perhaps OpenOCD has a channel you can send these sort of commands.

I admit when I’ve debugged bare metal with qemu, I would just restart the entire thing. Brute force but at least qemu has a flag to stop on first instruction.

All three of those are likely linked. LLDB does have the smarts to tell Arm from Thumb, but as before, if the debug server is sending us things we don’t expect it’s easy to break things like this.

Finding the underlying reason would require looking at the logs and the packets exchanged with OpenOCD.

In summary:

  • I’m not too surprised it has issues given the focus of lldb development (Apple aside).
  • We (Linaro) would be happy to see it improve but don’t have resource to work on it ourselves at this time.
  • Fixing it needs someone to pour over the logs.
  • If anyone does work on it we can provide reviews and find you any Arm documentation you might need.
1 Like

And if it wasn’t clear, the use of platform mode is you can spawn a bunch of debug sessions from one platform. It will persist even if they all quit.

1 Like

Thanks. When I revisit this after the side quest I’m currently on, I’ll keep that in mind!