While the prelink(8) tool and concept are widely detested (and probably not shipped on your distribution), you may be able to use it to link libraries into a binary into low memory:
-r --reloc-only=ADDRESS
Instead of prelinking, just relink given shared libraries
to the specified base address.
Since the address that libraries will be mapped into the process is determined by ld(1), you might be able to modify your Makefile to invoke ld with different --section-start values:
--section-start SECTION=ADDRESS
Set address of named section
-Tbss ADDRESS Set address of .bss section
-Tdata ADDRESS Set address of .data section
-Ttext ADDRESS Set address of .text section
-Ttext-segment ADDRESS Set address of text segment
I moved the text and bss segments down to lower addresses:
$ gcc -Wl,-Ttext-segment=0x200000 -Wl,-Tbss=0x400000 -o broken broken.c
$ readelf -a broken
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x200450
...
If you can move all the sections provided in the executable with --section-start and move the libraries down with prelink(8), you might be able to get all the code loaded below 4 gigabytes.