strace outputs, the paths to the libraries that executables call are in calls to
open(). Is this the system call used by executables that are dynamically linked? What about
open() isn't a call I'd have guessed would play a role in the execution of programs.
dlopen isn't a system call, it's a library function in the libdl library. Only system calls show up in
On Linux and on many other platforms (especially those that use the ELF format for executables),
dlopen is implemented by opening the target library with
open() and mapping it into memory with
mmap() is really the critical part here, it's what incorporates the library into the process' address space, so the CPU can execute its code. But you have to
open() the file before you can
dlopen has nothing to do with shared libraries as you think of them. There are two methods of loading a shared object:
- You tell the compile-time linker (ld, though usually it's called through the compiler) that you want to use functions from a particular shared library. With this approach, you must know what the name of the library will be at when the compile-time linker is run, but you can call the library's functions as if they were statically linked into your program. When the application is run, the dynamic, run-time linker (ld.so) will be called just before the
mainfunction is called, and set up the application's process space so that the application will find the functions of the library. This involves
open()ing the lubrary, and then
mmap()ing it, followed by setting up some lookup tables.
- You tell the compile-time linker that you want to link with
libdl, from which you then (using the first method) can call the
dlsym()functions. With dlopen you get a handle to the library, which you can then use with dlsym to receive a function pointer to a particular function. This method is much more complicated for the programmer than the first method (since you have to do the setup manually, rather than have the linker do it automatically for you), and it also is more fragile (since you don't get the compile-time checks that you're calling functions with the correct argument types as you get in the first method), but the advantage is that you can decide which shared object to load at runtime (or even whether to load it at all), making this an interface meant for plugin type functionality. Finally, the dlopen interface is also less portable than the other way, since its mechanics depend on the exact implementation of the dynamic linker (hence libtool's
libltdl, which tries to abstract away these differences).
Today, most operating systems use the method for shared libraries introduced in late 1987 by SunOS-4.0. This method is based on mapping memory via mmap().
Given the fact that in the early 1990s, Sun did even donate the old a.out based code (Solaris at that time was already ELF based) to the FreeBSD people and that this code later was handed over to many other systems (including Linux), you may understand why there is no big difference between platforms.
ltrace -S analysis of a minimal example shows that
mmap is used in glibc 2.23
In glibc 2.23, Ubuntu 16.04, running
latrace -S on a minimal program that uses
ltrace -S ./dlopen.out
dlopen("libcirosantilli_ab.so", 1 <unfinished ...> SYS_open("./x86_64/libcirosantilli_ab.so", 524288, 06267650550) = -2 SYS_open("./libcirosantilli_ab.so", 524288, 06267650550) = 3 SYS_read(3, "\177ELF\002\001\001", 832) = 832 SYS_brk(0) = 0x244c000 SYS_brk(0x246d000) = 0x246d000 SYS_fstat(3, 0x7fff42f9ce30) = 0 SYS_getcwd("/home/ciro/bak/git/cpp-cheat"..., 128) = 54 SYS_mmap(0, 0x201028, 5, 2050) = 0x7f1c323fe000 SYS_mprotect(0x7f1c323ff000, 2093056, 0) = 0 SYS_mmap(0x7f1c325fe000, 8192, 3, 2066) = 0x7f1c325fe000 SYS_close(3) = 0 SYS_mprotect(0x7f1c325fe000, 4096, 1) = 0
so we see immediately that
ltrace tool traces both library calls and system calls, and is therefore perfect to examine what is going on in this case.
A closer analysis shows that
open returns the file descriptor
3 (next free one after stdin, out and err).
read then uses that file descriptor, but TODO why
mmap's arguments are limited to four, and we can't see which fd was used there since that is the 5th argument.
strace confirms as expected that
3 is the one, and the order of the universe is restored.
Brave souls can also venture into glibc code, but I could not find the
mmap after a quick grep and I'm lazy.
Tested with this minimal example with build boilerplate on GitHub.