Systems & Design
SPONSOR BLOG

printf(“I Like”);

Debugging software isn’t always clean, but there are some proven methods that get the job done.

popularity

By Achim Nohl
Debugging software by adding printf statements in the code is not considered the cleanest and most advanced debugging approach, but when you are searching for the root cause of a problem you often look to the debugging method you are most familiar with and can apply easily.

The hurdle of setting up a complex debug or trace tool is counterproductive when dealing with schedule commitments and printf is the fallback method, which almost always works as long as you are able to modify and rebuild the software source code. I recently was talking to a software engineer who was working on driver development for a new WiFi chip. When I asked how he is debugging the driver, he answered, “I am doing printk debugging” (printk: debug messages inside the Linux kernel).

The reason behind his decision was to avoid the complicated steps involved in setting up a kernel debugger such as kdb. Luckily, the entire Linux kernel is full of printk messages, just like each and every module of the Android source code is instrumented with “log” messages. The beauty of this kind of debugging is that it can be used to expose the semantics of the software and identify what the software is actually doing. Instead of tracing functions like foo and bar you get useful information such as, “Probing device returned false!” message. It becomes immediately clear what is going on, even if the code was written by somebody else.

However, there are limitations, drawbacks and side effects to consider:

  1. When using printf statements in low-level software, such as bare metal firmware, boot-loaders and hypervisors, the kernel is often restricted by the number of serial interfaces (UARTs) available. Each software layer on each CPU needs its own UART.
  2. Sometimes you cannot modify or recompile the code to add printf statements.
  3. Use of printf statements have an intrusive impact on software execution and may hide defects in parallel software, and finally, they are not applicable for performance debugging. In the context of embedded software, “semihosting” is a well-known mechanism to allow embedded software communication directly with the host computer debugger. A special C-library is linked to the code which routes the printf messages to the host side debugger. Here, the semihosting printf implementation triggers a software exception that is trapped and handled by the debugger to display the message. By using semihosting, the number of debug channels is no longer limited. Unfortunately, even semihosting is not always practical. Semihosting requires you to re-link your software and has an intrusive impact on the software. Suddenly, some bugs disappear and maybe new bugs hit the surface. Also, the software performance is impacted and performance sensitive code such as a scheduler cannot be debugged.

Using virtual prototyping semihosting can be implemented in an almost non-intrusive fashion, without changing the original code. When using a virtual prototype, no special software exceptions are needed to trigger the debugger to catch the debug message. Instead, a breakpoint is directly set at the printf function in your embedded software stack. Unlike hardware, breakpoints in software can be set anywhere, even in ROM code; there are no limitations. The breakpoint is not used to stop the simulation, but instead to trigger automated actions on the host. Synopsys Virtual Prototypes provide a TCL based scripting interface to describe such actions.

Below is the code required to add semi hosting:

# Create a breakpoint at the embedded printk function
set vp_breakpoint [create_breakpoint printk]
# Trigger a an action on the host which displays the message
$vp_breakpoint set_callback vp_printk_action
# Define the callback action
proc vp_printk_action {} {
# Get the message address in the memory from the CPU register
set string_address [A15/cpu0/R0 get_value]
# Read the message from memory and display it.
puts [read_string_from_memory_at $string_address]
}

Using this method requires no modifications or recompilation of the embedded software. And yes, even though not shown here, formatted strings are also supported. Still, the printf code is intrusive since the implementation of printf (using a serial interface) is still executed. In order to overcome this drawback, only one additional line of code is needed in our callback action.

# Immediately exit printf by setting the program counter (R15)
# to the return address (R14, linked register)
A15/cpu0/R set_value [ A15/cpu0/R get_value 14 ] 15

Now, our printf semihosting is almost non-intrusive (just the function call is left). Summarizing, you can use printf wherever you want, even when no UART is available or accessible such as with the early bootloader. There is no need to link in a special runtime library for printf semihosting which is ideal for size restricted code, such as ROM code. You can safely add printfs without impacting system performance, making printf semihosting useful for debugging and optimizing software performance of OS schedulers.

The above example works out of the box with any Linux kernel. You can try it using Synopsys’ ARM® Cortex™-A15 MPCore demo in the cloud (see examples). I hope this is not too much code for a blog post during the vacation season☺.



Leave a Reply


(Note: This name will be displayed publicly)