How to Troubleshoot Embedded Device Software Faster with a Static Call Flow Browser

By Hari Nagalla

Principal Software Engineer

Texas Instruments

November 16, 2021


How to Troubleshoot Embedded Device Software Faster with a Static Call Flow Browser

Internet of Things (IoT) devices have grown exponentially thanks to advances in low-cost, integrated system-on-chip devices with both radio-frequency connectivity and microcontroller cores.

Many of these devices are predominantly based on an Arm® Cortex®-M architecture. Along with hardware advances, embedded software plays an important role in keeping up with new connectivity protocols, protocol stacks, and frameworks.

The proliferation of connected devices creates a challenge for embedded software engineers, however, especially application and maintenance engineers who work on multiple devices and frameworks at the same time.

Learning how new device software, frameworks, and protocol stacks work can be very time consuming and limit an engineer’s ability to solve problems quickly. Design documents and in-line source code comments can help, but they may not be easily accessible and may not provide a full picture of how the code works.

In these instances, engineers rely on their ingenuity, resourcefulness, and source-code browsing using integrated development environments (IDEs). While this helps when trying to comprehend software code flows, it is a time-consuming and tedious process, and there is a better way.

In this article, I’ll present a novel approach using existing toolchain utilities to generate the software’s static function call hierarchies and understand software flows quicker and better.

Common types of function call tracing

You can use function call tracing to understand code flow or identify a bug. Comparing the program flow (through function call tracing) between successful and failed scenarios can help you quickly identify problematic areas of code for further inspection.

Function call tracing complements IDE-based source code browsing to better understand the entire software implementation, and can be divided into two common categories:

  • Runtime function call tracing. This is an invasive procedure that entails instrumenting the source code. Toolchains like GNU Compiler Collection provide instrumentation to place function calls, which requires rebuilding the code to regenerate a new binary but results in additional code size and longer execution times. For resource-constrained IoT devices that lack memory, runtime function call tracing may not be a viable option. Plus, you have no guarantee that the instrumented code will behave identically to the uninstrumented code.
  • Static function calls. For read-only memory (ROM)-based devices, instrumentation is not a viable option. Although you could simply browse the source code with IDEs such as Eclipse or Source Insight to understand the software implementation, that is a tedious process. Some IDEs (typically pricy commercial versions) can derive static function call diagrams. These static function call browsers are typically limited in scope and may not provide an accurate picture of the overall call flow if there are conditional compiles in the source code.

It is possible, however, to generate a static call flow browser from binary executable and linkable format (ELF) files, which reflects the actual binary code.

Fix software faster with a static call flow browser

Let’s use the device’s ELF binary image to generate function call reference details. As shown in Figure 1, the idea is to take the ELF binary and pass it through various code generation tools such as TI’s object file display (armofd) and disassembler (armdis) to generate a list of functions and a database of call references. Once the database is generated, display the call hierarchy and flow in a simple tree browser to view the function call references. These static call flow diagrams can also help with debugging by overlaying the runtime ROM code message logs on top of the static function tree – a combination that will provide insight into the runtime code flow and help you isolate problems.


Figure 1: ELF file format

Binary file (ELF) analysis

The ELF file contains a program header, section headers, and code and data sections. Toolchains provide various tools to inspect and display ELF binary file contents in a readable format. At TI, we use utility names such as armofd and armdis to obtain the function details and complete program coding in Arm disassembly.


Figure 2: The process of static function analysis

The parsing engine goes though the disassembly code and inspects for function calls through branch with link (BL) and branch with link and exchange (BLX) instructions, finds all of the call functions for each function, and populates the function database. The database itself is arranged as an Adelson-Velsky and Landis self-balancing search tree for quick search and browsing.

Compiler optimizations may skew some function calls by directly branching to the called function. These functions do not have any stack allocations, and so the parsing engine needs to be smart enough to detect these compiler optimizations.

Function browser

A simple graphical user interface (GUI) interface called Java frames (JFrames) selects the functions of interest for function call browsing. Selecting a function displays two frames, one for “callee/called functions” and another for “called from” functions. These frames display a hierarchical tree structure with further node expansions, as shown in Figures 3, 4, 5 and 6.

Browser GUI

The function list displays all of the available functions, enabling you to select functions of interest for browsing the references.

Figure 3: A function list display

It is possible to navigate further down the tree to see function call possibilities.

Figure 4: Called function references

Figure 5: Called from references

Figure 6: Function list GUI

Simplify software

By using this approach to derive static call flow charts from a binary image, you can now get a better view of the software functional flow and complement your source-code browsing to obtain a deeper understanding of software implementations. Best of all, this approach can accelerate the process and make troubleshooting software simpler.