The one header library to allow statically linked binaries to get access to the dynamic loading functionality.
The trick for doing this is to make the application do part of the kernel job
and load the dynamic linker manually. This has the additional benefit of making
the application libc version independent. The path to the linker is found
using an assumption that on each Linux machine there is a dynamically linked
/bin/sh binary from which the system's linker path can be obtained. Assuming
this holds on all distributions, this also makes the application linker
independent.
For doing all of the preparatory work, the library needs some space to store data required for the dynamic linker to launch properly. Trying to take as little space as possible and be non-intrusive as possible, the library allocates all of the permanent data on the stack in a tightly packed manner. This uses ~2K (depends on the number of args and env vars present) of the stack space which is just a small percentage of the usual stack on linux.
Note
Currently only x86_64 and aarch64 are supported
Just include the header to your application and ensure the main function signature is
intmain(intargc, char**argv)Note
Even though unlikely, there is still a possibility that loading the dynamic
linker will fail. To check if there was an error just check sd_got.success
and if there was one, sd_got.error will contain the error code. It is
advised to check for the success at the beginning of the main.
If the linker was loaded successfully, sd_got will contain dlopen, dlsym,
dlclose, dlerror function pointers. There are also SD_RTLD_NOW and
SD_RTLD_LAZY macros already defined to avoid including additional headers.
void*libc=sd_got.dlopen("libc.so.6", SD_RTLD_NOW);The loading of the dynamic linker happens before the main is called. For this
reason the library defines its own _start symbol from which the program
execution should start. For this to work, build must include compilation flags:
-nostartfiles -fno-stack-protector.
It is not advisable to link musl in addition to using this library. The
reason for this is that musl (like glibc) needs to be initialized before it
can be properly used. This is usually done by calling __libc_start_main, but
since the program is initialized though the dynamic linker, the musl
initialization is never invoked. But that is not the whole issue. Both musl
and glibc want to set up TLS the way they need it, so calling
__libc_start_main after dynamic linker setup will break glibc, but without
it musl is in a broken state.
Fortunately, some musl functions (like printf) can still work as long as
they do not set errno (since errno is inside TLS block, but glibc and
musl put it at different offsets), or access uninitialized global variables
(for example pthread_create will fail because libc.can_do_threads will not
be set by __init_tls call).
Warning
In general it is better to avoid linking musl and just rely on raw syscalls
or functions obtained from loaded glibc.
There is an example static_dynamic_test.c with build.sh that shows all of this and
builds a simple raylib demo with this functionality
First you need to add _start definition to your root module:
pubconst_start= {};This will tell Zig to not generate the _start symbol since it is already present in static_dynamic.h.
The main function should be defined as:
exportfnmain(argc: u64, argv: [*]const [*:0]constu8) callconv(.c) i32The first thing you would need to call inside main is sd.init which will
perform setup of global variables inside Zig std. More about this down below.
Additionally don't forget to check sd.got.result.success just in case.
exportfnmain(argc: u64, argv: [*]const [*:0]constu8) callconv(.c) i32 {
sd.init(argc, argv);
if (sd.got.result.success==0) return1;
...
}The usage of provided functions is very close to the C version:
constlibc=sd.got.fns.dlopen("libc.so.6", .{ .NOW=true });All you need to do is to add C source file to your root_module:
constsd_c=b.addWriteFiles().add("static_dynamic.c",
\\#include "static_dynamic.h"
);
root_module.addCSourceFile(.{
.file=sd_c,
.flags= &[_][]constu8{ "-fno-stack-protector" },
});
root_module.addIncludePath(b.path("."));Zig needs to have some initialization performed in order for std to work
properly. Usually Zig does it inside its std.start code, but since we skip
it, we need to do it ourselves. The static_dynamic.zig provides an init
function specifically for this purpose.
Note
Even though sd.init makes Zig std functions work, there is a caveat when
it comes to the std.Thread.spawn: it only creates TLS for the Zig usage,
so using libc functions from these threads most likely will crash the
program. Instead it is better to just load pthread_create and use that for
new thread creation. This way new threads will be able to use both std and
libc functions.
Linking with musl does not work. Compiling with link_libc = true and
-Dtarget=x86_64-linux-musl creates a symbol collision since Zig links
crt1.o unconditionally which defines its own _start. Working around this
issue by compiling Zig code to object files and doing external linking will hit
same issues as the C version.
All of the info above is also repeated in build.zig and static_dynamic_test.zig.
- Detour - provided the base idea of how this whole machinery should work