GNU C library

Most of us appreciate when our compiler lets us know we made a mistake. Finding coding errors early lets us correct them before they embarrass us in a code review or, worse, turn into bugs that impact our customers. Besides the compulsory errors, many projects enable additional diagnostics by using the -Wall and -Wextra command-line options. For this reason, some projects even turn them into errors via -Werror as their first line of defense. But not every instance of a warning necessarily means the code is buggy. Conversely, the absence of warnings for a piece of code is no guarantee that there are no bugs lurking in it.

In this article, I would like to shed more light on trade-offs involved in the GCC implementation choices. Besides illuminating underlying issues for GCC contributors interested in implementing new warnings or improving existing ones, I hope it will help calibrate expectations for GCC users about what kinds of problems can be expected to be detected and with what efficacy. Having a better understanding of the challenges should also reduce the frustration the limitations of the available solutions can sometimes cause. (See part 2 to learn more about middle-end warnings.)

The article isn't specific to any GCC version, but some command-line options it refers to are more recent than others. Most are in GCC 4 that ships with Red Hat Enterprise Linux (RHEL), but some are as recent as GCC 7. The output of the compiler shown in the examples may vary between GCC versions. See How to install GCC 8 on RHEL if you'd like to use the latest GCC.

We rarely notice when a warning is not issued even though it should be. When we do, it's usually when we are looking for a reason why a bug wasn't discovered earlier in the development cycle. Only a small subset of missing warnings are reported to GCC, and those that are tend to be treated with lower priority. To an extent this is unavoidable because other bugs are often more important (think about the compiler emitting incorrect code for valid programs, for instance). Despite that, reports of missing warnings are useful not just because they let compiler writers know the compiler isn't doing what it advertises to do. Often they can also help identify missed optimization opportunities that might otherwise go unnoticed.

In contrast, warnings that complain about code that is not buggy stand out. In projects that enable -Werror, they are impossible to miss. We call them "false positives," or "false alarms," or "spurious," or sometimes, perhaps a little pejoratively, "bogus." But not all such warnings do fit the notion of a false positive as it's defined in literature. Formally, a false positive is a report of a violation of a rule that isn't supported by the rule's definition. Simply put, it's a bug in the warning.

True positives versus bugs

As an example of the distinction, take the -Wchar-subscripts option, whose documented purpose is to

Warn if an array subscript has type char. This is a common cause of error, as programmers often forget that this type is signed on some machines.

Therefore, issuing the warning for an array subscript of any other type, including signed char, would be a false positive. However, even though no instance of -Wchar-subscripts when char is unsigned is indicative of a bug, none is a false positive because the warning is meant to trigger regardless of char's signedness. Whether that is a good design is a separate issue.

If, on the other hand, the -Wchar-subscripts documentation instead read:

Warn if an array subscript with type char has a negative value.

then only instances pointing to negative subscripts would be true positives, and all others of any type, including and especially char, would be false positives. But even with this refined formulation, not every instance of this hypothetical warning would be a bug either because negative subscripts from the end of an array may be valid. That this definition would make -Wchar-subscripts superfluous is interesting, because -Warray-bounds detects exactly the kinds of bugs this warning tries to prevent, with a rate of false positives approaching zero.

This might seem like a subtle distinction, but an important one because it determines where in GCC a warning has to be implemented to achieve satisfactory rates of true and false positives and false negatives, and how challenging that might be.

Front-end warnings

Most GCC diagnostics, errors, and warnings alike are implemented in language front ends. For our purposes, a GCC front end translates programs from source code into a language-independent form called GENERIC that the subsequent phases of the compiler work with. A front end handles parsing and semantic analysis and ends just before the GENERIC form of the program is converted into a form suitable for optimization (GIMPLE). During the translation to GENERIC, a front end checks code against the lexical, type-safety, and other static constraints of the language and issues errors where required by the language specification. This process is also an opportunity to check for constructs that are strictly valid but that may be unsafe and issue warnings.

Preprocessor warnings

A small number of GCC warnings are implemented in the preprocessor. This is necessary because detecting problems after code has been preprocessed would no longer be possible. Even though the GCC preprocessor is a standalone program, thanks to its close integration with the front ends, it makes sense to consider it a part of a front end for simplicity's sake. A few interesting examples of preprocessor warnings are -Wtrigraphs that detects uses of trigraphs, -Wundef to identify uses of undefined macros in #if directives, and -Wunused-macros to help find macros that are not used.

Lexical warnings

A broad class of warnings implemented in GCC front ends are those that are based on lexical rules. These can be thought of as sophisticated regular expressions. In static analysis terminology, these rules are sometimes referred to as pattern rules. An example of such a warning is -Wstrict-prototypes: it points out C functions that are declared without a prototype (that is, with an empty set of parentheses). This is valid (though deprecated in the C standard), but because it lets the function be called with any number of arguments of arbitrary types, it is unsafe, and so issuing a warning for it is useful. Other similar variations of this warning detect related problems: -Wold-style-declaration, -Wold-style-definition for K&R style of function definitions, and -Wmissing-parameter-type for declaring function parameters without specifying their type.

Other interesting and useful warnings based on lexical rules are -Wempty-body for if statements with no body, -Winit-self for definitions of variables initialized to their own (indeterminate) value, -Wmissing-field-initializers to help detect struct members inadvertently initialized to zero as a result of a missing initializer, -Woverride-init to prevent accidentally overwriting the value of an already initialized aggregate member with a different value when using designated initializers, -Wvla for making use of C99 variable length arrays due to their propensity for overflowing the stack, -Wsizeof-array-argument for applying the sizeof operator to a function parameter declared using the array form and expecting it to compute the size of the array, and others.

Type-safety warnings

Another class of front-end warnings is based on enhanced rules for type-safety. C has historically been a permissive language whose specification imposes only loose constraints on the properties of types in mixed expressions. Over time, the laxity has caused its share of bugs, so much so that C++ tightened many of the same rules in its specification. As a result, GCC often issues warnings for strictly valid C and also C++ constructs that are suggestive of bugs due to taking liberties with type conversions.

Typical examples are the -Wcast-qual, -Wcast-align, and -Wcast-function-type warnings for casts that remove one or more qualifiers in an unsafe way, or that increase the alignment of a pointer target type, or cast function pointers to incompatible types, respectively. Others include the -Wint-to-pointer-cast and -Wpointer-to-int-cast pair of options that control diagnostics for implicit conversions between integers and pointers. Conversions between pointers and integers are valid in C even without a cast (they are invalid in C++), but because they can cause bugs, the warnings are helpful in finding those. Yet another example, -Wsign-compare, detects comparisons between expressions with different signedness.

Other front-end warnings

The last category of front-end warnings is simply those that don't fit into either of the two categories above. Typically they involve some simple form of data or control flow analysis, or they require a global view of the whole translation unit (for the most part, the front end works on one declaration or statement at a time).

All the -Wshift- warnings (-Wshift-count-negative, -Wshift-count-overflow, -Wshift-negative-value, and -Wshift-overflow) fall into this category because their efficacy depends on GCC's ability to determine the value of one of the operands of the shift expression.

Similarly, some -Wunused warnings also fall in here because they depend on GCC's ability to track the flow of control or to distinguish statements with no effect from others (-Wunused-value), or to detect unused results of functions declared with attribute warn_unused_result (-Wunused-result). These latter warnings are at least in part implemented outside the front end, in the middle end. Warnings that detect unused static functions (-Wunused-function) are also partially implemented in the middle end, where they have a global view of the entire translation unit.

A slightly different example is -Wtype-limits, which detects equality and relational expressions that are inevitably true or false as a result of the limited range of the type of one of the operands. For the warning to do its job, only the value of one of the equality operands has to be known. In fact, the warning triggers only when the value of just one of the operands, not both, is known.

Trade-offs of front-end warnings

Preprocessor warnings are usually based on straightforward rules and so are prone to neither false positives nor false negatives. They are also easy to suppress in code that cannot be fixed and tend not to be very interesting. They will not be discussed further.

Although more complex than preprocessor warnings, lexical warnings are usually also fairly straightforward and typically have low rates of false positives and false negatives. Unintended instances (false positives) and missing warnings where warnings should be issued (false negatives) are simple bugs in the implementation that can be easily fixed. Except for requests to include options such as -Wall, there are no open bug reports against any of them in GCC Bugzilla.

Like lexical warnings, type-based warnings are also relatively simple and suffer from near-zero rates of false positives and false negatives. A front end is the appropriate compiler component for implementing them.

The trade-offs of front-end warnings come into focus for those that depend on any sort of data or control-flow analysis. Front ends simply don't have sufficient information about either to track the values of variables across statements, let alone across function calls, or to determine whether statements are reachable. As a result, such warnings are prone to both false negatives and false positives, suggesting that a front end may not be the most appropriate choice for their implementation. In the sections that follow, we will take a look at problems that stem from this choice.

False negatives because of a lack of constant propagation

A good example of a front-end warning that is compromised by the lack of constant propagation is -Wformat. The implementation of the warning is able to check printf format strings that are either literals or compile-time constants but not others. For instance, when the format string is declared as a const char array, GCC can check it and catch mistakes like this one:

  const char fmt[] = "%s";
  printf (fmt, 123);   // checked

  warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]

But when the format string is assigned to a pointer variable that itself isn't a constant, the front end doesn't see its value (because the value of the pointer can change between its initialization and use) and GCC fails to detect the same mistake below:

  const char *fmt = "%s";
  printf (fmt, 456);   // not checked

Only later stages of the compiler that track the constant values of variables across expressions (they perform constant propagation) would make it possible to detect the mistake in this latter example.

False negatives due to a lack of value range propagation

The -Wtype-limits option mentioned above is documented like so:

Warn if a comparison is always true or always false due to the limited range of the data type, but do not warn for constant expressions.

This specification leaves it open to a whole range of false positives. For instance, the manual implies -Wtype-limits should be issued for this example:

int f (int i)
{
  if (i < 0)
    i = 0;

  return i - INT_MIN < 0;   // always false
}

In the return statement, because i's value is known to be non-negative and limited by its type to be less than -(INT_MIN + 1), the comparison is necessarily false. Regrettably, partly as a result of a bug, but more interestingly as a result of the unavailability of value range optimization (VRP) in the front end, GCC fails to diagnose this (and many similar cases) because it doesn't have the knowledge that the if statement constrains the range of i's values to no less than zero. This information is readily available in later stages of compilation thanks to the VRP optimizing pass.

Warnings in unreachable code

As an example of a false positive, consider -Wshift-count-overflow and the following two equivalent functions. (The example is deliberately contrived to illustrate the limitation in the implementation of the warning. There are much better ways to code it.)

unsigned long f (unsigned long x)
{
  if (sizeof (long) == 8)
    x <<= 63;
  else
    x <<= 31;
  return x;
}

unsigned long g (unsigned long x)
{
  x <<= (sizeof (long) == 8) ? 63 : 31;
  return x;
}

In ILP64 mode, where most of us develop and test software and where long is a 64-bit wide type, the code compiles without a warning. But in ILP32 mode with 32-bit long, GCC complains:

warning: left shift count >= width of type [-Wshift-count-overflow]
4 | x <<= 63;

The warning is a clear false positive: sizeof (long) is 4 in ILP32, but the assignment on the line the warning points to only takes place when sizeof (long) == 8 holds. GCC must see that in g() because it doesn't issue the warning there, but it cannot determine the same thing in f() because the C front end in GCC translates one statement at a time. In g(), the front end is able to evaluate the right operand of the assignment expression and "fold" it into 31 before it evaluates the shift assignment. But in f(), even though it also evaluates the condition in the if statement to false, it still translates both assignments in the two arms of the if statement, even though one is clearly dead (later stages of the compiler eliminate dead code). The front end's inability to eliminate unreachable code is the main reason for false positives in front-end warnings that depend on flow analysis.

In part 2, learn how implementing flow-based warnings in the GCC middle end overcomes front-end limitations.

More articles for C/C++ developers

Last updated: March 15, 2019