Is it possible to override symbols in the non-dynamic segments of dynamically linked executable? (For example: the .text and .(ro)data segments of an ELF executable on Linux.)
I know that it is possible to override references in the dynamic section using LD_PRELOAD
.
An answer at What is the difference between LD_PRELOAD_PATH and LD_LIBRARY_PATH? claims that LD_PRELOAD
also allows overriding statically linked functions, but I'm not sure how. The man page says
LD_PRELOAD A list of additional, user-specified, ELF shared objects to be loaded before all others. This feature can be used to selectively override functions in other shared objects.
so unless everything in a dynamically linked executable is a shared object (only some are included and some are referenced?) this sounds like what I want isn't possible using LD_PRELOAD
, but maybe still using some other tool?
Your terminology suggests some haziness about the anatomy of an ELF program but I believe you are asking: If the definition of symbol foo
is statically linked into a program, can that definition by dynamically overridden with an alternative definition
of foo
provided by a shared library pre-loaded at runtime?
In that case the answer is No.
You'll see why if you know how some parts of an ELF program actually enable the dynamic linker, at runtime, to bind symbols it can see in the program to definitions that it finds in shared libraries (whether or not such a binding is overriding any prior or alternative one), and if you also know how symbols that have statically linked definitions in the program may or may not be visible to the dynamic linker to do anything with.
When I talk about symbols in an ELF file here, I always exclude local symbols (a.k.a symbols with
internal linkage) such as are created by the C storage-class specifier static
. They are not
visible either to the static linker or the dynamic linker. When I say that a symbol is statically linked
into the program, I mean that the static linker physically merges its definition into a code or data section of the
program executable.
Gist
By default, the static linker does not enter any symbol in the dynamic symbol table of a program that is statically defined in the program. It is impossible for the dynamic linker to bind such a symbol to another definition from a shared library, whether pre-loaded or loaded in the normal course of events, because the dynamic linker does not see any such symbol. The static linker can be expressly directed to add symbols that are statically defined in the program to its dynamic symbol table, but they appear there as defined and the dynamic linker cannot be coerced to redefine a defined symbol that it sees there.
The dynamic symbol table and dynamic section of a program
A symbol referenced by an ELF program that is visible to the dynamic linker is a dynamic symbol.
A dynamic symbol is exposed to the dynamic linker in the dynamic symbol table (.dynsym
section) of
the program. The dynamic symbols are the only ones that the dynamic linker
can bind to definitions provided by shared libraries because they are the only symbols seen by
the dynamic linker.
By default all the symbols in a the dynamic symbol table of a program will be undefined symbols. That's because the dynamic symbol table of a program is put there by the static linker at buildtime and its default purpose is to inform the dynamic linker, at runtime, of the symbols which it must bind to definitions it finds in the shared libraries it loads at the program's request. So such symbols are undefined in the program: they are defined in the shared libraries it needs to load. If any symbols appear in the dynamic symbol table as defined, that means they are defined in the program and the dynamic linker of course will not bother trying to find other definitions for them in the shared libraries.
All symbols that are referenced in the program executable are referenced in object files that are statically linked into it (either object files explicitly input in the static linker's commandline or object files selected for extraction from static libraries input in the commandline).
The undefined symbols that are entered into the program's dynamic symbol table by default are the ones for which the static linker has found references in statically linked object files and:
are not defined in any of the input object files.
are defined by the shared libraries that are input in the static linker's commandline.
By definition, those shared libraries wont't (and can't) be statically linked into the program, hence these
symbols cannot be defined in the program. Instead, the static linker
just puts into the program the fact that it needs those shared libraries, for the information of the dynamic linker
at runtime. It puts a dynamic section into the program (.dynamic
section) and in that section it writes (among other things)
a list of the shared libraries that the dynamic linker will need to load to resolve all the undefined symbols
that it has written into the program's dynamic symbol table.
An runtime, the dynamic linker reads the program's dynamic symbol table (.dynsym
) and learns what undefined dynamic
symbols it has to resolve using shared libraries. It reads the program's dynamic section (.dynamic
) and learns what
shared libraries it must find and load to resolve those symbols. It goes to work, using an algorithmically
defined sequence of directories in which to search for shared libraries. The work is recursive,
because each shared library that it loads has its own dynamic symbol table, listing the symbols that are
defined in it, as well as the ones that it references but does not define; and it has its own dynamic section
that lists the shared libraries it needs to resolve its undefined symbols. If the work finishes with
all the recursively needed shared libraries found and all of the recursively discovered undefined references
resolved by the recursively discovered definitions, then the program gets to start successfully.
The global symbol table of a program
The dynamic symbol table of a program is distinct from its global symbol table (.symtab
section). The global
symbol table lists all global symbols in the program that were referenced in the object files statically
linked into the program. By default all of these symbols will appear as defined, because if any symbol is
referenced in the program and is undefined in the program, then either:-
a) a definition was found by the static linker in some shared library, or
b) no definition was found by the static linker at all.
In case a) the symbol will be listed in the dynamic symbol table (as undefined) and not listed in the global symbol table. In case b) the static linker by default will fail the linkage with an unresolved reference error, so the program will not even exist.
But a statically linked definition with its symbol in the global symbol table is invisible to the dynamic linker. The
global symbol table can be stripped out (man strip
)
with no effect on execution of the program. Its existence only supports tools that investigate or manipulate ELF files.
But that's all by default. What about coercion?
That's a dead end too.
You can coerce the static linker to link a program that contains unresolved symbol references (linker option -z=undefs
). That is
obviously of no help because you are interested in dynamically overriding the statically linked definition of a defined symbol.
You can also coerce the static linker to add all, or a selection, of the symbols it puts in global symbol table of
a program into the dynamic symbol table as well: linker option --export-dynamic
to add them all (the GCC linkage option
-rdynamic
enables that one), linker option --export-dynamic-symbol=sym
to
add one, linker option --export-dynamic-symbol-list=file
to export a selection. But that doesn't help either because
these statically defined symbols will be added to the dynamic symbol table as defined. The usefulness of
that is that it enables shared libraries that are loaded by the program to contain undefined references to symbols
that the dynamic linker will resolve to definitions in the program. It is of no use for dynamically overriding such
a definition, because the dynamic linker will not seek to resolve a symbol that is already defined.
A static linker could be written that classifies a symbol in the dynamic symbol table with a new type, say, defined-as-last-resort, meaning the the dynamic linker should bind it to the definition statically linked into the file that exports it failing a definition in any (other) shared library. Perverse, but it would do the trick. But if such a static linker has been written, it's not the GNU/linux linker.
A worked illustration
$ cat main.c
void statically_linked(void);
void dynamically_linked(void);
int main(void)
{
statically_linked();
dynamically_linked();
return 0;
}
$ cat static.c
#include <stdio.h>
void statically_linked(void)
{
puts(__FUNCTION__);
}
$ cat dynamic.c
#include <stdio.h>
void dynamically_linked(void)
{
puts(__FUNCTION__);
}
$ cat preload_static.c
#include <stdio.h>
void statically_linked(void)
{
printf("%s: pre-loaded\n",__FUNCTION__);
}
$ gcc -shared -o libdynamic.so dynamic.c
$ gcc -shared -o libpreload_static.so preload_static.c
$ gcc -o prog main.c static.c -L. -ldynamic
$ LD_LIBRARY_PATH=./ ./prog
statically_linked
dynamically_linked
$ readelf --syms --wide prog | egrep \(.symtab\|.dynsym\|ally_linked\)
Symbol table '.dynsym' contains 8 entries:
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dynamically_linked
Symbol table '.symtab' contains 40 entries:
32: 0000000000001182 26 FUNC GLOBAL DEFAULT 16 statically_linked
33: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dynamically_linked
The dynamic section of prog
contains:
$ readelf --dynamic --wide ./prog | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libdynamic.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
The dynamic linker will find libc.so.6
by default search. It will have to be told where to
look for libdynamic.so
, which we did with LD_LIBRARY_PATH=./
. Alternatively we could
have got the static linker to add this information to the dynamic section:
$ gcc -o prog main.c static.c -L. -ldynamic -Wl,-rpath=$(pwd)
$ readelf --dynamic --wide ./prog | egrep \(NEEDED\|RUNPATH\)
0x0000000000000001 (NEEDED) Shared library: [libdynamic.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [/home/imk/develop/so/scrap]
$ unset LD_LIBRARY_PATH
$ ./prog
statically_linked
dynamically_linked
statically_linked
appears as defined in the global symbol table only. dynamically_linked
appears as undefined (UND
) in both the global symbol table and the dynamic symbol table.
dynamically_linked
is the only symbol that is seen by the dynamic linker, so it is
the only one for which the dynamic linker could be induced to seek a definition in a some shared library pre-loaded before libdynamic.so
, where
it will otherwise find the same definition discovered by the static linker.
Let's rebuild prog
this time adding statically_linked
to the dynamic symbol table:
$ gcc -o prog main.c static.c -rdynamic -L. -ldynamic -Wl,-rpath=$(pwd)
$ readelf --syms --wide prog | egrep \(.symtab\|.dynsym\|ally_linked\)
Symbol table '.dynsym' contains 17 entries:
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dynamically_linked
16: 0000000000001182 26 FUNC GLOBAL DEFAULT 16 statically_linked
Symbol table '.symtab' contains 40 entries:
34: 0000000000001182 26 FUNC GLOBAL DEFAULT 16 statically_linked
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dynamically_linked
There it is - it's defined (in section 16, which is .text
). Let's try to override the
statically linked definition by pre-loading libpreload_static.so
. That should result
in the program outputing statically_linked: preloaded
instead of statically_linked
$ LD_PRELOAD=./libpreload_static.so ./prog
statically_linked
dynamically_linked
But it doesn't. statically_linked
is defined in the dynamic symbol table. The
dynamic linker sees nothing to be done.
If that's convincing, stop. But I anticipate...
This is the example presented by @hackerb9 in this answer, which purportedly showed that:
LD_PRELOAD can replace functions statically linked into a binary.
But the answer does not actually demonstrate that. It just implies that @hackerb9 observed it to be so after doing what is described in the example.
The implied claim is that:
The stock bash
shell is built with libreadline.a
linked in furnishing statically linked definitions of the readline
functions called by bash
. (That much is true).
@hackerb9 built a version of the shared library libreadline.so
in which some of those functions were modified.
When @hackerb9 LD_PRELOAD
-ed the modified libreadline.so
for running bash
, the modified readline
functions
were called, overriding their statically linked definitions.
Since that defies first principles, our only resort is to test it by reproduction. Sadly that's a long addendum, but...
Let's do it!
For this, my modified readline
shared library will be built from the source:
$ cat badrl.c
#include <stddef.h>
#include <assert.h>
char * readline (const char *prompt) {
assert(0);
return NULL;
}
#ifdef PROGRAM
int main(void)
{
(void)readline(NULL);
return 0;
}
#endif
A program that calls my readline
function is:
$ gcc -o prog -DPROGRAM badrl.c
and it aborts:
$ ./prog
prog: badrl.c:5: readline: Assertion `0' failed.
Aborted (core dumped)
Build my shared readline library, called libbadrl.so
:
$ gcc -shared -o libbadrl.so badrl.c
My installed bash
version is 5.2.21. I'll build that one locally from source to tinker with it:
$ wget https://ftp.gnu.org/gnu/bash/bash-5.2.21.tar.gz
...[all good]...
$ tar -xzf bash-5.2.21.tar.gz
$ cd bash-5.2.21/
$ ./configure
...[all good]...
I'll grep the gcc
linkage line for bash
out of the make
output for later use:
$ make 2>&1 | grep 'gcc .*-o bash .*'
gcc -L./builtins -L./lib/readline -L./lib/readline -L./lib/glob -L./lib/tilde -L./lib/malloc -L./lib/sh -rdynamic -g -O2 -o bash shell.o eval.o y.tab.o general.o make_cmd.o print_cmd.o dispose_cmd.o execute_cmd.o variables.o copy_cmd.o error.o expr.o flags.o jobs.o subst.o hashcmd.o hashlib.o mailcheck.o trap.o input.o unwind_prot.o pathexp.o sig.o test.o version.o alias.o array.o arrayfunc.o assoc.o braces.o bracecomp.o bashhist.o bashline.o list.o stringlib.o locale.o findcmd.o redir.o pcomplete.o pcomplib.o syntax.o xmalloc.o -lbuiltins -lglob -lsh -lreadline -lhistory -ltermcap -ltilde -lmalloc -ldl
Notice - scroll right - that the gcc
linkage options include -rdynamic
. That enables --export-dynamic
for the linker and means that all
the symbols in the global symbol table will be added to the dynamic symbol table. We've already observed that makes no difference.
I've got a ./bash
from that make
. Let's check the symbol tables for readline
:
$ readelf -Ws ./bash | egrep \(.symtab\|.dynsym\|FUNC.*readline$\)
Symbol table '.dynsym' contains 2603 entries:
932: 00000000000d1c90 153 FUNC GLOBAL DEFAULT 16 readline
1323: 0000000000099510 2039 FUNC GLOBAL DEFAULT 16 initialize_readline
Symbol table '.symtab' contains 3995 entries:
1846: 0000000000099510 2039 FUNC GLOBAL DEFAULT 16 initialize_readline
2260: 00000000000d1c90 153 FUNC GLOBAL DEFAULT 16 readline
Yes, it's defined and has been statically linked per the .symtab
, and -rdynamic
has put it into .dynsym
as well.
Check my bash
runs, and its read
command works:
$ ./bash
$ read
Hello World!
$ echo $REPLY
Hello World!
$ exit
exit
Good. Now I'll compile my aborting badrl.c
into a object file (PROGRAM
not defined):
$ gcc -c -o badrl.o ../badrl.c
And I'll repeat the gcc
linkage command for bash
with the following tweaks:
bash
to badrlbash
.badrl.o
to the linkage, so that my aborting readline
function is statically linked.readline
library from -lreadline
to -Wl,--push-state,-Bdynamic,-lreadline,--pop-state
. The
effect of that is to link the shared library version of libreadline
instead of linking it statically, eliminating any
multiple definition error for readline
caused by statically linking definitions from both badrl.o
and ./lib/readline/libreadline.a(readline.o)
,
(the one output by make
, pulled into the linkage by other symbols).
The shared libreadline
will resolve all the undefined libreadline
symbols in the program other than the readline
function from badrl.o
already statically linked.-L./lib/readline
from the linkage command so that the linker won't look there for the readline
library
and will instead just search in my system default library directories.That gives:
$ gcc -L./builtins -L./lib/glob -L./lib/tilde -L./lib/malloc -L./lib/sh -rdynamic -g -O2 -o badrlbash badrl.o shell.o eval.o y.tab.o general.o make_cmd.o print_cmd.o dispose_cmd.o execute_cmd.o variables.o copy_cmd.o error.o expr.o flags.o jobs.o subst.o hashcmd.o hashlib.o mailcheck.o trap.o input.o unwind_prot.o pathexp.o sig.o test.o version.o alias.o array.o arrayfunc.o assoc.o braces.o bracecomp.o bashhist.o bashline.o list.o stringlib.o locale.o findcmd.o redir.o pcomplete.o pcomplib.o syntax.o xmalloc.o -lbuiltins -lglob -lsh -Wl,--push-state,-Bdynamic,-lreadline,--pop-state -lhistory -ltermcap -ltilde -lmalloc -ldl
/usr/bin/ld: ./lib/sh/libsh.a(tmpfile.o): in function `sh_mktmpname':
/home/imk/develop/so/scrap/bash-5.2.21/lib/sh/tmpfile.c:160:(.text+0x18f): warning: the use of `mktemp' is dangerous, better use `mkstemp' or `mkdtemp'
And I've got:
$ ./badrlbash
badrlbash: ../badrl.c:5: readline: Assertion `0' failed.
Aborted (core dumped)
Now if @hackerb9 is right, then I can fix this broken bash by overriding the statically linked definition of readline
by pre-loading the system libreadline.so
,
which is:
$ find /usr/lib -name libreadline.so
/usr/lib/x86_64-linux-gnu/libreadline.so
Here goes:
$ LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libreadline.so ./badrlbash
badrlbash: ../badrl.c:5: readline: Assertion `0' failed.
Aborted (core dumped)
But it makes no difference.
Likewise, if @hackerb9 is right, then by pre-loading my libbadrl.so
I can cause an abort in the stock bash 5.2.21 I built first.
$ LD_PRELOAD=../libbadrl.so ./bash
$ read
Hello World!
$ echo $REPLY
Hello World!
$ exit
exit
But that doesn't happen either.