I have been looking to add support for this to pam_cap.so, and found this question. As @EmployedRussian notes in a follow-up to their own post, the accepted answer stopped working at some point. It took a while to figure out how to make this work again, so here is a worked example.
This worked example involves 5 files to show how things work with some corresponding tests.
First, consider this trivial program (call it empty.c):
int main(int argc, char **argv) { return 0; }
Compiling it, we can see how it resolves the dynamic symbols on my system as follows:
$ gcc -o empty empty.c
$ objcopy --dump-section .interp=/dev/stdout empty ; echo
/lib64/ld-linux-x86-64.so.2
$ DL_LOADER=/lib64/ld-linux-x86-64.so.2
That last line sets a shell variable for use later.
Here are the two files that build my example shared library:
/* multi.h */
void multi_main(void);
void multi(const char *caller);
and
/* multi.c */
#include <stdio.h>
#include <stdlib.h>
#include "multi.h"
void multi(const char *caller) {
printf("called from %s\n", caller);
}
#ifdef __GLIBC__
/* magic to make glibc work more reliably. */
extern const int _IO_stdin_used;
const int _IO_stdin_used __attribute__((weak)) = 131073;
#endif
__attribute__((force_align_arg_pointer))
void multi_main(void) {
multi(__FILE__);
exit(42);
}
const char dl_loader[] __attribute__((section(".interp"))) =
DL_LOADER ;
Updates:
2021-11-13: The forced alignment is to help __i386__ code be SSE compatible - without it we get hard to debug glibc SIGSEGV crashes.
2025-03-22: The _IO_stdin_used weak definition works around another hard to debug glibc issue.
We can compile and run it as follows:
$ gcc -fPIC -shared -o multi.so -DDL_LOADER="\"${DL_LOADER}\"" multi.c -Wl,-e,multi_main
$ ./multi.so
called from multi.c
$ echo $?
42
So, this is a .so that can be executed as a stand alone binary. Next, we validate that it can be loaded as shared object.
/* opener.c */
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
typedef void (*mainfn_t)(const char *);
int main(int argc, char **argv) {
void *handle = dlopen("./multi.so", RTLD_NOW);
if (handle == NULL) {
perror("no multi.so load");
exit(1);
}
mainfn_t multi = (mainfn_t) dlsym(handle, "multi");
multi(__FILE__);
}
That is we dynamically load the shared-object and run a function from it:
$ gcc -o opener opener.c -ldl
$ ./opener
called from opener.c
Finally, we link against this shared object:
/* main.c */
#include "multi.h"
int main(int argc, char **argv) {
multi(__FILE__);
}
Where we compile and run it as follows:
$ gcc main.c -o main multi.so
$ LD_LIBRARY_PATH=./ ./main
called from main.c
Note: because multi.so isn't in a standard system library location, we need to override where the runtime looks for the shared object file with the LD_LIBRARY_PATH environment variable.
2025-05-04 For C++ users (at least using g++), as noted by @Haydentech in the comments, trying to compile the above files generates a compile time error:
multi.c:13:11: error: weak declaration of ‘_IO_stdin_used’ must be public
13 | const int _IO_stdin_used __attribute__((weak)) = 131073;
| ^~~~~~~~~~~~~~
This is because of a known bug with the compiler. However, it is not the only problem you will encounter. C++ linking mangles symbols, so the name you give things isn't always the name they are linked with. This leads to problems with the entry point not working. For these cases, you will need the following workarounds. Replace the multi.h file content with this:
/* multi.h - C++ compatible */
#ifdef __cplusplus
extern "C" {
#endif
void multi_main(void);
void multi(const char *caller);
#ifdef __cplusplus
}
#endif
and replace multi.c file content with this:
/* multi.c - C++ compatible */
#include <stdio.h>
#include <stdlib.h>
#include "multi.h"
void multi(const char *caller) {
printf("called from %s\n", caller);
}
#ifdef __GLIBC__
extern const int _IO_stdin_used;
#ifdef __cplusplus
/* Workaround for https://gcc.gnu.org/bugzilla/show_bug.cgi?id=83271 */
extern
#endif /* def __cplusplus */
const int _IO_stdin_used __attribute__((weak)) = 131073;
#endif /* def __GLIBC__ */
#ifdef __cplusplus
extern "C" {
#endif /* def __cplusplus */
__attribute__((force_align_arg_pointer))
void multi_main(void) {
multi(__FILE__);
exit(42);
}
#ifdef __cplusplus
}
#endif /* def __cplusplus */
const char dl_loader[] __attribute__((section(".interp"))) =
DL_LOADER ;
These changes should still permit the files to compile with gcc but they make the source code even harder to read, so I've opted to explain them as a postscript.
/lib/ld-linux.so.2is just another example :)