Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No warning with uninitialized C string

I'm currently wondering why I don't get an error from GCC during compilation/linking of a small C program.

I declared in version.h the following string:

const char* const VERSION;

In version.c I have set the initialization of the variable:

const char* const VERSION = "0.8 rev 213";

No problem with that. I can use the string in the rest of the program.

If the c file is missing, no error occurs during compilation/linking but the program fails with SIGSEGV (of course) when it tries to access the variable.

Is my way of setting up the variable VERSION correct or is there a better way? Or is there a chance to get an error during compilation/linking?

like image 842
Max Senft Avatar asked Jul 02 '18 11:07

Max Senft


3 Answers

You have defined (not just declared) a variable in the header.

If you ever include this header from more than one source file, the behaviour is undefined. Here's the relevant quote from the standard:

J.2 Undefined behavior

An identifier with external linkage is used, but in the program there does not exist exactly one external definition for the identifier, or the identifier is not used and there exist multiple external definitions for the identifier.

You are relying on a GCC-specific (actually common to many compilers, but still non-standard) behaviour here, which is merging of duplicate tentative data definitions. See help for -fcommon and -fno-common GCC compilation flags. Not all compilers behave this way. Historically this is the common behaviour for linkers, because that's how Fortran worked before there was C.

Assuming this language extension, one of the definitions (one that has an explicit initializer) initialises the variable to point to your string literal. But if you omit this definition, it will remain zero-initialised. That is, it will be a constant null pointer. Not very useful.

To make long story short, never ever do that. In order to declare (but not define) a global variable in a header, use extern. If you do, and try to omit a definition elsewhere, you will likely get a linker error (though the standard does not require diagnostic for this violation, all known implementation produce one).

like image 174
n. 1.8e9-where's-my-share m. Avatar answered Oct 17 '22 09:10

n. 1.8e9-where's-my-share m.


Your example works because of a Fortran-inspired (mis)feature of C (but not C++) called tentative definitions (6.9.2p2) which is commonly but nonstandardly extended to multiple files.

Tentative definitions are variable declarations without extern and with no initializer. In common implementations (pun intended), tentative definitions create a special kind of symbol which is called a common symbol. During linking, in the presence of a regular symbol of the same name, other common symbols become references to the regular symbol which means all the empty VERSIONs in your translation units created there due to inclusions will become references to the regular symbol const char* const VERSION = "0.8 rev 213";. If there's no such regular symbol, the common symbols will get merged into a single zero-initalized variable.

I don't know how to get a warning against this with a C compiler.

This

const char* const VERSION;
const char* const VERSION = "0.8 rev 213";

seems to work with gcc no matter what I've tried (g++ won't accept it -- C++ doesn't have the tentative definition feature and it doesn't like const variables that aren't explicitly initialized). But you can compile with -fno-common (which is a fairly "common" (and highly recommended) nonstandard option (gcc,clang, and tcc all have it)) and then you will get a linker error if the non-initialized and the initialized extern-less declarations are in different translation units.

Example:

v.c:

const char * VERSION;

main.c

const char* VERSION;
int main(){}

compilation and linking:

gcc main.c v.c #no error because of tentative definitions
g++ main.c v.c #linker error because C++ doesn't have tentative definitions
gcc main.c v.c -fno-common #linker error because tentative defs. are disabled

(I removed the second const in the example for the sake of the C++ example — C++ additionally makes const globals static or something like that which only complicates the demonstration of the tentative definition feature.)

With tentative definitions disabled or with C++, all your variable declarations in headers should have extern in them

version.h:

extern const char* const VERSION;

and you should have exactly one definition for each global and that definition should have an initializer or be without extern (some compilers will warn if you apply extern to an initialized variable).

version.c:

#include "version.h" //optional; for type checking
const char* const VERSION = "0.8 rev 213";
like image 20
PSkocik Avatar answered Oct 17 '22 10:10

PSkocik


In version.h you should declare VERSION as a extern like

extern const char* const VERSION;

And in version.c you should define the extern variable like

const char* const VERSION = "0.8 rev 213";

EDIT :- Also you need to make sure that only one source file defined the variable VERSION while the other source files declared it extern and you can do it by defining it in a source file VERSION.c and put the extern declaration in a header file VERSION.h.

like image 6
Achal Avatar answered Oct 17 '22 10:10

Achal