Multi-core debugging with GDB in Renode


Topics: Open source tools

Antmicro‘s open source simulation framework, Renode, provides a familiar debugging experience to embedded development teams by serving as a target for remote GDB connections, which allows users to work with GDB or GDB-based IDEs as they normally would with hardware.

Before version 1.8 each CPU core had to be exposed with a separate server. This worked well for single-core systems, but with the growing portfolio of multi-core SoCs (like the heterogeneous 64-bit 5-core RISC-V U-540 core complex found in SiFive’s HiFive unleashed and Microsemi’s Polarfire SoC) we wanted to improve the user experience and make debugging even easier.

After all, in real life you’d expect to be connecting just one GDB instance even for a multi-core device.

API changes in Renode

Before we introduced this change, the standard way of creating a GDB server listening on port 3333 looked like this:

(machine-0) sysbus.cpu0 StartGdbServer 3333

As creating a GDB instance is now no longer tied to a single core but instead the entire device (machine in Renode terminology), this command obviously had to change, and StartGdbServer can now be used to create a server with either all the CPU cores of a machine, or with a subset of those.

By default, StartGdbServer now adds all CPU cores to the newly created server:

(machine-0) machine StartGdbServer 3333

It can be also used to create a new server with a specific CPU core, or to add a core to an already existing server. Thus, now Renode users will be able to create more complex setups, with multiple GDB instances running debug sessions with different CPU cores (or sets of cores). To give an example, this command will create a GDB server on port 3333 with one CPU:

(machine-0) machine StartGdbServer 3333 true sysbus.cpu1

To add a second CPU to that server, call:

(machine-0) machine StartGdbServer 3333 true sysbus.cpu2

To start a new GDB server on port 3334 with another CPU, call:

(machine-0) machine StartGdbServer 3334 true sysbus.cpu3

The true parameter indicates whether the emulation should autostart when we connect the first GDB client (and of course can be changed to false).

Running all the above commands in Renode will result in a setup consisting of two GDB servers - on port 3333 with two CPU cores, and on port 3334 with one. As you can see the GDB servers are differentiated by the port number.

Multi-core debugging with GDB

GDB Remote Protocol

The GDB server setup has to be prepared before you connect the GDB client (normally, you’d just put it in your Renode script file, .resc). Upon establishing the connection, Renode reports the number of available threads to GDB, which results in GDB operating in multithreaded mode.

To enable support for multi-core systems, Renode had to handle a bunch of new commands from the remote GDB protocol:

  • Hg, for switching thread context,
  • qAttached, for reporting that GDB connected to an existing process,
  • T, for reporting whether a thread is alive,
  • vCont?, for reporting supported multithread continuation operations,
  • vCont;, for managing multi-core continuation operations,
  • qC, for reporting the ID of the currently selected core.

For more detailed information about this, please refer to GDB’s remote protocol documentation.

Thanks to the new commands Renode can manage all of the configured threads (which represent CPU cores of the platform), and the debugger connected to Renode can switch between them if needed, as if it was connected to a physical device. For example, breakpoints and watchpoints can be set on different threads and Renode will switch between them on hits.

Before we introduced this change, if you wanted to debug an application running on 4 cores, you would start 4 GDB client instances and connect them to their respective cores. Now the usage is much simpler. After starting a GDB server with all cores in Renode, you can start GDB and run:

(gdb) target remote :3333
Remote debugging using :3333
(gdb) info threads 
  Id   Target Id                     Frame 
* 1    Thread 1 "machine-0.cpu1[0]" 0x08000000 in __reset ()
  2    Thread 2 "machine-0.cpu2[1]" 0x08000000 in __reset ()
  3    Thread 3 "machine-0.cpu3[2]" 0x08000000 in __reset ()
  4    Thread 4 "machine-0.cpu4[3]" 0x08000000 in __reset ()
(gdb) thread 2
[Switching to thread 2 (Thread 2)]
#0  0x08000000 in __reset ()

Integrate Renode into your everyday toolchain

Renode gives you great debugging and logging capabilities, but it also allows you to use the same workflow you are used to. By enabling standard tools, Renode can be integrated with IDEs and become an invaluable element of your toolchain.

We are always happy to hear about your use cases and see how we can tailor Renode to be used both in your day-to-day development and in your testing infrastructure. Let us know how you work, and we’ll advise on options:

See Also: