Weird PIE-related linker errors on OS X
Weird error – Recently, I have encountered a really strange linker error after a ridiculously small change in the code. Look at the error message and find the reason in 5 seconds without looking further in the text and I owe you a beer ;).
Linking failed with:
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not allowed in code signed PIE,
but used in __ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5imbueERKNS_6localeE from test.cpp.o.
To fix this warning, don't compile with -mdynamic-no-pic or link with -Wl,-no_pie
I omit the majority of the details apart from:
ld: 32-bit RIP relative reference out of range (-4296998218 max is +/-4GB): from __ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5imbueERKNS_6localeE (0x1001EFD30) to __ZNSt3__17codecvtIcc11__mbstate_tE2idE@0x00057A50 (0x00000000) in '__ZNSt3__113basic_filebufIcNS_11char_traitsIcEEE5imbueERKNS_6localeE' from test.cpp.o for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Quest for the remedy
An important thing to mention is that we compiled with
-fPIC and never used
If you are not acquainted with PIC, PIE and so on, have a read or just skip the details and go to the solution.
Looking at the error message, I immediately searched for
-mdynamic-no-pic in the compiler flags, but it wasn’t there. Checked. Let’s search further. Then, I tried to recompile with
-Wl,-no_pie… Hmmm, not the best idea, other weird errors. I started googling… someone solved his problem by adding
-Wl,-read_only_relocs,suppress. It wasn’t me…
Ok, so let’s start thinking. I do generate position-independent code (PIE), no strange flags are passed to the compiler, nor linker. Apparently, the RIP is 32-bit (even though I compile a 64-bit program) and it is out of range. Nothing comes to my mind. Finally, I’ve found a forum on a very similar problem. But before jumping into the solution…
The code (being the cause)
Somewhere in our codebase in a third-party library (Skein to be precise), I’ve found:
#pragma GCC visibility push(hidden)
// ... code ...
#pragma GCC visibility pop
Well, what’s the relation to our linker error?! What’s this hodgepodge?! I’ve hidden the interesting part on purpose. The code between push and pop
#pragmas contained, among other things,
#include <stddef.h>. Whaaaat??? Yes, we changed the visibility of symbols from standard C headers! On the aforementioned forum, the very same bug was pinpointed.
The full answer can be found here. Put shortly, when we include standard headers that happen to contain global variables such as stdout, we try to be smarter than the compiler. We trick it into thinking that these (extern) global variables are located in our process but they are not!
The compiler, while optimizing, assumes that hidden symbols cannot be accessed outside of the module which declares them. Without optimization, the compiler would use a global offset table (GOT) to get the full 64-bit address and no problem. But as we kindly asked to optimize, it will! So the compiler take the instruction pointer register (RIP) and just add a 32-bit offset. 32-bit should be sufficient, because, on OS X, a statically allocated code/data per binary image never exceeds 4 GB.
Well, it should be obvious, just don’t
#pragmas that change the visibility. And voilà, DONE!