Question

TL;DR

Protection against binary incompatibility resulting from compiler argument typos in shared, possibly templated headers' preprocessor directives, which control conditional compilation, in different compilation units?

Ex.

g++ ... -DYOUR_NORMAl_FLAG ... -o libA.so
/**Another compilation unit, or even project. **/
g++ ... -DYOUR_NORMA1_FLAG ... -o libB.so
/**Another compilation unit, or even project. **/
g++ ... -DYOUR_NORMAI_FLAG ... main.cpp libA.so //The possibilities!

The Basic Story

Recently, I ran into a strange bug: the symptom was a single SIGSEGV, which always seemed to occur at the same location after recompling. This led me to believe there was some kind of memory corruption going on, and the actual underlying pointer is not a pointer at all, but some data section.

I save you from the long and strenuous journey taking almost two otherwise perfectly good work days to track down the problem. Sufficient to say, Valgrind, GDB, nm, readelf, electric fence, GCC's stack smashing protection, and then some more measures/methods/approaches failed.

In utter devastation, my attention turned to the finest details in the build process, which was analogous to:

  • Build one small library.
  • Build one large library, which uses the small one.
  • Build the test suite of the large library.

Only in case when the large library was used as a static, or a dynamic library dependency (ie. the dynamic linker loaded it automatically, no dlopen) was there a problem. The test case where all the code of the library was simply included in the tests, everything worked: this was the most important clue.

The "Solution"

In the end, it turned out to be the simplest thing: a single (!) typo.

Turns out, the compilation flags differed by a single char in the test suite, and the large library: a define, which was controlling the behavior of the small library, was misspelled. Critical information morsel: the small library had some templates. These were used directly in every case, without explicit instantiation in advance. The contents of one of the templated classes changed when the flag was toggled: some data fields were simply not present in case the flag was defined! The linker noticed nothing of this. (Since the class was templated, the resultant symbols were weak.) The code used dynamic casts, and the class affected by this problem was inheriting from the mangled class -> things went south.

My question is as follows: how would you protect against this kind of problem? Are there any tools or solutions which address this specific issue?

Future Proofing

I've thought of two things, and believe no protection can be built on the object file level:

  • 1: Save options implemented as preprocessor symbols in some well defined place, preferably extracted by a separate build step. Provide check script which uses this to check all compiler defines, and defines in user code. Integrate this check into the build process. Possibly use Levenshtein distance or similar to check for misspellings. Expensive, and the script / solution can get complicated. Possible problem with similar flags (but why have them?), additional files must accompany compiled library code. (Well, maybe with DWARF 2, this is untrue, but let's assume we don't want that.)
  • 2: Centralize build options: cheap, customization option left open (think makefile.local), but makes monolithic monstrosities, strong project couplings.

I'd like to go ahead and quench a few likely flame inducing embers possibly flaring up in some readers: "do not use preprocessor symbols" is not an option here.

  • Conditional compilation does have it's place in high performance code, and doing everything with templates and enable_if-s would needlessly overcomplicate things. While the above solution is usually not desirable it can arise form the development process.
  • Please assume you have no control over the situation, assume you have legacy code, assume everything you can to force yourself to avoid side-stepping.
  • If those won't do, generalize into ABI incompatibility detection, though this might escalate the scope of the question too much for SO.

I'm aware of:

No correct solution

OTHER TIPS

If it matters, don't have a default case.

#ifdef YOUR_NORMAL_FLAG
  // some code
#elsif YOUR_SPECIAL_FLAG
  // some other code
#else
  // in case of a typo, this is a compilation error
#  error "No flag specified"
#endif

This may lead to a large list of compiler options if conditional compilation is overused, but there are ways around this like defining config-files

flag=normal
flag2=special

which get parsed by build scripts and generate the options and can possibly check for typos or could be parsed directly from the Makefile.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top