pebble/devsite/source/_guides/debugging/debugging-with-gdb.md
2025-02-24 18:58:29 -08:00

9.3 KiB

title description guide_group order
Debugging with GDB How to use GDB to debug a Pebble app in the emulator. debugging 0

As of SDK 3.10 (and Pebble Tool 4.2), developers can use the powerful GDB debugging tool to find and fix errors in Pebble apps while they are running in an emulator. GDB allows the user to observe the state of the app at any point in time, including the value of global and local variables, as well as current function parameters and a backtrace. Strategically placing breakpoints and observing these values can quickly reveal the source of a bug.

{% alert notice %} GDB cannot be used to debug an app running on a real watch. {% endalert %}

Starting GDB

To begin using GDB, start an emulator and install an app:

$ pebble install --emulator basalt

Once the app is installed, begin using GDB:

$ pebble gdb --emulator basalt

Once the (gdb) prompt appears, the app is paused by GDB for observation. To resume execution, use the continue (or c) command. Similarly, the app can be paused for debugging by pressing control + c.

(gdb) c
Continuing.

A short list of useful commands (many more are available) can be also be obtained from the pebble tool. Read the Emulator Interaction section of the {% guide_link tools-and-resources/pebble-tool %} guide for more details on this list.

$ pebble gdb --help

Observing App State

To see the value of variables and parameters at any point, set a breakpoint by using the break (or b) command and specifying either a function name, or file name with a line number. For example, the snippet below shows a typical TickHandler implementation with line numbers in comments:

/* 58 */  static void tick_handler(struct tm *tick_time, TimeUnits changed) {
/* 59 */    int hours = tick_time->tm_hour;
/* 60 */    int mins = tick_time->tm_min;
/* 61 */
/* 62 */    if(hours < 10) {
/* 63 */      /* other code */
/* 64 */    }
/* 65 */  }

To observe the values of hours and mins, a breakpoint is set in this file at line 61:

(gdb) b main.c:61
Breakpoint 2 at 0x200204d6: file ../src/main.c, line 61.

Use info break to see a list of all breakpoints currently registered. Each can be deleted with delete n, where n is the breakpoint number.

With this breakpoint set, use the c command to let the app continue until it encounters the breakpoint:

$ c
Continuing.

When execution arrives at the breakpoint, the next line will be displayed along with the state of the function's parameters:

Breakpoint 2, tick_handler (tick_time=0x20018770, units_changed=(SECOND_UNIT | MINUTE_UNIT))
    at ../src/main.c:62
62    if(hours < 10) {

The value of hours and mins can be found using the info locals command:

(gdb) info locals
hours = 13
mins = 23

GDB can be further used here to view the state of variables using the p command, such as other parts of the tm object beyond those being used to assign values to hours and mins. For example, the day of the month:

(gdb) p tick_time->tm_mday
$2 = 14

A backtrace can be generated that describes the series of function calls that got the app to the breakpoint using the bt command:

(gdb) bt
#0  segment_logic (this=0x200218a0) at ../src/drawable/segment.c:18
#1  0x2002033c in digit_logic (this=0x20021858) at ../src/drawable/digit.c:141
#2  0x200204c4 in pge_logic () at ../src/main.c:29
#3  0x2002101a in draw_frame_update_proc (layer=<optimized out>, ctx=<optimized out>)
    at ../src/pge/pge.c:190
#4  0x0802627c in ?? ()
#5  0x0805ecaa in ?? ()
#6  0x0801e1a6 in ?? ()
#7  0x0801e24c in app_event_loop ()
#8  0x2002108a in main () at ../src/pge/pge.c:34
#9  0x080079de in ?? ()
#10 0x00000000 in ?? ()

Lines that include '??' denote a function call in the firmware. Building the app with pebble build --debug will disable some optimizations and can produce more readable output from GDB. However, this can increase code size which may break apps that are pushing the heap space limit.

Fixing a Crash

When an app is paused for debugging, the developer can manually advance each statement and precisely follow the path taken through the code and observe how the state of each variable changes over time. This is very useful for tracking down bugs caused by unusual input to functions that do not adequately check them. For example, a NULL pointer.

The app code below demonstrates a common cause of an app crash, caused by a misunderstanding of how the Window stack works. The TextLayer is created in the .load handler, but this is not called until the Window is pushed onto the stack. The attempt to set the time to the TextLayer by calling update_time() before it is displayed will cause the app to crash.

#include <pebble.h>

static Window *s_window;
static TextLayer *s_time_layer;

static void window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  s_time_layer = text_layer_create(bounds);
  text_layer_set_text(s_time_layer, "00:00");
  text_layer_set_text_alignment(s_time_layer, GTextAlignmentCenter);
  layer_add_child(window_layer, text_layer_get_layer(s_time_layer));
}

static void update_time() {
  time_t now = time(NULL);
  struct tm *tick_time = localtime(&now);

  static char s_buffer[8];
  strftime(s_buffer, sizeof(s_buffer), "%H:%M", tick_time);
  text_layer_set_text(s_time_layer, s_buffer);
}

static void init() {
  s_window = window_create();
  window_set_window_handlers(s_window, (WindowHandlers) {
    .load = window_load
  });

  update_time();

  window_stack_push(s_window, true);
}

static void deinit() {
  window_destroy(s_window);
}

int main() {
  init();
  app_event_loop();
  deinit();
}

Supposing the cause of this crash was not obvious from the order of execution, GDB can be used to identify the cause of the crash with ease. It is known that the app crashes on launch, so the first breakpoint is placed at the beginning of init(). After continuing execution, the app will pause at this location:

(gdb) b init
Breakpoint 2 at 0x2002010c: file ../src/main.c, line 26.
(gdb) c
Continuing.

Breakpoint 2, main () at ../src/main.c:41
41    init();

Using the step command (or Enter key), the developer can step through all the statements that occur during app initialization until the crash is found (and the app_crashed breakpoint is encountered. Alternatively, bt full can be used after the crash occurs to inspect the local variables at the time of the crash:

(gdb) c
Continuing.

Breakpoint 1, 0x0804af6c in app_crashed ()
(gdb) bt full
#0  0x0804af6c in app_crashed ()
No symbol table info available.
#1  0x0800bfe2 in ?? ()
No symbol table info available.
#2  0x0800c078 in ?? ()
No symbol table info available.
#3  0x0804c306 in ?? ()
No symbol table info available.
#4  0x080104f0 in ?? ()
No symbol table info available.
#5  0x0804c5c0 in ?? ()
No symbol table info available.
#6  0x0805e6ea in text_layer_set_text ()
No symbol table info available.
#7  0x20020168 in update_time () at ../src/main.c:22
        now = 2076
        tick_time = <optimized out>
        s_buffer = "10:38\000\000"
#8  init () at ../src/main.c:31
No locals.
#9  main () at ../src/main.c:41
No locals.
#10 0x080079de in ?? ()
No symbol table info available.
#11 0x00000000 in ?? ()
No symbol table info available.

The last statement to be executed before the crash is a call to text_layer_set_text(), which implies that one of its input variables was bad. It is easy to determine which by printing local variable values with the p command:

Breakpoint 4, update_time () at ../src/main.c:22
22    text_layer_set_text(s_time_layer, s_buffer);
(gdb) p s_time_layer
$1 = (TextLayer *) 0x0 <__pbl_app_info>

In this case, GDB displays 0x0 (NULL) for the value of s_time_layer, which shows it has not yet been allocated, and so will cause text_layer_set_text() to crash. And thus, the source of the crash has been methodically identified. A simple fix here is to swap update_time() and window_stack_push() around so that init() now becomes:

static void init() {
  // Create a Window
  s_window = window_create();
  window_set_window_handlers(s_window, (WindowHandlers) {
    .load = window_load
  });

  // Display the Window
  window_stack_push(s_window, true);

  // Set the time
  update_time();
}

In this new version of the code the Window will be pushed onto the stack, calling its .load handler in the process, and the TextLayer will be allocated and available for use once execution subsequently reaches update_time().