Illustration de l'article

Compiling & Dangerous : wrong usage of extern keyword in C

Écrit par Victor Lambret

Compiling & Dangerous

C is not a strong typed language but it can run some basic checks. Knowing that, C developers are generally careful because debugging type problems is annoying and can be limited with some simple good practices.

Sometimes a bored developer tries something bold, something new. Here we will detail one of these experiments I recently encountered in a legacy project: what if we use the extern keyword without headers?

It’s uncommon but maybe it’s not that bad? Let’s see…

Test program

We start from here, a simple program printing his version number. version is declared in main file and defined in version file.

I ensure that both declaration and definition are of the same type. Anyway, if something is wrong the compiler will at least raise a warning, right?

I can’t C any problem here

To test this I modified version to pass version type from int to char. There is no complaint from compiler and the version number is correct so obsiously my code is not broken (1)

A charity problem

As the previous program is not bugged I can continue my work, let’s add a simple char ET in version. Wait, what happened with my version number?

The answer is simple: due to type difference main is reading an int of 4 bytes but version is defined as char so it is a single byte long. In the previous program due to luck the 3 extra bytes were containing zeroes so the problem was not detected. Simply by adding a new variable we modified one of those bytes and so increased the version number by 256.

We have to fix that. A good practice is to hide implementation with a function call, let’s try it!

Is half a good practice still a good practice?

Though I refactored with a function exactly like this stackoverflow thread said, I still get a version number that is complete non-sense.

Well there is a tiny problem: I forgot to update the main so the extern declaration is still an int. The version number printed here is the address of the version function.

For curiosity’s sake let’s make the mirror mistake. Here the program is segfaulting because we’re calling the function at adress 42. As 42 is near 0 it’s in a non valid memory range and OS raised a segfault. With a different value it might call a valid function, possibly doing something really wrong.

Understanding the problem

By using the extern mechanism you tell the compiler: I declared something that’s defined elsewhere, you will find at link time. It’s problematic for type verification as the linker works simply with symbol names and addresses. All type related information is forgotten at this time.

It can be verified by looking at object files:

victor@sogilis$ sh gcc -c main2.c
victor@sogilis$ sh gcc -c version3.c
victor@sogilis$ readelf -s main2.o version3.o
File: main2.o
Num   :    Value          Size Type    Bind   Vis      Ndx Name
10    : 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND version

File: version3.o
Num:    Value          Size Type    Bind   Vis      Ndx Name
7: 0000000000000000     1 OBJECT  GLOBAL DEFAULT    2 version

We can see that in main2.o version size is 0 and has NOTYPE.

Fixing the issue

What I would consider a fix is any mechanism that allows type mismatch detection. There are several ways to do that.

Clean fix

This fix is so obvious I’m sure it’s a reflex for almost every C developer. If a module defines an extern variable then this variable is part of the module public interface and should be declared in the header.

Important note: sometimes you can encounter some C code where a module does not include its own header. As we can see with this example, the type conflict is not detected.

The reason is simple: a C definition is also a declaration. So version5.c contains two declarations: one in the header and one in the C file, while version6.c is compiled without header and there is a single declaration.

The hostile environment fix

When you have a code base already corrupted with bad extern usage, you just can’t fix it in a instant. First you want to detect if there are type bugs you have not yet detected with your tests.

It’s possible thanks to the -ftlo option (for link-time optimizer). With this option the compiler adds metadata about the objects and the linker uses them to perform several optimizations. As the metadata contains the object types, the linker also raises a warning if there is a confusion. To quote documentation:

if LTO encounters objects with C linkage declared with incompatible types in separate translation units to be linked together (undefined behavior according to ISO C99 6.2.7), a non-fatal diagnostic may be issued.

Use an extern tool to detect extern issues, it’s logical!

With a static code analyzer like splint you can detect this kind of problem:

Conclusion

Without this legacy project I would have never explored that far how much damage can be done in this situation. It reminds us, C developers what a dangerous language we’re using, and how special and perfect we are to make it work.

Let’s leave the final word to a specialist:

Christ, people. Learn C, instead of just stringing random characters together until it compiles (with warnings). Linus Torvalds

Victor Lambret

Special thanks to Graham & Haze for their feedbacks

(1) for the purpose of this article, let’s pretend that I’m that naive

Illustration de l'article
comments powered by Disqus