How Does Execve Call Dynamic Linker/Loader (Ld-Linux.So.2)

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?


  1. ld-linux.so* doesn't depends any other libraries. It is runable by itself when loaded to memory.

  2. 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


  1. file reads the magic number or elf header to figure whether the object is dynamically or statically linked, it may output different value from ldd

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 opened) 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 mmapping 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



Leave a reply



Submit