Debugging software isn’t always clean, but there are some proven methods that get the job done.
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:
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