12

I need to dynamically open a shared library lib.so if a specific condition is met at runtime. The library contains ~700 functions and I need to load all their symbols.

A simple solution is to define the function pointers to all symbols contained in lib.so, load the library using dlopen and finally get the addresses of all symbols using dlsym. However, given the number of functions, the code implementing this solution is very cumbersome.

I was wondering if a more elegant and concise solution exists, maybe with an appropriate use of macros for defining the function pointers. Thanks!

yugr
  • 19,769
  • 3
  • 51
  • 96
mdag
  • 129
  • 1
  • 4
  • 1
    If you need to call 700 functions from a shared library why not just have one function in the shared library that calls those functions for you? Such a list would be easier to maintain inside the library rather than in macros outside. – Art Aug 28 '17 at 11:35
  • Actually the application is a profiler which intercepts the calls of another library `lib_ext.so` and replace them with its profiled version defined in `lib.so`. The symbols of `lib_ext.so` are overloaded using the LD_PRELOAD variable. Since I do not know in advance which functions of `lib_ext.so` will be called at runtime I need the addresses of all symbols. – mdag Aug 28 '17 at 11:50
  • @n.m. You are right about `dlsym`. I changed the text of the question. – mdag Aug 28 '17 at 11:52
  • 1
    It's still unclear why you would need all the addresses. – n. m. could be an AI Aug 28 '17 at 11:58
  • @n.m. Because I do not know in advance which functions in `lib.so` will be called and if there is one address missing then the corresponding function will not be correctly profiled. – mdag Aug 28 '17 at 12:10
  • I'd propose to write a tool creating the relevant C code for you, that is defining all the correctly typed function pointers along with all the calls to `dlopen()`. – alk Aug 28 '17 at 12:18
  • So you have some kind of a profiler and you want to add each function address to the profiler before running any code in the library, is this correct? Does it require to register all functions you want to profile ahead of time? If so, you have a vey unusual and hostile profiler. Perhaps you misunderstand what it does? – n. m. could be an AI Aug 28 '17 at 12:34
  • @n.m. It is a profiler for a MPI application. It does not instrument the code and it profiles only a known set of functions (those defined by the MPI API). Therefore I guess I really need to register all function addresses. As you say I might have misunderstood anyway. – mdag Aug 28 '17 at 13:24
  • @alk I thought about that too. If I did not find any other solution I will write the external generator as you propose. Thanks! – mdag Aug 28 '17 at 13:25
  • If you're intercepting function calls, you need more than just the address of the function. How do you handle arguments? What about return values? – Andrew Henle Aug 28 '17 at 13:32
  • @AndrewHenle The arguments and return values of the functions are given by the MPI API so I already know them. – mdag Aug 28 '17 at 23:04
  • 1
    Please **edit your question** to improve it and explain why do you ask and how to you use `dlopen` and what are your program and its plugin doing. Add a few sentences on motivation and context. – Basile Starynkevitch Nov 10 '17 at 12:05

3 Answers3

14

You could automatically generate trampoline functions for all symbols in dlopen-ed library. Trampolines would be seen as normal functions in application but would internally redirect to real code in library. Here's a simple 5-minute PoC:

$ cat lib.h
// Dynamic library header
#ifndef LIB_H
#define LIB_H
extern void foo(int);
extern void bar(int);
extern void baz(int);
#endif

$ cat lib.c
// Dynamic library implementation
#include <stdio.h>

void foo(int x) {
  printf("Called library foo: %d\n", x);
}

void bar(int x) {
  printf("Called library baz: %d\n", x);
}

void baz(int x) {
  printf("Called library baz: %d\n", x);
}

$ cat main.c
// Main application
#include <dlfcn.h>
#include <stdio.h>

#include <lib.h>

// Should be autogenerated
void *fptrs[100];
void init_trampoline_table() {
  void *h = dlopen("./lib.so", RTLD_LAZY);
  fptrs[0] = dlsym(h, "foo");
  fptrs[1] = dlsym(h, "bar");
  fptrs[2] = dlsym(h, "baz");
}

int main() {
  init_trampoline_table();
  printf("Calling wrappers\n");
  foo(123);
  bar(456);
  baz(789);
  printf("Returned from wrappers\n");
  return 0;
}

$ cat trampolines.S
  // Trampoline code.
  // Should be autogenerated. Each wrapper gets its own index in table.
  // TODO: abort if table wasn't initialized.

  .text

  .globl foo
foo:
  jmp *fptrs(%rip)

  .globl bar
bar:
  jmp *fptrs+8(%rip)

  .globl baz
baz:
  jmp *fptrs+16(%rip)

$ gcc -fPIC -shared -O2 lib.c -o lib.so
$ gcc -I. -O2 main.c trampolines.S -ldl
$ ./a.out
Calling wrappers
Called library foo: 123
Called library baz: 456
Called library baz: 789
Returned from wrappers

Note that application code in main.c is using only local functions (which wrap library functions) and does not have to mess with function pointers at all (apart from initializing redirection table at startup which should be autogenerated code anyway).

EDIT: I've created a standalone tool Implib.so to automate creation of stub libraries like in example above. This turned out to be more or less equivalent to well known Windows DLL import libraries.

yugr
  • 19,769
  • 3
  • 51
  • 96
  • I don't understand how your solution could work with a library open with dlopen or you sure of your solution ? – Stargateur Nov 10 '17 at 11:46
  • 1
    @Stargateur I originally wrote PoC which didn't use shlibs to illustrate the idea but your comment motivated me to rewrite it, hopefully my intent is more clear now. Note that I hope to do mini-project soon to have a fully working solution and I'll post a link here. – yugr Nov 10 '17 at 12:16
  • 2
    @Stargateur I've added link to generic tool I promised. – yugr Nov 27 '17 at 09:24
  • 1
    The solutions looks amazing but I got error when trying: `gcc -fPIC -shared -O2 lib.c -o lib.so` `gcc -I. -O2 main.c trampolines.S -ldl` Error: `/usr/bin/ld: /tmp/ccfZfE32.o: relocation R_X86_64_32S against symbol 'fptrs' can not be used when making a PIE object; recompile with -fPIE` – Rick Jul 09 '22 at 16:06
  • @Rick thank you for noticing this. Modern distros compile executables with -fPIE flag by default which required some changes in assembly code in trampolines.S. I've updated the answer. – yugr Jul 10 '22 at 02:23
  • It works now. It's like magic. What kind of CS knowledge should one learn in order to understand and write a `.S` file ? x86_64 assembly? – Rick Jul 10 '22 at 02:49
  • @rick yes, the assembly here is simple so assembly primer like [this one](https://gist.github.com/AVGP/85037b51856dc7ebc0127a63d6a601fa) should be enough. – yugr Jul 10 '22 at 08:03
  • @Rick note that there is now a [tool](https://github.com/yugr/Implib.so) for generating the wrappers which hides all the details. – yugr Jul 10 '22 at 08:32
9

I need to dynamically open a shared library lib.so if a specific condition is met at runtime. The library contains ~700 functions and I need to load all their symbols.

role of dlopen and dlsym

When you dlopen a library, all the functions defined by that library becomes available in your virtual address space (because all the code segment of that library is added into your virtual address space by dlopen calling mmap(2) several times). So dlsym don't add (or load) any additional code, it is already there. If your program is running in the process of pid 1234, try cat /proc/1234/maps after the successful dlopen.

What dlsym provides is the ability to get the address of something in that shared library from its name, using some dynamic symbol table in that ELF plugin. If you don't need that, you don't need to call dlsym.

Perhaps you could simply have, in your shared library, a large array of all the relevant functions (available as a global variable in your shared library). Then you'll just need to call dlsym once, for the name of that global variable.

BTW, a constructor (constructor is a function attribute) function of your plugin could instead "register" some functions of that plugin (into some global data structure of your main program; this is how Ocaml dynamic linking works); so it even makes sense to never call dlsym and still be able to use the functions of your plugin.

For a plugin, its constructor functions are called at dlopen time (before the dlopen returns!) and its destructor functions are called at dlclose time (before dlclose returns).

repeating calls to dlsym

It is common practice to use dlsym many times. Your main program would declare several variables (or other data e.g. fields in some struct, array components, etc...) and fill these with dlsym. Calling dlsym a few hundred times is really quick. For example you could declare some global variables

void*p_func_a;
void*p_func_b;

(you'll often declare these as pointers to functions of appropriate, and perhaps different, types; perhaps use typedef to declare signatures)

and you'll load your plugin with

void*plh = dlopen("/usr/lib/myapp/myplugin.so", RTLD_NOW);
if (!plh) { fprintf(stderr, "dlopen failure %s\n", dlerror()); 
            exit(EXIT_FAILURE); };

then you'll fetch function pointers with

p_func_a = dlsym(plh, "func_a");
if (!p_func_a) { fprintf(stderr, "dlsym func_a failure %s\n", dlerror());
                 exit(EXIT_FAILURE); };
p_func_b = dlsym(plh, "func_b");
if (!p_func_b) { fprintf(stderr, "dlsym func_b failure %s\n", dlerror());
                 exit(EXIT_FAILURE); };

(of course you could use preprocessor macros to shorten such repetitive code; X-macro tricks are handy.)

Don't be shy in calling dlsym hundreds of times. It is however important to define and document appropriate conventions regarding your plugin (e.g. explain that every plugin should define func_a and func_b and when are they called by your main program (using p_func_a etc... there). If your conventions require hundreds of different names, it is a bad smell.

agglomerating plugin functions into a data structure

So assume your library defines func_a, func_b, func_c1, ... func_c99 etc etc you might have a global array (POSIX allows casting functions into void* but the C11 standard does not allow that):

const void* globalarray[] = {
  (void*)func_a,
  (void*)func_b,
  (void*)func_c1,
  /// etc
  (void*)func_c99,
  /// etc
  NULL /* final sentinel value */
};

and then you'll need to dlsym only one symbol: globalarray; I don't know if you need or want that. Of course you could use more fancy data structures (e.g. mimicking vtables or operation tables).


using a constructor function in your plugin

With the constructor approach, and supposing your main program provides some register_plugin_function which do appropriate things (e.g. put the pointer in some global hash table, etc...), we would have in the plugin code a function declared as

static void my_plugin_starter(void) __attribute__((constructor));
void my_plugin_starter(void) {
  register_plugin_function ("func", 0, (void*)func_a);
  register_plugin_function ("func", 1, (void*)func_b);
  /// etc...
  register_plugin_function ("func", -1, (void*)func_c1);
  /// etc...
};

and with such a constructor the func_a etc... could be static or with restricted visibility. We then don't need any call to dlsym from the main program (which should provide the register_plugin_function function) loading the plugin.


references

Read more carefully dynamic loading and plug-ins and linker wikipages. Read Levine's Linkers and Loaders book. Read elf(5), proc(5), ld-linux(8), dlopen(3), dlsym(3), dladdr(3). Play with objdump(1), nm(1), readelf(1).

Of course read Drepper's How To Write Shared Libraries paper.

BTW, you can call dlopen then dlsym a big lot of times. My manydl.c program is generating "random" C code, compiling it as a plugin, then dlopen-ing and dlsym-ing it, and repeats. It demonstrates that (with patience) you can have millions of plugins dlopen-ed in the same process, and you can call dlsym a lot of times.

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • On POSIX it is explicitly not UB, and `dlsym` is POSIX specific. – Basile Starynkevitch Nov 10 '17 at 11:48
  • "If you don't need that, you don't need to call dlsym" - but how would he access functions then? Note that explicitly referencing a function name in code is not possible because it'll cause linker errors (symbol is only available at runtime). – yugr Nov 10 '17 at 12:01
  • As I told, the plugin could register some function pointers in some constructor function – Basile Starynkevitch Nov 10 '17 at 12:02
  • "As I told, the plugin could register some function pointers in some constructor function" - isn't OP asking for solution which does not use function pointers? – yugr Nov 10 '17 at 12:03
  • Have you exactly understood what the OP wants? I did not yet (and I believe OP is very confused). I only made educated guess. I'm sad that so many questions stay unmotivated and without context. – Basile Starynkevitch Nov 10 '17 at 12:04
  • See also https://meta.stackoverflow.com/q/358255/841108 which explains my sadness. This question is another example... But my opinion (about insufficient motivations) is in the minority. – Basile Starynkevitch Nov 10 '17 at 12:09
  • Thanks, I see your point now. Our answers seem to stem from different understanding of the question then. – yugr Nov 10 '17 at 12:18
  • @yugr: at least we both agree that the OP's question is poorly asked and lacks motivation and context. I voted to close that question because it is unclear. Perhaps the OP would improve it (but I am not optimistic). – Basile Starynkevitch Nov 10 '17 at 12:22
  • 1
    Yes. But it's still an interesting question. – yugr Nov 10 '17 at 12:28
  • We disagree. I find the OP's question boring, because we need to guess his intents and try to read in his mind. For me it is not an interesting question, just a poorly asked and too broad one! – Basile Starynkevitch Nov 10 '17 at 12:30
-1

Refer to difference between dynamic loading and dynamic linking? , you may need dynamic linking instead of dynamic loading a shared library.

  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Shawn Aug 17 '23 at 05:33