How to Use GNU Coverage (gcov) in a Freestanding Environment for Embedded Systems

The GNU Coverage (gcov) is a source code analysis tool, and is a standard utility in the GNU gcc suite. It works great in a hosted environment (e.g. Linux or Windows), where you have plenty of resources and a file system. But the gcov tools is relevant and usable for restricted embedded systems too. I have used it for years with the help of debug probes and file I/O semihosting. But semihosting does not come for free, depends on a library with support for constructors and destructors, plus relies on file I/O.

Fortunately, there is a way to use gcov without debugger, semihosting, file I/O and special system initialization: using a freestanding environment:

gcov in freestanding environment with embedded target

This article explains how to collect coverage information using a data stream for example over UART or USB-CDC. Key benefits are less code side, no need for a debugger or on-target file system, improved performance, better automation and flexible data collection.

Outline

This article describes how to build and use a freestanding environment to collect gcov (GNU Coverage) data. It explains the concept and the needed tools and settings. The basics are explained in https://gcc.gnu.org/onlinedocs/gcc/Freestanding-Environments.html, but not with an embedded target.

With the presented approach, one does not need a debug probe or semihosting to get the coverage data from the embedded target. Instead, a serial link, UART or USB CDC connection to the host is enough, for example with the USB connection on a NXP FRDM-MCXA153:

FRDM-MCXA153 board with USB-CDC (VCOM) connection to the host.

If you are new to GNU coverage, gcov or semihosting: please check the links to previous articles and extended information at the end of this article.

The key steps are:

  1. Configuring the application to use the freestanding environment
  2. Instruct the compiler to instrument the code
  3. Include gcov information section with the linker file
  4. Run the application and stream the coverage data over a serial connection to the host
  5. Encode the data and decode it
  6. Use gcov-tool to merge the data on the host
  7. Use gcov to generate the report

GNU Coverage with Debugger and Semihosting

In a ‘traditional’ embedded environment, gcov is used with a debugger and semihosting:

gcov with debugger and semihosting

The gcc compiler compiles the sources files with semihosting functionality (McuSemihost) and instruments the binary, plus producing the notes (.gcno) files. The resulting binary (ELF/Dwarf) including the GNU libgcov library then gets loaded onto the embedded target and executed. The collected data then gets sent via debug probe and semihosting to the host, producing coverage data (.gdca) files. The GNU gcov utility then produces reports based on the notes and coverage data files.

In a freestanding environment, no file system support is needed, so less overhead for the embedded application. Additionally the freestanding environment with gcov does not need to call the application constructors and destructors: therefore it works nicely with a C++ environment as the OOP constructors do not interfere with the gcov constructors and can be used as ‘as is’, plus allows collecting coverage information from global object constructors and destructors.

GNU Coverage with Freestanding Environment

In a freestanding environment, no debugger or target related file system (semihosting) is needed. Instead, a freestanding environment with McuCoverage is used which produces an encoded data stream to the host. The data stream can use a UART, USB, SPI, I2C or for example a network socket. The data stream then is merged with the gcov-tool (part of the GNU gcov suite) which generates and updates the data (.gcda) files. Depending on the communication channel, the date gets encoded on the target and decoded on the host. From there, the normal gcov or other tools like gcovr can be used to produce reports.

freestanding environment with gcov

In the next sections, I’ll use above picture to guide you through the process.

McuCoverage Configuration

With a freestanding approach, no target file system is used. It does not use the expensive semihosting operations. And no debug probe or debugger is needed. Instead, a fast and inexpensive serial data connection to the host is used, for example UART.

To make using gcov for embedded simpler and easier, I have created the McuCoverage module in the McuLib.

The module uses a configuration file McuCoverageconfig.h, which has a setting to turn on the freestanding environment:

#ifndef McuCoverage_CONFIG_USE_FREESTANDING
  #define McuCoverage_CONFIG_USE_FREESTANDING         (1 && McuCoverage_CONFIG_IS_ENABLED)
    /*!< 1: Implementation using a freestanding environment, add -fprofile-info-section compiler option; 0: No freestanding environment */
#endif

Instrumenting code for Coverage

To instrument source files, add the --coverage option to the gcc compiler settings. One way to do this with CMake:

set_source_files_properties(
  main.c
  leds.c
  platform.c
  PROPERTIES COMPILE_FLAGS --coverage
)

Linking with gcov Library

The application needs to link with the GNU gcov library, for example in a CMake CMakeLists.txt:

target_link_libraries(
  ${THIS_LIBRARY_NAME}
  PRIVATE McuLib   # includes McuCoverage module
  PUBLIC gcov      # GNU gcov library
...
)

💡 Be aware that recent ARM GNU tool chain and library distributions might not include a full gcov library. See for example this article: Tutorial: GNU gcov Coverage with the NXP i.MX RT1064.

In a freestanding environment, the gcov counter initialization is *not* handled with module constructors. Instead, the coverage information is collected inside the gcov_info linker section. For this, the following needs to be added to the GNU linker file:

    /* needed for freestanding coverage  */
    .gcov_info : {
        PROVIDE (__gcov_info_start = .);
        KEEP (*(.gcov_info))
        PROVIDE (__gcov_info_end = .);
    } > FLASH

The collection of coverage information for every instrumented module should then be visible in the linker map file, for example:

.gcov_info      0x100151a8       0x10
                0x100151a8                        PROVIDE (__gcov_info_start = .)
 *(.gcov_info)
 .gcov_info     0x100151a8        0x4 srcLib/libsrcLib.a(main.c.obj)
 .gcov_info     0x100151ac        0x4 srcLib/libsrcLib.a(platform.c.obj)
 .gcov_info     0x100151b0        0x4 srcLib/libsrcLib.a(gcov_test.c.obj)
 .gcov_info     0x100151b4        0x4 srcLib/libsrcLib.a(leds.c.obj)
                0x100151b8                        PROVIDE (__gcov_info_end = .)

It defines the two linker symbols __gcov_info_start and __gcov_info_end which are used inside the McuCoverage module, as we will see later.

Application

The application has to setup the McuCoverage module with the callback, then can write the data stream. Below is how this looks like:

int main(void) {
  /* initialize hardware and drivers .... */

  /* run application and tests ... */

  /* write coverage information */
#if McuCoverage_CONFIG_USE_FREESTANDING
  McuCoverage_SetOuputCharCallback(Cdc_SendChar); /* register a callback to use USB CDC to write the data */
  McuCoverage_OutputString("\n"); /* visual indicator to separate data streams */
  McuCoverage_WriteFiles(); /* write coverage data stream */
#endif
  for(;;) {
    /* do not return from main() */
  }
  return 0;
}

Data Stream

We can use any kind of way to send the data stream to the host. For this the application has to register a callback, for example:

McuCoverage_SetOuputCharCallback(Cdc_SendChar); /* register a callback to use USB CDC to write the data */

The callback is of the following type:

/*!
  * \brief Type of callback used for output data in freestanding mode.
  */
typedef void (*McuCoverage_OutputCharFct_t)(unsigned char);

With this, you can stream the data over any kind of channel (UART, I2C, SPI, …) to the host.

To write the coverage information, call the following function in the application code:

McuCoverage_WriteFiles();

which internally calls dump_gcov_info():

extern const struct gcov_info *const __gcov_info_start[];
extern const struct gcov_info *const __gcov_info_end[];

/* Dump the gcov information of all translation units. */
static void dump_gcov_info (void) {
  const struct gcov_info *const *info = __gcov_info_start;
  const struct gcov_info *const *end = __gcov_info_end;

  __asm__ ("" : "+r" (info)); /* Obfuscate variable to prevent compiler optimizations.  */
  while (info != end) { /* iterate through all infos (or modules instrumented) */
    void *arg = NULL;
    __gcov_info_to_gcda(*info, filename, dump, allocate, arg); /* write record*/
    if (OutputCharFct!=NULL) { /* each record is terminated with a newline character */
      OutputCharFct('\n');
    }
    info++;
  }
}

It iterates through the data we have reserved above in the linker file. For each info block, we call:

__gcov_info_to_gcda(*info, filename, dump, allocate, arg);

That function __gcov_info_to_gcda() takes three callbacks:

  1. filename: For the gcov-tool merging part, we need to stream the file name. This callback
  2. dump:Function to write the data.
  3. allocate: Function to allocate dynamic memory.

Below the implementation of the above callbacks in McuCoverage.c:

First the function which writes the full file name:

/* The filename is serialized to a gcfn data stream by the
   __gcov_filename_to_gcfn() function.  The gcfn data is used by the
   "merge-stream" subcommand of the "gcov-tool" to figure out the filename
   associated with the gcov information. */
static void filename(const char *f, void *arg) {
  __gcov_filename_to_gcfn(f, dump, arg);
}

In case the libgcov needs dynamic memory, a memory allocation function is provided:

/* The __gcov_info_to_gcda() function may have to allocate memory under
   certain conditions.  Simply try it out if it is needed for your application
   or not.  */
static void *allocate (unsigned length, void *arg) {
  (void)arg;
  void *p;
  #if McuLib_CONFIG_SDK_USE_FREERTOS
    p = pvPortMalloc(length);
  #else
    p = malloc (length);
  #endif
  if (p==NULL) {
    McuLog_fatal("malloc failed");
    for(;;) {} /* error */
  }
}

A ‘dump’ function is used to send the data e.g. over the UART:

/* This function shall produce a reliable in order byte stream to transfer the
   gcov information from the target to the host system.  */
static void dump(const void *d, unsigned n, void *arg) {
  (void)arg;
  const unsigned char *c = d;
  unsigned char buf[2];

  for(unsigned int i = 0; i<n; i++) {
    if (OutputCharFct!=NULL) {
      encode(c[i], buf);
      OutputCharFct(buf[0]);
      OutputCharFct(buf[1]);
    }
  }
}

Stream Encoding

You might have noticed the encode() function in above dump() function. The purpose of this encode() is to write the binary data as ASCII stream:

static const unsigned char a = 'a';

/* each 8bit binary data value c gets encoded into two characters, each in rang 'a'-'p' (p is 'a'+16):
  buf[0]: 'a' + LowNibble(c)
  buf[1]: 'a' + HighNibble(c)
*/
static inline unsigned char *encode(unsigned char c, unsigned char buf[2]) {
  buf[0] = a + c % 16;
  buf[1] = a + (c / 16) % 16;
  return buf;
}

The encoding depends on the stream type used: here I want to send printable characters over the UART.

Sending Data Stream

The encoded data is sent to the host. In my case here I’m using the USB CDC interface of the board to the host. The function below is what I have registered as callback for the libgcov implementation mentioned earlier.

void Cdc_SendChar(unsigned char ch) {
  McuShell_SendCh(ch, cdc_stdio.stdOut);
}

When I want to send the coverage information from the application to the host, I add a new-line as a visual separator and then dump the coverage information to the host:

McuCoverage_OutputString("\n"); /* visual indicator to separate data streams */
McuCoverage_WriteFiles(); /* write coverage data stream */

Data Stream File

Below is how the encoded data stream looks like in a terminal program:

Later I need to decode the data: the easiest way is to store all the data in a file:

Save the data for example in a file named ‘stream.txt’

Decode

To decode the data, I wrote a simple C program (decode) on the host. You can find the sources on GitHub. Use ‘make’ to build it:

With the -f option I specify the input file and with -o the output file:

decode -f stream.txt -o stream.gcfn

gcov-tool

The gcov-tool is part of the GNU coverage tools and is used to merge and combine data files, including merging stream data files (aka gcfn files).

With the below command I merge the stream data information. It takes the stream.gcfn file as input:

arm-none-eabi-gcov-tool merge-stream < stream.gcfn

Because the data stream data has the full path to the source files used, it will automatically create and update the .gcda files! Below the location of the files as shown in VS Code:

VS Code with .gcda files

Reports

The final part is common to freestanding and non-freestanding gcov: use your favorite IDE and tools to display the data, for example with gcovr:

gcovr report

Or for example in Eclipse:

Eclipse (MCUXpresso IDE)

Congratulations, you are now using a gcov freestanding environment!

Summary

The GNU Coverage (gcov) is a great framework to collect coverage information. By default it depends on a file system, and for embedded targets the semihosting technology is a solution. But semihosting is complex and depends on a debug probe.

Another solution is to use gcov in a freestanding environment, using a stream of data which then is processed by the gcov-tool to produce and merge gcov data files. That way no file system, debugger or semihosting is required.

It took me a while to get it setup and working. But is it worth the efforts? So here is the application code and data size with freestanding mode:

text       data     bss      dec    hex filename
338112 0 155932 494044 789dc TSM_App.elf

The same application in non-freestanding (semihosting) mode:

text	   data	    bss	    dec	    hex	filename
364800 0 155964 520764 7f23c TSM_App.elf

I think 26 kByte less code size is a good reason to use the freestanding mode. Plus I don’t need a debug probe to collect the data, which makes things easier for a test farm too.

Happy covering 🙂

Links

2 thoughts on “How to Use GNU Coverage (gcov) in a Freestanding Environment for Embedded Systems

  1. Hi,

    i am going crazybwith 2 smoothieboard v1 they bothbdisconnect frim 5 laptops w10 and Ubuntu communicates with putty but open pnp cannot open IO ports could u pls help me a bit?

    regards

    Like

Leave a reply to david Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.