In my previous blog, I demonstrated how to compile fio with the -fno-omit-frame-pointer flag in order to enable stack traces in perf record. The approach involved modifying the Makefile to add the compiler flag directly. While effective, this method is neither safe nor elegant: users must fully understand the complex Makefile to avoid introducing unintended side effects.

Furthermore, in practice, we often want to automate the build process within a script. Injecting flags programmatically using tools like sed can be brittle and error-prone—nobody wants to wrestle with obscure sed syntax or risk breaking a Makefile. So, is there a better way?

Yes, there is. In this post, I’ll walk you through a more robust and script-friendly method to inject the -fno-omit-frame-pointer flag when building fio, continuing from the example in my previous blog.

Method 1: Using Make Command-Line Variable Overrides

Our first attempt involves overriding variables via Make’s command-line interface. This method is documented in GNU Make's official manual. For our case, we can try appending the new compiler flag as follows:

make CFLAGS+='-fno-omit-frame-pointer'

To verify that the flag is correctly injected, we can perform a dry run and inspect Make’s variable database:

make CFLAGS+='-fno-omit-frame-pointer' -pn | grep CFLAGS

Relevant output:

-*-command-variables-*- := CFLAGS=-fno-omit-frame-pointer
CFLAGS := -DFIO_VERSION='"fio-3.40-34-g306d8"' -std=gnu99 -Wwrite-strings -Wall -Wdeclaration-after-statement -g -ffast-math  -D_GNU_SOURCE -include config-host.h  -Wimplicit-fallthrough -I. -I. -O3 -march=native -fno-omit-frame-pointer

Although we used +=, what actually happens here is a := override. This means our command-line value replaces the Makefile-defined value unless the latter was marked with override. As a result, flags defined in the Makefile might be inadvertently ignored, which can cause subtle bugs or missing optimizations.

Let’s compare it with the default behavior by running:

make clean
make distclean
./configure
make -pn | grep CFLAGS

Relevant output:

CFLAGS := -DFIO_VERSION='"fio-3.40-34-g306d8"' -std=gnu99 -Wwrite-strings -Wall -Wdeclaration-after-statement -g -ffast-math  -D_GNU_SOURCE -include config-host.h  -Wimplicit-fallthrough -I. -I. -O3 -march=native -D_GNU_SOURCE -include config-host.h  -Wimplicit-fallthrough

We can see that the command-line override caused some flags—such as -D_GNU_SOURCE, -include config-host.h, and -Wimplicit-fallthrough—to be omitted. Clearly, overriding CFLAGS via the Make command line is not safe in this case.

Method 2: Injecting Flags via configure Script

Fortunately, fio uses a configure script to generate build options. This gives us a safer and more robust way to inject custom flags.

First, let’s check what options the configure script provides:

./configure --help

Relevant output:

--extra-cflags=         Specify extra CFLAGS to pass to compiler

Perfect—we’re given a dedicated option for injecting custom CFLAGS. Let’s use it:

make clean
make distclean
./configure --extra-cflags='-fno-omit-frame-pointer'
make -pn | grep CFLAGS

Relevant output:

CFLAGS := -DFIO_VERSION='"fio-3.40-34-g306d8"' -std=gnu99 -Wwrite-strings -Wall -Wdeclaration-after-statement -g -ffast-math  -D_GNU_SOURCE -include config-host.h  -fno-omit-frame-pointer -Wimplicit-fallthrough -I. -I. -O3 -march=native -D_GNU_SOURCE -include config-host.h  -fno-omit-frame-pointer -Wimplicit-fallthrough

This time, the flag is correctly injected without removing or overriding existing ones. Keep in mind the rule: last flag wins. In this case, since -fno-omit-frame-pointer appears after -O3, it will successfully override the -fomit-frame-pointer implied by the optimization level.

How Does It Work?

Let’s take a look under the hood. The Makefile includes the following logic:

config-host.mak: configure
    @if [ ! -e "$@" ]; then                 \
      echo "Running configure ...";             \
      ./configure;                      \
    else                            \
      echo "$@ is out-of-date, running configure";      \
      sed -n "/.*Configured with/s/[^:]*: //p" "$@" | sh;   \
    fi

ifneq ($(MAKECMDGOALS),clean)
include config-host.mak
endif

As you can see, make includes config-host.mak, which is generated by the configure script. This file defines the build-time variables.

Let’s inspect it:

tail config-host.mak

Sample output:

CONFIG_HAVE_TIMERFD_CREATE=y
CONFIG_HAVE_NO_STRINGOP=y
CONFIG_HAVE_THP=y
LIBS+=-l:libtcmalloc_minimal.so.4 
GFIO_LIBS+=
CFLAGS+=-D_GNU_SOURCE -include config-host.h  -fno-omit-frame-pointer -Wimplicit-fallthrough
LDFLAGS+=
CC=gcc
BUILD_CFLAGS= -D_GNU_SOURCE -include config-host.h  -fno-omit-frame-pointer -Wimplicit-fallthrough
INSTALL_PREFIX=/usr/local

As expected, the -fno-omit-frame-pointer flag is appended to both CFLAGS and BUILD_CFLAGS. This explains how the configure script safely injects new flags into the build system.

Conclusion

Modifying the Makefile directly to inject compiler flags like -fno-omit-frame-pointer is risky and not ideal for automation. While using Make’s command-line variable overrides might seem cleaner, it can unintentionally override or exclude essential flags defined in the Makefile.

The safer and more maintainable approach—at least for fio—is to inject flags via the configure script using options like --extra-cflags. This method integrates well with the existing build system and ensures flags are applied without disrupting other settings.

That said, not all configure scripts are created equal. Although uncommon, a poorly implemented script might ignore or misplace injected flags. To be sure everything worked as expected, always verify the final compiler flags by running:

make -pn | grep CFLAGS

This quick check can save you from subtle bugs or performance issues down the line.