Small library allowing to use dlopen() from statically-linked applications (where statically-linked executable vs loaded shared library may use completely different libc's)
C
280
30 commits
updated Feb 12, 2023
Calling dlopen() function from statically-linked binaries is a well-known (in narrow circles) problem *. A common approach is not supporting it at all. A usual explanation goes along the lines of:
ld --export-dynamic). That avoids carriying
around extra libc.so file, but again requires extra legwork. And it still
undermines the original benefit of static linking, as your app will be
the size of libc.so (+ your app's code).The summary is that if you want to use dlopen() from static binary, you would need to do extra legwork, and would lose benefits of static linking. Ergo, don't use dlopen() with static linking, use dynamic linking instead! And as no reasonable person would use dlopen() with static linking, let's remove dlopen() (and that's entire dynamic loader, quite a bloaty piece of code!) support from statically linked libc, to let people who really need static linking, reap the maximum benefits of it.
Those are all very valid arguments, but they are all based on a "plugin" model: you have a common system, sharing a common libc.
That's not the only usage model though. There's another model, which we'll call "FFI (Foreign Function Interface) model". It goes along the lines of:
Again, the only thing you want is to maintain your static perfect world, independent from outside hustle. But, at user discretion, you want to allow this hustling outside world into your address space, by means of dlopen().
This cute project is a proof-of-concept solution for this usecase.
Implementation idea #1: a (custom) ELF loader. Problem: trying to implement "full" ELF loader (recursively load dependent .so, etc.) is prolematic, e.g. because glibc (libc.so) is tightly coupled with dynamic loader aka interpreter (ld.so). If you just load libc.so, and its dependency ld.so, a lot of stuff in ld.so will remain uninitialized, then libc.so will call into ld.so, which will crash. To properly initialize ld.so, it must be "run", i.e. execution should go into its entry point (and not just ELF INIT func). But when started that way, ld.so loads an executable, which then terminates.
Idea #2: Make ld.so load an executable which will jump back into our custom loader. This way, both ld.so will be initialized, and we get back control. Coupled with setjmp/longjmp, this can be turned into a reusable library. Its structure is:
setjmp. The corresponding
longjmp is wrapped into a global func, whose address (in ascii) we pass as
a command-line argument to the "helper" target binary.longjmp,
after storing the passed dlopen(), etc. function addresses for future use.dlopen(), etc. of the target system.cd srcmake -f Makefile.fdlhelper. As explained
above, it should be built against target system from which you want to load
shared libs dynamically using dlopen(). (If you build this on a typical Linux
system, it will be built against glibc.)make STDLIB=0. (You must
pass STDLIB=0 for full effect. By default, the sample is built against
stdlib, which is helpful during development/debugging.)./foreign_dlopen_demo. While it is static, it will
dynamically load libc.so.6 and call printf() from it. (All this using
fdlhelper executable built in a previous step.)"Foreign dlopen" idea and implementation is by Paul Sokolovsky. The implementation is based on the ELF loader by Mikhail Ilyin: https://github.com/MikhailProg/elf , the original README of that project follows.
A small elf loader. It can load static and dynamically linked ELF EXEC and DYN (pie) binaries. The loader is PIE program that doesn't depend on libc and calls kernel services directly (z_syscall.c).
If the loader needs to load a dynamically linked ELF it places an interpreter (usually ld.so) and a requested binary into a memory and then calls the interpreter entry point.
Default build is for amd64:
$ make
Build for i386:
$ make ARCH=i386
Small build (exclude all messages and printf):
$ make SMALL=1
Load ls:
$ ./loader /bin/ls
Load galculator:
$ ./loader /usr/bin/galculator
C
83.7%
Assembly
8.6%
Makefile
7.7%
Small library allowing to use dlopen() from statically-linked applications (where statically-linked executable vs loaded shared library may use completely different libc's)
C
280
30 commits
updated Feb 12, 2023
Calling dlopen() function from statically-linked binaries is a well-known (in narrow circles) problem *. A common approach is not supporting it at all. A usual explanation goes along the lines of:
ld --export-dynamic). That avoids carriying
around extra libc.so file, but again requires extra legwork. And it still
undermines the original benefit of static linking, as your app will be
the size of libc.so (+ your app's code).The summary is that if you want to use dlopen() from static binary, you would need to do extra legwork, and would lose benefits of static linking. Ergo, don't use dlopen() with static linking, use dynamic linking instead! And as no reasonable person would use dlopen() with static linking, let's remove dlopen() (and that's entire dynamic loader, quite a bloaty piece of code!) support from statically linked libc, to let people who really need static linking, reap the maximum benefits of it.
Those are all very valid arguments, but they are all based on a "plugin" model: you have a common system, sharing a common libc.
That's not the only usage model though. There's another model, which we'll call "FFI (Foreign Function Interface) model". It goes along the lines of:
Again, the only thing you want is to maintain your static perfect world, independent from outside hustle. But, at user discretion, you want to allow this hustling outside world into your address space, by means of dlopen().
This cute project is a proof-of-concept solution for this usecase.
Implementation idea #1: a (custom) ELF loader. Problem: trying to implement "full" ELF loader (recursively load dependent .so, etc.) is prolematic, e.g. because glibc (libc.so) is tightly coupled with dynamic loader aka interpreter (ld.so). If you just load libc.so, and its dependency ld.so, a lot of stuff in ld.so will remain uninitialized, then libc.so will call into ld.so, which will crash. To properly initialize ld.so, it must be "run", i.e. execution should go into its entry point (and not just ELF INIT func). But when started that way, ld.so loads an executable, which then terminates.
Idea #2: Make ld.so load an executable which will jump back into our custom loader. This way, both ld.so will be initialized, and we get back control. Coupled with setjmp/longjmp, this can be turned into a reusable library. Its structure is:
setjmp. The corresponding
longjmp is wrapped into a global func, whose address (in ascii) we pass as
a command-line argument to the "helper" target binary.longjmp,
after storing the passed dlopen(), etc. function addresses for future use.dlopen(), etc. of the target system.cd srcmake -f Makefile.fdlhelper. As explained
above, it should be built against target system from which you want to load
shared libs dynamically using dlopen(). (If you build this on a typical Linux
system, it will be built against glibc.)make STDLIB=0. (You must
pass STDLIB=0 for full effect. By default, the sample is built against
stdlib, which is helpful during development/debugging.)./foreign_dlopen_demo. While it is static, it will
dynamically load libc.so.6 and call printf() from it. (All this using
fdlhelper executable built in a previous step.)"Foreign dlopen" idea and implementation is by Paul Sokolovsky. The implementation is based on the ELF loader by Mikhail Ilyin: https://github.com/MikhailProg/elf , the original README of that project follows.
A small elf loader. It can load static and dynamically linked ELF EXEC and DYN (pie) binaries. The loader is PIE program that doesn't depend on libc and calls kernel services directly (z_syscall.c).
If the loader needs to load a dynamically linked ELF it places an interpreter (usually ld.so) and a requested binary into a memory and then calls the interpreter entry point.
Default build is for amd64:
$ make
Build for i386:
$ make ARCH=i386
Small build (exclude all messages and printf):
$ make SMALL=1
Load ls:
$ ./loader /bin/ls
Load galculator:
$ ./loader /usr/bin/galculator
C
83.7%
Assembly
8.6%
Makefile
7.7%