May 7, 2020·6 min read·engineeringcgcclinkeranalysis

GCC 10 makes global definitions and ownership paths explicit

GCC 10 changes the default treatment of tentative definitions and introduces an experimental path analyzer. Both changes expose assumptions that separate compilation can conceal.

A header can look like a list of shared declarations while actually defining storage in every source file that includes it. GCC's earlier default behavior often allowed the resulting objects to link together. GCC 10 changes that default and introduces an experimental analyzer for mistakes that depend on a sequence of operations. The May 7 release announcement puts both changes in a release that requires some previously accepted programs to change.

The common subject is information that the source leaves implicit. A global variable needs a place where its definition belongs. A dynamically allocated object needs an account of who may release it and which paths still use it. The compiler cannot supply the application's design, but it can expose places where that design has failed to produce consistent code.

C's small size is part of its appeal to me. Its practical difficulty comes from the agreements that separately compiled code must maintain. These changes make two such agreements more visible without changing the source into a different programming model.

A header declaration can allocate storage

The GCC 10 porting guide explains the new default of -fno-common. Multiple tentative definitions that the previous -fcommon behavior merged now produce linker errors. The recommended source correction is an extern declaration in the header and exactly one definition in a C file. The incorrect header can be as small as this.

int request_count;

If two translation units include that line, each receives the same file-scope tentative definition. The apparent simplicity conceals an important ownership decision. The header has not merely told both files that a shared object exists. It has supplied the definition form to both.

A corrected header describes the shared object without supplying its storage definition. Its consumers can name the object without each claiming to provide it.

#ifndef REQUEST_STATE_H
#define REQUEST_STATE_H

extern int request_count;

#endif

One implementation file then supplies the definition. Including the header in that file also lets the compiler see its declaration alongside the definition.

#include "request_state.h"

int request_count = 0;

The include guard addresses repeated inclusion within one translation unit. It cannot prevent two different translation units from seeing the header, because each compilation processes its own source. This is why adding a guard to the incorrect header does not resolve the multiple-definition problem.

The arrangement also gives the object a definite owner in the source tree. A reader can identify the implementation file that supplies storage and the header through which other files refer to it. That has value even when a linker previously accepted the less explicit arrangement.

The linker error reports a program-wide disagreement

Separate compilation lets each source file be valid enough for compilation while the complete program still has inconsistent definitions. The linker is where the object files meet. A new failure at that stage does not necessarily identify a new source mistake introduced by the compiler upgrade.

For this example, the same header text existed before the upgrade. The change removes a behavior that previously combined its consequences. The repair should therefore answer what the source intends to define, rather than merely remove the diagnostic.

The porting guide retains -fcommon as a legacy workaround. That option can preserve the older merging behavior, but it does not identify which source file owns the object. It is a compatibility decision for a build that intentionally depends on common symbols. It is less informative than correcting an accidental header definition.

The distinction matters in generated code as well. If a generator emits the incorrect header repeatedly, fixing each resulting source tree addresses symptoms downstream. Correcting the generator's representation of declarations addresses the source of the repeated error. The language issue remains the same whether a person or a tool wrote the line.

A useful design question is whether the object truly needs external linkage. If it is private state for one implementation file, a shared header may not need to mention it at all. That is an application decision beyond the mechanical extern repair, but the linker error creates a reason to inspect the intended sharing.

Ownership errors need more than one statement

The new analyzer addresses a different class of incomplete agreement. The GCC 10.1 analyzer documentation describes interprocedural exploration of program paths. Enabling -fanalyzer can produce warnings for errors such as double release or use after release. GCC describes the facility as experimental in this release.

An illustrative failure can contain operations that each look reasonable when considered alone. One branch releases an allocation after an error. Later cleanup releases the same allocation without accounting for that earlier branch. The error lies in the reachable sequence and the allocation's state when cleanup runs.

This is why a path report matters. A statement-level message pointing only at the last free omits the earlier event that makes it invalid. An explanation that identifies the preceding release and the intervening decisions gives the programmer a claim to examine.

Consider a helper that releases an object on failure while its caller assumes ownership remains with the caller. Both functions can appear internally consistent. Their combined execution reveals the disagreement. The interface needs to say whether a failed call consumes the supplied resource, and both implementations need to follow that rule.

I regard the analyzer's value as making those disagreements concrete. C leaves resource ownership largely to conventions and control flow. A report that follows a resource through those conventions can help explain the defect without inventing a new application contract.

Limits affect what silence means

The analyzer manual says the analysis costs more than ordinary warnings. It also documents internal exploration limits and the optional -Wanalyzer-too-complex diagnostic. By default, sufficiently complex analysis can stop without producing that particular warning.

A resource protocol can also have several valid states after failure. One API might preserve the input object for another attempt, while another might consume it unconditionally. A caller cannot choose correctly from the word failure alone. It needs the documented ownership rule, and a path report can reveal where its interpretation differs from the implementation. This is a specific reason for examining the report across function boundaries instead of changing only the statement at the reported location.

Those limits change how a clean result should be interpreted. A report gives a specific path to investigate. An absence of reports does not establish that the analyzer explored every path through the program. The two outcomes carry different amounts of evidence.

This distinction is particularly important when a function has many independent decisions. Combining the decisions creates more possible histories for the same source location. Resource state can differ among those histories, even though the final statement is textually identical. Any practical analysis needs a way to control that growth.

The optional complexity warning helps distinguish a completed exploration from one that encountered a documented limit. It still does not transform the tool into a proof of whole-program correctness. It gives the build a more explicit account of what the analyzer attempted.

There is also a practical difference between a compiler diagnostic and a runtime failure. A static report need not wait for an execution to take the troublesome path. It therefore offers a way to examine uncommon error handling, where manual reasoning often spans multiple functions and cleanup blocks.

Making the program's agreement readable

The global-definition change and the analyzer ask different questions. The linker checks how separately emitted definitions combine. Path analysis follows possible state changes through code. Their shared practical effect is to require a clearer account of relationships that individual lines do not explain.

For the shared counter, the account is a declaration available to consumers and a single storage definition. For a resource, it includes the conditions under which ownership transfers and which paths perform cleanup. A repair that leaves those relationships ambiguous can remove one diagnostic while preserving the reason the code was difficult to assess.

A small ownership example shows why naming alone is insufficient. Calling a pointer owned does not establish whether a failed helper releases it. Setting a pointer to null after releasing it changes a local value, but does not update every alias that another function might retain. The appropriate representation depends on the actual interface and lifetime. An analyzer report is useful when it gives enough path detail to connect the observed operations to that interface.

GCC 10 gives existing C programs a concrete reason to revisit these agreements. Shared globals now need definitions that survive a stricter link, and resource-management paths can receive an additional form of static examination. The resulting source can make the defining translation unit and the responsible cleanup path visible to the next reader.