Developing in C for the ATmega328P: Using the GNU Linker to Enhance printf()

6 minute read

Where I describe how to use the GNU Linker wrapper capability and variable arguments to enhance printf() for better debugging.

Introduction

This recent post fascinated me. It describes a feature of the GNU linker called wrapping which allows you to replace an existing symbol (command) with a new version. The example describes adding a timestamp to printf(). While, for me, this might be a solution looking for a problem, I really liked the idea. And at the very least wanted to add the concept to AVR_C.

Overview

In order to execute this approach, you need to understand two concepts, GNU Linker wrapping and C variable arguments. The former is an technique which (from the manual):

Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to __wrap_symbol. Any undefined reference to __real_symbol will be resolved to symbol.

The translation is that you may replace any symbol, which is the linker’s name for a function or command, to be replaced by one you specifically wrote. In doing so, you will need to ensure the existing symbol is accounted for as well.

In addition, in re-writing printf(), you need to recognize that this command takes a variable argument list. This is indicated in the ‘’ or ellipse in the documentation:

int printf ( const char * __fmt, )

This requires reading a bit more on using variable arguments, and perhaps here and here. There is also a longer discussion below.

A variable argument list is a concept which enables defining a function which can take an undefined number of arguments. In this case, undefined is in the context of the definition of the function, not undefined in the calling of the function. The function, printf() is a great example, you are able to call it, each time, with different number of arguments.

The C compiler uses a set of macros starting with va_ and are located in the stdarg.h header file.

va_list type # declare the variable-argument list pointer
va_start # initialize the va_list variable
va_arg # Retrieve the present argument from the va_list type variable and increment it
va_end # Invalidate the va_list type variable by assigning the NULL to it

Wrapping printf()

I originally simplified the example to this one to test it.:

// uses CR as end of line, might need to be changed to LF
#define CR 13

int __real_printf(const char *fmt, ...);

void __wrap_printf(const char *fmt, ...)
{
    va_list args;
    __real_printf("%lums ", millis());
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}

int main(void) {    

    init_serial();
    init_sysclock_2 ();

    char input;

    __real_printf("printf wrapper  Test\n");
    while((input = getchar())!= CR) {
        printf("Testing %c\n", input);
    }
    __real_printf("Program Exit\n");
}

I then reviewed printf() and vprintf() and realized that both “return the number of characters written to stream”, which is why count0 and count1 are used/returned. This keeps the wrapped function functionality to be the same as the real function. Which means I use the original version, with slight modifications. The above main() test program stays the same.:

uint16_t __real_printf(const char *fmt, ...);

uint16_t __wrap_printf(const char *fmt, ...) 
{
  va_list args;
  uint16_t count0, count1;
  count0 = __real_printf("%d: ", millis());
  va_start(args, fmt);
  count1 = vprintf(fmt, args);
  va_end(args);
  return count0+count1;
}

There is one more thing which needs to be performed to make this work. I need to advise the linker that I am wrapping printf(). I do that by this command on line 55 in the main Makefile:

LDFLAGS += -Wl,--wrap=printf

The output of this program, will show the elapsed milliseconds since I reset the processor. The value is a 32-bit unsigned variable, so it can count up to ~ 49 days. :)

printf wrapper  Test
1063ms Testing a
3466ms Testing k
8289ms Testing k
147416ms Testing l

Variable Arguments

I spent a little bit of time attempting to understand the concept of ellipses in an argument, variadic functions and variable arguments. All three concepts are generally, the same. They refer to a aspect of the C Language which allows for a function to have a variable number of arguments. The function printf() is typically provided as a great example.

This tutorial provides the best explanation. Key to the explanation is this “The argument list containing the ellipsis must have at least another argument (other than the ellipsis) in the list. Let us call it as “Mandatory Argument”….This Mandatory Argument must be placed just before the ellipsis and this Mandatory Argument must reflect the number of variable arguments currently being passed explicitly or implicitly.

I placed the emphasis on the phrase “explicitly or implicitly” at the end. Many explanations simply claim the last argument (or Mandatory Argument) must be the number of arguments to be expected. However, clearly fmt in printf(const char *fmt, ...) doesn’t do this. It is because printf() implicitly provides the number of arguments by the formatting characters following the “%” sign. This not only provides the number of arguments, it also provides the requisite type of argument as well.

Indepth Example of Variable Arguments

I used an example from the K & R book Second Edition to further explore variable arguments. The example creates a simple minprintf() function then demonstrates how the function uses an implicit definition of the number of variables to successfully execute. I documented the code, so I won’t review it here. It is shown below:

#include <stdio.h>  // required for printing
#include "uart.h"   // required for printing
#include <stdarg.h> // required for variable arguments va_

// minprintf - page 156 K&R Second Edition
// minprintf: minimal printf with variable argument list
// with additional comments for clarification

// Many explanations of variable arguments state the last named argument
// must be the count of arguments. This is incorrect, the count may be
// explicit or implicit. This is a implicit example, where the function
// doesn't depend on a count, it exits when it hits the end of a string

void minprintf(char *fmt, ...)
{
    va_list ap;     // arg pointer, will point to each unnamed arg in turn
    char *p;        // will point to each char in fmt
    char *sval;     // will contain the string value if passed
    int ival;       // will contain the integer value if passed
    double dval;    // will contain the doublt value if passed

    // use the final named argument to get va_ initialized
    // for other functions, this argument will be the count of arguments
    va_start(ap, fmt);

    // note this loop depends on the last char being a 0, due to the *p
    // as the conditional, loop exits when conditional is false (0)
    for (p = fmt; *p; p++)
    {
        // examine each char in fmt, print if not a %
        if (*p != '%')
        {
            putchar(*p);
            continue;
        }
        // % found, now determine what type of value is arg
        switch(*++p)
        {
        // d - defines the arg as an integer
        case 'd':
             ival = va_arg(ap, int);
             printf("%d", ival);
             break;
        // f - defines the arg as an double
        case 'f':
             dval = va_arg(ap, double);
             printf("%f", dval);
             break;
        // s - defines the arg as a string
        // similar to the fmt loop, the conditional depends on last char as 0
        case 's':
             for (sval = va_arg(ap, char *); *sval; sval++)
             {
                putchar(*sval);
             }
             break;
        // if the desired format is undefined, print format letter and a ?
        default:
            putchar(*p);
            putchar('?');
            break;
        }
    }
    putchar('\n'); // add a new line to make it easy to read
    va_end(ap);     // clean up when done then exit
}

int main(void)
{
    init_serial();      // initialze the serial port

    char string_value[] = {"A new string"};
    char missing_format[] = {"missing format value"};

    minprintf("This is just a string, no formats");
    minprintf("This is a string with an integer format %d", 123);
    minprintf("This is a string with an double format %f", 123.456);
    minprintf("This is a string with another string format %s", string_value);
    minprintf("This is a string with a missing format %g", missing_format);
}

Comments powered by Talkyard.