How does the dynamic linker executes /proc/self/exe
As you pointed out, the kernel is not passing the executed binary as a path to the interpreter:
$ /lib64/ld-linux-x86-64.so.2 /proc/self/exe
loader cannot load itself
Although the glibc dynamic linker supports this invocation method (providing the program to run as an argument), it's not what's used during the normal execution of an interpreted ELF binary. In fact, the kernel provides the arguments from the execve unmodified to the dynamic linker.
The dynamic linker doesn't "load" or "execute" the interpreted ELF binary at all. The kernel loads both the interpreter and the interpreted binary into memory and begins execution at the entry point of the interpreter. The entry point of the interpreted binary is passed to the interpreter via the AT_ENTRY
field in the auxiliary vector.
The dynamic linker then preforms the necessary runtime linking and jumps to the "real" entry point.
You can observe this all in gdb if you set a break point on _start when executing a normal interpreted ELF executable. With "show args" you'll see the "real" argv without any extra values, and the memory map of the process will already have the interpreted binary loaded (before the interpreter has run a single instruction).
#!
scripts work the way you expect (actually manipulating the argv values).
how is ld-linux.so* itself linked and loaded?
ld-linux.so*
doesn't depends any other libraries. It is runable by itself when loaded to memory.ldd
is a script, it loads the object file via the loader, and the loader checks whether the object is dynamically or statically linked, try this:
LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
file
reads the magic number or elf header to figure whether the object is dynamically or statically linked, it may output different value fromldd
IMO, ld-linux.so
is static linked, because it doesn't have an .interp
section which all dynamically linked object must have.
How does dynamic linker changes text segment of process?
The actual implementation is quite complex as it builds on top of ELF, which is quite complex as it tries to accommodate many scenarios, but conceptually it's quite simple.
Basically (after the library dependencies are located and open
ed) it's a couple of mmaps, mprotects, some modifications to implement the linking by binding symbols (can be deferred), and then jump to code.
Ideally, the linked shared libraries will be compiled with -fpic
/-fPIC
which will allow the linker to place them anywhere in the processes address space without having to write to the .text
section (=executable code) of the library.
Such a library/executable will call functions from other libraries via a modifiable table, which the linker will fix up (probably lazily) to point to the actual locations where it has loaded the dependent library.
Access to variables from one shared library to another is similarly indirected.
Limiting the modifying library data/code as much as possible allows marking sections of code to be marked read only (via the MMU / the mprotect
system call) and mapped into memory that's shared among all processes that use that particular library.
To get an idea of what happens at the syscall level, you can try e.g.:
strace /bin/echo hello world
and all the syscalls up to about sbrk
included (=setting up the heap / .data
segment) should be the doings of the dynamic linker.
(malloc
is indeed unavailable to the linker as malloc
is a feature of the c library, not the system. malloc
is about growing and managing the heap section and potentially mmap
ping other separate blocks and managing those as well as the writable "heap", and the dynamic linker isn't concerned about these sections of a process image, mainly just its writable indirection tables and where it maps libraries).
Is Dynamic Linker part of Kernel or GCC Library on Linux Systems?
In an ELF executable, this is referred to as the "ELF interpreter". On linux (e.g.) this is /lib64/ld-linux-x86-64.so.2
This is not part of the kernel and [generally] with glibc
et. al.
When the kernel executes an ELF executable, it must map the executable into userspace memory. It then looks inside for a special sub-section known as INTERP
[which contains a string that is the full path].
The kernel then maps the interpreter into userspace memory and transfers control to it. Then, the interpreter does the necessary linking/loading and starts the program.
Because ELF
stands for "extensible linker format", this allows many different sub-sections with the ELF file.
Rather than burdening the kernel with having to know about all the myriad of extensions, the ELF interpreter that is paired with the file knows.
Although usually only one format is used on a given system, there can be several different variants of ELF files on a system, each with its own ELF interpreter.
This would allow [say] a BSD ELF file to be run on a linux system [with other adjustments/support] because the ELF file would point to the BSD ELF interpreter rather than the linux one.
UPDATE:
every process(vlc player, chrome) had the shared library ld.so as part of their address space.
Yes. I assume you're looking at /proc/<pid>/maps
. These are mappings (e.g. like using mmap
) to the files. That is somewhat different than "loading", which can imply [symbol] linking.
So primarily loader after loading the executable(code & data) onto memory , It loads& maps dynamic linker (.so) to its address space
The best way to understand this is to rephrase what you just said:
So primarily the kernel after mapping the executable(code & data) onto memory, the kernel maps dynamic linker (.so) to the program address space
That is essentially correct. The kernel also maps other things, such as the bss
segment and the stack. It then "pushes" argc
, argv
, and envp
[the space for environment variables] onto the stack.
Then, having determined the start address of ld.so
[by reading a special section of the file], it sets that as the resume address and starts the thread.
Up until now, it has been the kernel doing things. The kernel does little to no symbol linking.
Now, ld.so
takes over ...
which further Loads shared Libraries , map & resolve references to libraries. It then calls entry function (_start)
Because the original executable (e.g. vlc
) has been mapped into memory, ld.so
can examine it for the list of shared libraries that it needs. It maps these into memory, but does not necessarily link the symbols right away.
Mapping is easy and quick--just an mmap
call.
The start address of the executable [not to be confused with the start address of ld.so
], is taken from a special section of the ELF executable. Although, the symbol associated with this start address has been traditionally called _start
, it could actually be named anything (e.g. __my_start
) as it is what is in the section data that determines the start address and not address of the symbol _start
Linking symbol references to symbol definitions is a time consuming process. So, this is deferred until the symbol is actually used. That is, if a program has references to printf
, the linker doesn't actually try to link in printf
until the first time the program actually calls printf
This is sometimes called "link-on-demand" or "on-demand-linking". See my answer here: Which segments are affected by a copy-on-write? for a more detailed explanation of that and what actually happens when an executable is mapped into userspace.
If you're interested, you could do ldd /usr/bin/vlc
to get a list of the shared libraries it uses. If you looked at the output of readelf -a /usr/bin/vlc
, you'll see these same shared libraries. Also, you'd get the full path of the ELF interpreter and could do readelf -a <full_path_to_interpreter>
and note some of the differences. You could repeat the process for any .so
files that vlc
wanted.
Combining all that with /proc/<pid>maps
et. al. might help with your understanding.
How can I verify what dynamic linker is used when a program is run?
There is no need to actually run the executable to determine the ELF interpreter that it will use.
We can use static tools and be guaranteed that we can get the full path.
We can use a combination of readelf
and ldd
.
If we use readelf -a
, we can parse the output.
One part of the readelf
output:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000000002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
Note the address of the .interp
section. It is 0x2e0
.
If we open the executable and do a seek to that offset, we can read the ELF interpreter string. For example, here is [what I'll call] fileBad
:
000002e0: 2F6C6962 36342F7A 642D6C69 6E75782D /lib64/zd-linux-
000002f0: 7838362D 36342E73 6F2E3200 00000000 x86-64.so.2.....
Note that the string seems a little odd ... More on that later ...
Under the "Program Headers:" section, we have:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/zd-linux-x86-64.so.2]
Again, note the 0x2e0
file offset. This may be an easier way to get the path to the ELF interpreter.
Now we have the full path to the ELF interpreter.
We can now do ldd /path/to/executable
and we'll get a list of the shared libraries it is/will be using. We'll do this for fileGood
. Normally, this looks like [redacted]:
linux-vdso.so.1 (0x00007ffc96d43000)
libpython3.7m.so.1.0 => /lib64/libpython3.7m.so.1.0 (0x00007f36d1ee2000)
...
libc.so.6 => /lib64/libc.so.6 (0x00007f36d1ac7000)
/lib64/ld-linux-x86-64.so.2 (0x00007f36d23ff000)
...
That's for a normal executable. Here's the ldd
output for fileBad
:
linux-vdso.so.1 (0x00007ffc96d43000)
libpython3.7m.so.1.0 => /lib64/libpython3.7m.so.1.0 (0x00007f36d1ee2000)
...
libc.so.6 => /lib64/libc.so.6 (0x00007f36d1ac7000)
/lib64/zd-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f3f4f821000)
...
Okay, to explain ...
fileGood
is a standard executable [/bin/vi
on my system]. However, fileBad
is a copy that I made where I patched the interpreter path to a non-existent file.
From the readelf
data, we know the interpreter path. We can check for existence of that file. If it doesn't exist things are [obviously] bad.
With the interpreter path we got from readelf
, we can find the output line from ldd
for the interpreter.
For the good file, ldd
gave us the simple interpreter resolution:
/lib64/ld-linux-x86-64.so.2 (0x00007f36d23ff000)
For the bad file, ldd
gave us:
/lib64/zd-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f3f4f821000)
So, either ldd
or the kernel detected the missing interpreter and substituted the default one.
If we try to exec fileBad
from the shell we get:
fileBad: Command not found
If we try to exec fileBad
from a C program we get an ENOENT
error:
No such file or directory
From this we know that the kernel did not try to use a "default" interpreter when we did an exec*
syscall.
So, we now know that the static analysis we did to determine the ELF interpreter path is valid.
We can be assured that the path we came up with is [will be] the path to the ELF interpreter that the kernel will map into the process address space.
For further assurance, if you need to, download the kernel source code. Look in the file: fs/binfmt_elf.c
I think that's sufficient, but to answer the question in your top comment
with that solution would I not have to race to read
/proc/<pid>/maps
before the program terminates?
There's no need to race.
We can control the fork
process. We can set up the child to run under [the syscall] ptrace
, so we can control its execution (Note that ptrace
is what gdb
and strace
use).
After we fork
, but before we exec
, the child can request that the target of the exec
sleep until a process attaches to it via ptrace
.
So, the parent can examine /proc/pid/maps
[or whatever else] before the target executable has executed a single instruction. It can control execution via ptrace
[and, eventually, detach to allow the target to run normally].
Is there a way to predict what PID will be generated next and then wait on its creation in
/proc
?
Given the answer to the first part of your question, this is a bit of a moot point.
There is no way to [accurately] predict the pid
of a process we fork
. If we could determine the pid
that the system would use next, there is no guarantee that we will win the race against another process doing a fork
[before us] and "getting" the pid
we "thought" would be ours.
Program Loader and Runtime linker are the same?
Program Loader and Runtime linker are the same in linux?
Yes, they are. This is also true for every other ELF platform.
Related Topics
How to Disable or Change the Timeout Limit for the Gpu Under Linux
What Does "|" Mean in a Terminal Command Line
What Is Terminal Escape Sequence for Ctrl + Arrow (Left, Right,...) in Term=Linux
How to Translate Linux Keycodes from /Dev/Input/Event* to Ascii in Perl
"Cannot Execute Binary File" When Trying to Run a Shell Script on Linux
How to Write Conditional Import Statements in Qml
Difference Between Number in the Same Column Using Awk
Chmod 777 to a Folder and All Contents
How to Get the Interface Name/Index Associated with a Tcp Socket
Differencebetween Ldd and Objdump
Is /Dev/Random Considered Truly Random
Hardware Acceleration Without X
Fork() Failing with Out of Memory Error