---
title: "GDB tutorial"
canonical_url: https://developer.repebble.com/pebbleos/docs/development/debugging/gdb_tutorial/
last_updated: 2026-10-29
sdk_version: 4.33.1
---
# GDB tutorial

Source: https://developer.repebble.com/pebbleos/docs/development/debugging/gdb_tutorial/

This tutorial debugs the firmware in the emulator with GDB, on the command line. Nothing in it needs a watch, and on a watch only starting the session differs: see [On a watch](#on-a-watch). The native simulator runs under LLDB on macOS, with the same steps in other words: see [In the native simulator](#in-the-native-simulator).

## Start a session

Build the firmware for the emulator and start it:

```
pbl configure --board qemu_emery
pbl build
pbl run
```

In another terminal, attach GDB:

```
pbl debug
```

GDB stops the firmware wherever it was, most of the time idling:

```
Remotedebuggingusing:1233\_\_ISB()at./third\_party/cmsis\_core/CMSIS/CMSIS/Core/Include/cmsis\_gcc.h:266266\_\_ASMvolatile("isb 0xF":::"memory");(gdb)
```

The emulator is frozen until `continue`. `Ctrl-C` stops it again, and `quit` detaches GDB and leaves the firmware running.

## Look at the threads

Each kernel thread is a GDB thread, with its name and state:

```
(gdb)infothreadsIdTargetIdFrame\*1Thread1.1(IDLE0x2001E8B0:Running)pbl\_soc\_idle(max\_ticks=max\_ticks@entry=33)at./soc/qemu/idle.c:193Thread1.536990144(PULSE0x2001D1C0:Blocked)0x00143666inarch\_irq\_enable()at./kernel/arch/arm/arch.c:2134Thread1.536989952(NewTimer0x2001D100:Blocked)0x00143666inarch\_irq\_enable()at./kernel/arch/arm/arch.c:2135Thread1.536989376(NimbleHost0x2001CEC0:Blocked)0x00143666inarch\_irq\_enable()at./kernel/arch/arm/arch.c:2136Thread1.536989184(App\<Launcher\>0x2001CE00:Blocked)0x00143666inarch\_irq\_enable()at./kernel/arch/arm/arch.c:213...
```

A blocked thread’s stack shows what it waits for. Switch to one and print its backtrace:

```
(gdb)thread5[Switchingtothread5(Thread1.536989376)]#0 0x00143666 in arch\_irq\_enable () at ./kernel/arch/arm/arch.c:213213}(gdb)bt#0 0x00143666 in arch\_irq\_enable () at ./kernel/arch/arm/arch.c:213#1 0x000ecad0 in pbl\_irq\_unlock () at ./kernel/irq.c:16#2 0x000eceee in sched\_block (wq=wq@entry=0x2000cb38 \<g\_eventq\_dflt+20\>, timeout=timeout@entry=...)at./kernel/sched.c:192#3 0x0014333a in pbl\_msgq\_get (q=0x2000cb24 \<g\_eventq\_dflt\>, msg=msg@entry=0x2000336c \<s\_host\_task\_stack+4972\>,timeout=...)at./kernel/msgq.c:81#4 0x00014bee in npl\_pebble\_eventq\_get (evq=evq@entry=0x2000cb24 \<g\_eventq\_dflt\>, tmo=tmo@entry=4294967295)at./third\_party/nimble/port/src/npl\_os\_pebble.c:31#5 0x00016270 in ble\_npl\_eventq\_get (evq=0x2000cb24 \<g\_eventq\_dflt\>, tmo=4294967295)at./third\_party/nimble/port/include/nimble/nimble\_npl\_os.h:98#6 prv\_host\_task\_main (unused=\<optimized out\>) at ./subsys/bluetooth/init.c:104
```

The Bluetooth host thread is waiting on its event queue.

## Set breakpoints

A breakpoint stops the firmware when it reaches a function, or a line of a file:

```
(gdb)breakFUNCTION(gdb)breakFILE:LINE(gdb)continue
```

`tbreak` sets one that is deleted once hit, `condition N EXPR` makes breakpoint `N` stop only when `EXPR` is true, `info breakpoints` lists them and `delete` removes them all. GDB reports the stop with the function and its arguments, and the line about to run, e.g. on `event_put`:

```
Breakpoint1,event\_put(event=event@entry=0x2000960c\<s\_new\_timer\_stack+1292\>)at./fw/kernel/events.c:222222uintptr\_tsaved\_lr=(uintptr\_t)PBL\_RETURN\_ADDRESS(0);
```

## Inspect a stop

`bt` prints how the firmware got there; `bt N` only the innermost `N` frames:

```
(gdb)bt4#0 event\_put (event=event@entry=0x2000960c \<s\_new\_timer\_stack+1292\>) at ./fw/kernel/events.c:222#1 0x12154dce in launcher\_task\_add\_callback (callback=\<optimized out\>, data=\<optimized out\>) at ./fw/kernel/event\_loop.c:95#2 0x120982b0 in task\_timer\_manager\_execute\_expired\_timers (manager=manager@entry=0x2002bc60 \<s\_task\_timer\_manager\>) at ./fw/kernel/task\_timer.c:350#3 0x1206cb86 in new\_timer\_service\_loop (data=\<optimized out\>) at ./fw/services/new\_timer/new\_timer.c:87(Morestackframesfollow...)
```

`frame N` moves to another frame, where `info args` and `info locals` show its arguments and variables. `print` evaluates any C expression, following pointers and calling firmware functions:

```
(gdb) print event->type
$1 = PEBBLE_CALLBACK_EVENT
(gdb) print pbl_cur->name
$2 = "NewTimer\000\000\000\000\000\000\000"
```

`x/8x ADDRESS` dumps memory, and `display EXPR` prints an expression at every stop. The firmware is optimized for size, so some variables are `<optimized out>`; building with `-DCONFIG_DEBUG_OPTIMIZATIONS=y` keeps more of them.

## Step

From a stop, `next` runs to the next line, stepping over calls, `step` into them, `finish` to the end of the current function, and `until LINE` to a later line. Interrupts and other threads can run during a step, so it can stop somewhere else when a breakpoint hits there. `continue` lets the firmware run until the next breakpoint or `Ctrl-C`.

## Use the PebbleOS commands

The `pbl` commands read the firmware’s own structures. `pbl` lists them; a few examples:

```
(gdb)pblheapstats&s\_kernel\_heapHeapstart0x20023780Heapend0x2004377cHeaptotalsize131068Heapallocated14192Heaphighwatermark51900Heapfreeblocks:116876bytes,8blocksHeapallocblocks:14192bytes,64blocksHeaplargestfreeblock:77720(gdb)pblmetadataBuildTimestamp:2026-10-2909:54:22+01:00VersionTag:"v4.43.0-131-ga855823dd8"VersionShort:"a855823"IsRecovery:FalseHWPlatform:NoneHWBoardName:FirmwareMetadataPlatformPebbleEmeryEmuHWBoardNum:245
```

`pbl layer-tree` prints the layers of the window on screen, and `pbl to_png` saves the framebuffer as an image.

## Leave

`quit` detaches GDB and stops the proxy; the firmware carries on:

```
(gdb)quitDetachingfrom program:build/pebbleos.elf,process1Endingremotedebugging.[Inferior1(process1)detached]
```

## On a watch

The session is the same on a watch, once the firmware is flashed with [pbl run](https://developer.repebble.com/pebbleos/docs/pbl/run.md) and the build is the one on the watch. Release builds are not meant for debugging. Attach:

```
pbl debug
```

A serial-based debugger also needs the programmer’s port, given with `--tty`; [Debugging](https://developer.repebble.com/pebbleos/docs/development/debugging.md) covers what to look out for with those.

The watch stops for real while GDB has it: its display freezes and it may drop its Bluetooth connection if it stays stopped for long. The core has a handful of hardware breakpoints, which GDB uses for code in flash, so only that many breakpoints can be set at a time.

## In the native simulator

Build a native board and run it under the debugger:

```
pbl configure --board native_emery
pbl build
pbl debug
```

The program waits for `run` (`r`) to start. The steps above apply, with LLDB’s commands on macOS:

| 

GDB

 | 

LLDB

 |
| --- | --- |
| 

`break FUNCTION`

 | 

`b FUNCTION`

 |
| 

`continue`

 | 

`c`

 |
| 

`next`

 | 

`n`

 |
| 

`bt 4`

 | 

`bt 4`

 |
| 

`info threads`

 | 

`thread list`

 |
| 

`thread 5`

 | 

`thread select 5`

 |
| 

`print EXPR`

 | 

`p EXPR`

 |
| 

`info locals`

 | 

`frame variable`

 |

Each kernel thread is a host thread, named after it:

```
(lldb)bapp\_manager\_launch\_new\_app(lldb)rProcess84855stopped\*thread#10, name = 'KernelMain', stop reason = breakpoint 1.1frame#0: 0x00000001000f9a28 pebbleos`app\_manager\_launch\_new\_app(config=0x00000001702e6d70) at app\_manager.c:713:43 [opt](lldb)threadlist...\*thread#10: tid = 0x3c26550, ... app\_manager\_launch\_new\_app(config=0x00000001702e6d70) at app\_manager.c:713:43, name = 'KernelMain'thread#11: tid = 0x3c26551, ... libsystem\_kernel.dylib`\_\_psynch\_cvwait + 8, name = 'NewTimer'thread#15: tid = 0x3c2655a, ... libsystem\_kernel.dylib`\_\_psynch\_cvwait + 8, name = 'IDLE'thread#17: tid = 0x3c2657a, ... libsystem\_kernel.dylib`\_\_psynch\_cvwait + 8, name = 'NimbleHost'thread#21: tid = 0x3c2657e, ... libsystem\_kernel.dylib`\_\_psynch\_cvwait + 8, name = 'App \<Launcher\>'
```

The unnamed threads are the host’s: the window, the timer, the serial ports. `Ctrl-C` stops the firmware; the console is on TCP port 12345 meanwhile, where [pbl console](https://developer.repebble.com/pebbleos/docs/pbl/console.md) attaches.

Generated from PebbleOS [7e4f46eff](https://github.com/coredevices/pebbleos/commit/7e4f46effe247ce11eeee629a391e1d6a4988ef0). This page is maintained in the pebbleos repository: [docs/development/debugging/gdb\_tutorial.md](https://github.com/coredevices/pebbleos/blob/7e4f46effe247ce11eeee629a391e1d6a4988ef0/docs/development/debugging/gdb_tutorial.md).
