Tl:dr: If you need to keep using multiple versions of libc, like many of us do, then a key utility to use is PatchElf.
Normally, if all you want is to force your own version of the program interpreter when you build your own program, all you need to do is to compile with the following options:
gcc -Wl,-dynamic-linker,/my/lib/ld-linux.so.2 ...
But this does not work with third-party utilities and with some shared objects which are however executable all by themselves, so you invoke patchelf on a given ELF program as follows:
patchelf --set-interpreter /my/lib/my-ld-linux.so.2 someprogram
The reason why this is not just a matter of editing with an hex editor the old executable and overwriting the old interpreter address with the new one, is that the two need not be the same length; patchelf takes care of enlarging your executable for you.
You can also change the rpath variable, as follows:
patchelf --set-rpath /my_libs:$LD_LIBRARY_PATH someprogram
I find this much handier than using the usual wrappers around commands with LD_LIBRARY_PATH.
As for execve, it must be just about the most fundamental system call in Unix: execve(filename) executes the given filename. For instance, your shell executes a command by means of a call of the execve family: it first forks, then the child process thus generated execve's the command (ls, cd,,... you name it). Most programs need dynamically linked libraries, for instance for ls:
$ ldd $(which ls)
linux-vdso.so.1 => (0x00007ffe3b0e7000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f1423dda000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1423a11000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f14237a0000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f142359c000)
/lib64/ld-linux-x86-64.so.2 (0x0000563800043000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f142337f000)
When you load ls, it is not the binary normal entry point which is used at first, but ld-linux instead: it takes care of loading all the unresolved, required libraries, and then it transfers control to the real application, ls in this case. It does this by execve'ing the program file.
I am unable to say why exactly your program crashes., but I would try checking that all programs invoked require the same program interpreter; you do this by checking the output of:
$ readelf -l /bin/ls
Elf file type is EXEC (Executable file)
Entry point 0x4049a0
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...........................
(the emphasis is mine...).
EDIT: an excellent discussion by one of the authors of the execveat system call can be found here, and the relevant kernel code is here. The code shows that no system variable is read, and the routine load_elf_binary looks at the program interpreter PT_INTERP.