TLDR; -fno-pie
is a "code generation option" while -no-pie
is a "linker option". You need both.
-fno-pie
tell GCC that you don't want to make a PIE. If you are not making a PIE you are making either a shared object (using -shared
) or an ordinary executable.
Ordinary executables are loaded below the 4GiB and at a fixed address, furthermore their symbols cannot be interposed so -fno-pie
tell GCC that can translate an expression like &foo
in something like mov eax, OFFSET FLAT foo
.
It doesn't have to use the GOT since the symbols are not interposed and it doesn't have to use RIP-relative addressing since a 32-bit address fits in the 32-bit immediate/displacement of x86-64 instructions.
Check out what -fpie
/-fno-pie
do in terms of assembly instructions.
An instruction like mov eax, OFFSET FLAT foo
creates a R_X86_64_32
relocation, this relocation is used in 64-bit programs when the compiler is sure that an address will always fit 32 bits (i.e. is below 4GiB).
However -fno-pie
doesn't stop GCC from passing -pie
to the linker.
So the linker sees a R_X86_64_32
relocation and is still instructed to make a PIE executable. The relocation promises that the address will be below 4GiB but the -pie
flag promises that the executable is meant to be loaded anywhere, including above 4GiB.
These contrast each other and the linker is required to check this stalemate and produce an error.
To tell the linker that indeed you did not want to link a PIE executable, you need to pass -no-pie
to GCC.
You can pass -v
to GCC while compiling a file to see what options are passed to collect2
(a wrapper to the linker).
Regardless of the presence of -fno-pie
, -pie
is still passed to collect2
.
Adding -no-pie
to GCC will suppress -pie
in the collect2
command line.
Note: Older distributions built GCC without defaulting to -fpie -pie
.