Recommended compiler and linker flags for GCC

Did you know that when you compile your C or C++ programs, GCC will not enable all exceptions by default?  Do you know which build flags you need to specify in order to obtain the same level of security hardening that GNU/Linux distributions use (such as Red Hat Enterprise Linux and Fedora)? This article walks through a list of recommended build flags.

The GNU-based toolchain in Red Hat Enterprise Linux and Fedora (consisting of GCC programs such as gcc, g++, and Binutils programs such as as and ld)  are very close to upstream defaults in terms of build flags. For historical reasons, the GCC and Binutils upstream projects do not enable optimization or any security hardening by default. While some aspects of the default settings can be changed when building GCC and Binutils from source, the toolchain we supply in our RPM builds does not do this. We only align the architecture selection to the minimum architecture level required by the distribution.

Consequently, developers need to pay attention to build flags, and manage them according to the needs of their project for optimization, level of warning and error detection, and security hardening.

During the build process to create distributions such as Fedora and Red Hat Enterprise Linux, compiler and linker flags have to be injected, as discussed below. When you are using one of these distributions with the included compiler, this environment is recreated, requiring an extensive list of flags to be specified. Recommended flags vary between distribution versions because of toolchain and kernel limitations. The following table lists recommended build flags (as seen by the gcc and g++ compiler drivers), along with a brief description of which version of Red Hat Enterprise Linux and Fedora are applicable:

Flag Purpose Applicable Red Hat Enterprise Linux versions Applicable Fedora versions
-D_FORTIFY_SOURCE=2 Run-time buffer overflow detection All All
-D_GLIBCXX_ASSERTIONS Run-time bounds checking for C++ strings and containers All (but ineffective without DTS 6 or later) All
-fasynchronous-unwind-tables Increased reliability of backtraces All (for aarch64, i386, s390, s390x, x86_64) All (for aarch64, i386, s390x, x86_64)
-fexceptions Enable table-based thread cancellation All All
-fpie -Wl,-pie Full ASLR for executables 7 and later (for executables) All (for executables)
-fpic -shared No text relocations for shared libraries All (for shared libraries) All (for shared libraries)
-fplugin=annobin Generate data for hardening quality control Future Fedora 28 and later
-fstack-clash-protection Increased reliability of stack overflow detection Future (after 7.5) 27 and later (except armhfp)
-fstack-protector or -fstack-protector-all Stack smashing protector 6 only n/a
-fstack-protector-strong Likewise 7 and later All
-g Generate debugging information All All
-grecord-gcc-switches Store compiler flags in debugging information All All
-mcet -fcf-protection Control flow integrity protection Future 28 and later (x86 only)
-O2 Recommended optimizations All All
-pipe Avoid temporary files, speeding up builds All All
-Wall Recommended compiler warnings All All
-Werror=format-security Reject potentially unsafe format string arguents All All
-Werror=implicit-function-declaration Reject missing function prototypes All (C only) All (C only)
-Wl,-z,defs Detect and reject underlinking All All
-Wl,-z,now Disable lazy binding 7 and later All
-Wl,-z,relro Read-only segments after relocation 6 and later All

This table does not list flags for managing an executable stack or the .bss section, under the assumption that these historic features have been phased out by now.

Documentation for compiler flags is available in the GCC manual. Those flags (which start with -Wl) are passed to the linker and are described in the documentation for ld.

For some flags, additional explanations are in order:

  • -D_GLIBCXX_ASSERTIONS enables additional C++ standard library hardening. It is implemented in libstdc++ and described in the libstdc++ documentation. Unlike the C++ containers with full debugging support, its use does not result in ABI changes.
  • -fasynchronous-unwind-tables is required for many debugging and performance tools to work on most architectures (armhfp, ppc, ppc64, ppc64le do not need these tables due to architectural differences in stack management). Even though it is necessary on aarch64, upstream GCC does not enable it by default. The compilers for Red Hat Enterprise Linux and Fedora carry a patch to enable it by default.
  • -fexceptions is recommended for hardening of multi-threaded C and C++ code. Without it, the implementation of thread cancellation handlers (introduced by pthread_cleanup_push) uses a completely unprotected function pointer on the stack. This function pointer can simplify the exploitation of stack-based buffer overflows even if the thread in question is never canceled.
  • -fstack-clash-protection prevents attacks based on an overlapping heap and stack. This is a new compiler flag in GCC 8, which has been backported to the system compiler in Red Hat Enterprise Linux 7.5 and Fedora 26 (and later versions of both). We expect this compiler feature to reach maturity in Red Hat Enterprise Linux 7.6. The GCC implementation of this flag comes in two flavors: generic and architecture-specific. The generic version shares many of its problems with the older -fstack-check flag (which is not recommended for use). For the architectures supported by Red Hat Enterprise Linux, improved architecture-specific versions are available. This includes aarch64, for which only problematic generic support is available in upstream GCC (as of mid-February 2018). The Fedora armhfp architecture also lacks upstream and downstream support, so the flag cannot be used there.
  • -fstack-protector-strong completely supersedes the earlier stack protector options. It only instruments functions that have addressable local variables or use alloca. Other functions cannot be subject to direct stack buffer overflows and are not instrumented. This greatly reduces the performance and code size impact of the stack protector.
  • To enable address space layout randomization (ASLR) for the main program (executable), -fpie -Wl,-pie has to be used. However, while the code produced this way is position-independent, it uses some relocations which cannot be used in shared libraries (dynamic shared objects). For those, use -fpic, and link with -shared (to avoid text relocations on architectures which support position-dependent shared libraries). Dynamic shared objects are always position-independent and therefore support ASLR. Furthermore, the kernel in Red Hat Enterprise Linux 6 uses an unfortunate address space layout for PIE binaries under certain circumstances (bug 1410097) which can severely interfere with debugging (among other things). This is why it is not recommended to build PIE binaries on Red Hat Enterprise Linux 6.
  • -fplugin=annobin enables the annobin compiler plugin, which captures additional metadata to allow a determination of which compiler flags were used during the build. Annobin is currently available only on Fedora, and it is automatically enabled as part of the Fedora 28 build flags , where it shows up as -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1.
  • To generate debugging information we recommend (using -g), even for optimized production builds. Having only partly usable debugging information (due to optimization) certainly beats having none at all. With GCC, generating debugging information does not alter code generation. It is possible to use tools such as eu-strip to separate debugging information before distributing binaries (which automatically happens during RPM builds).
  • -grecord-gcc-switches captures compiler flags, which can be useful to determine whether the intended compiler flags are used throughout the build.
  • -mcet -fcf-protection enables support for the Control-Flow Enforcement Technology (CET) feature in future Intel CPUs. This involves the generation of additional NOPs, which are ignored by the current CPUs. It is recommended that you enable this flag now, to detect any issues caused by them (e.g., interactions with dynamic instrumentation frameworks, or performance issues).
  • For many applications, -O2 is a good choice because the additional inlining and loop unrolling introduced by -O3 increases the instruction cache footprint, which ends up reducing performance. -O2 or higher is also required by -D_FORTIFY_SOURCE=2.
  • By default, GCC allows code to call undeclared functions, treating them as returning int. -Werror=implicit-function-declaration turns such calls into errors. This avoids difficult-to-track-down run-time errors because the default int return type is not compatible with bool or pointers on many platforns. For C++, this option is not needed because the C++ compiler rejects calls to undeclared functions.
  • -Wl,-z,defs is required to detect underlinking, which is a phenomenon caused by missing shared library arguments when invoking the linked editor to produce another shared library. This produces a shared library with incomplete ELF dependency information (in the form of missing DT_NEEDED tags), and the resulting shared object may not be forward compatible with future versions of libraries which use symbol versioning (such as glibc), because symbol versioning information is missing from it.
  • -Wl,-z,now (also referred to as BIND_NOW) is not recommended for use on Red Hat Enterprise Linux 6 because the dynamic linker processes non-lazy relocations in the wrong order (bug 1398716), causing IFUNC resolvers to fail. IFUNC resolver interactions remain an open issue even for later versions, but -Wl,-z,defs will catch the problematic cases involving underlinking.

In RPM builds, some of these flags are injected using -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 and -specs=/usr/lib/rpm/redhat/redhat-hardened-ld because the option selection mechanism in GCC specs allows one to automatically drop the PIE-related flags (for static linking) for PIC builds (for dynamic linking). For historic reasons, -Wl,-z,now is included in -specs=/usr/lib/rpm/redhat/redhat-hardened-ld, and not on the command line, so it will not show up directly in build logs.

Injecting flags during RPM builds

RPM spec files need to inject build flags in the %build section, as part of the invocation of the build tools.

The most recent versions of the redhat-rpm-config package documents how to obtain the distribution compiler and linker flags. Note that the link goes to the most recent version of the Fedora package. For older distributions, only the following methods for obtaining flags are supported:

  • The %{configure} RPM macro, which runs ./configure, but also sets the CFLAGS and LDFLAGS macros.
  • The %{optflags} RPM macro and the $RPM_OPT_FLAGS environment variable, which provide compiler flags for C and C++ compilers.
  • The $RPM_LD_FLAGS environment variable, which provides linker flags.

Note that Red Hat Enterprise Linux 7 and earlier do not enable fully hardened builds for all packages, and it is necessary to specify:

%global _hardened_build 1
in the RPM spec file to enable the full set of hardening flags. The optional hardening comprises ASLR for executables (PIE) and non-lazy binding/BIND_NOW. For technical reasons, the recommended linker flag -Wl,-z,defs is not used either.

Other flags to consider

  • -fwrapv tells the compiler that the application assumes that signed integer overflow has the usual modulo behavior (like it has in Java, for example). By default, integer overflow is treated as undefined, which helps with certain loop optimizations. This can cause problems with legacy code which assume the Java behavior even for C/C++.
  • -fno-strict-aliasing instructs the compiler to make fewer assumptions about how pointers are used and which pointers can point to the same data (aliasing). This can be required to compile legacy code.
  • -flto and various other flags can be used to switch on link-time optimization (LTO). This can result in improved performance and smaller code, but may interfere with debugging. It may also reveal conformance issues in the source code that were previously hidden by separate compilations.
  • In some cases, -Os (optimize for small code) may result in faster code than -O2 due to reduced instruction cache pressure.
  • For some applications -O3 or -O2 -free-loop-vectorize will provide a significant speed boost. By default, GCC does not perform loop vectorization. Be aware that -O3 will change the way code reacts to ELF symbol interposition, so this option is not entirely ABI-compatible. Overall, we still consider -O2 the choice for the default.
  • -mstackrealign may be needed for compatibility with legacy applications (particularly on i686) which does not preserve stack alignment before calling library functions compiled with recent GCC versions.

Problematic flags

Some flags are used fairly often, but cause problems. Here is a list of a few of those:

  • -ffast-math can have very surprising consequences because many identities which usually hold for floating-point arithmetic no longer apply. The effect can extend to code not compiled with that option.
  • -mpreferred-stack-boundary and -mincoming-stack-boundary alter ABI and can break interoperability with other code and future library upgrades.
  • -O0 may improve the debugging experience while it disables all optimization, it also eliminates any hardening which depends on optimizations (such as source fortifictation/-D_FORTIFY_SOURCE=2).
  • Likewise, the sanitizer options (-fsanitize=address and so on) can be great debugging tools, but they can have unforeseen consequences when used in production builds for long-term use across multiple operating system versions. For example, the Address Sanitizer interceptors disable ABI compatibility with future library versions.

Flags for Red Hat Developer Toolset (DTS)

The -fstack-protector-strong flag is available in DTS 2.0 and later. DTS 6 and later versions support -D_GLIBCXX_ASSERTIONS. DTS 7.1 will support the -fstack-clash-protection flag. The other version specific limitations are due to system components which are not enhanced by DTS (such as glibc or the kernel), so these restrictions apply to DTS builds as well.

Language standard versions

The flags discussed so far mostly affect code generation and debugging information. An important matter specific to the C and C++ languages in particular is the selection of:

  • The system compilers in Red Hat Enterprise Linux 7 and earlier defaults to C90 for C and C++98 for C++, with many GNU extensions, some of which made it into later standards versions.
  • The Red Hat Enterprise Linux 7 system compiler is based on GCC 4.8 and supports the -std=gnu11 option for C and -std=gnu++11 option for C++. However, both C11 and C++11 support are experimental.
  • Developer Toolset (DTS) provides extensive support for newer versions of the standards, ensuring compatibility with the system libstdc++ library using a hybrid linkage model.
  • The Fedora 27 system compiler defaults to C11 and C++14 (which will change again in future Fedora versions).

In general, it is recommended to use the most recent standard version support by the toolchain, which is C99 (-std=gnu99) and C++98 (enabled by default default) for the Red Hat Enterprise Linux system compilers. For the Developer Toolset, the more recent defaults should be used. Some changes in the standards do not have perfect backwards compatibility.  As a result, a porting effort may be required to use the settings for the newer standards.

Note that even the most recent version of the GNU toolchain does not support some optional C features (such as C11 threads or Annex K and its _s functions), and C++ support is continuously evolving, especially for recent or upcoming versions of the C++ standard.

Join the Red Hat Developer Program (it’s free) and get access to related cheat sheets, books, and product downloads.