Saturday, 15 August 2026

Linux Dynamic Linking

A few months ago I wrote a post about glibc, it's a good time now to follow up with a related element of linux systems, ld-linux.so, the dynamic linker or dynamic loader.

If you need a refresh on what linking is, this wikipedia article should be enough. In this post I'll be talking about dynamic linking on Linux (performed by ld-linux.so), do not confuse it with the static linking performed by the ld tool (part of the binutils package, in turn part of the build-essential metapackage) to create an executable copying inside it the library code it references.

So dynamic linking means loading into a process memory at runtime the libraries (shared objects, .so's) that it needs. This loading is mainly done when the process starts (based on the imports defined in the ELF binary file), but can also be performed dynamically at any point during the process execution, with the dlopen function. In both cases, ld-linux.so (at least in Ubuntu its exact name is: /lib64/ld-linux-x86-64.so.2) is used to perform this loading and linking.

If ld-linux.so is used to load so's, and ld-linux is a so itself, this looks like a chicken-egg problem. Well, it's the kernel itself who takes care of loading it when a process is launched:

A binary names its loader in the ELF .interp section (readelf -p .interp /bin/ls → /lib64/ld-linux-x86-64.so.2). On execve the kernel reads .interp, hands control to that interpreter, which maps the libraries and jumps into the program.

Notice that the dynamic linker/loader is also known as the ELF interpreter.

When you run a dynamically linked program, the Linux kernel reads the executable's Executable and Linkable Format (ELF) header. It looks for a specific section called .interp (or the PT_INTERP program header), which contains the hardcoded string path to this exact interpreter. How the ELF Interpreter Works: Rather than running your program directly, the operating system kernel actually loads and hands control over to ld-linux first. The interpreter then performs several crucial tasks:
- Finds Dependencies: It scans your binary to see which shared libraries (such as libc.so) it needs.
- Loads Libraries: It locates those .so files on the disk and maps them into the program's memory space.
- Resolves Symbols: It performs "relocations," fixing memory references so your program knows exactly where library functions exist in memory.
- Launches Program: Once the environment is ready, it hands control back to your program's main entry point.

There's a strong relation between glibc and ld-linux. They are built together carrying the same version:

The loader (ld.so) and libc.so.6 are a version-locked matched pair. They are built from the same source tree in the same build and talk to each other over a private, unstable ABI: the GLIBC_PRIVATE symbols.
nm -D /lib64/ld-linux-x86-64.so.2 | grep GLIBC_PRIVATE # loader DEFINES them (T/D)nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC_PRIVATE # libc has them UNDEFINED (U)

libc.so.6 lists symbols like _dl_allocate_tls@GLIBC_PRIVATE, __tunable_get_val@GLIBC_PRIVATE, _dl_find_dso_for_object@GLIBC_PRIVATE as undefined — “the loader must hand these to me at startup.” The names, semantics, and the layouts behind them (TCB/TLS, _rtld_global) change freely between glibc versions.

ld-linux.so can be executed on its own, as a normal executable. If you run it with the --help version (/lib64/ld-linux-x86-64.so.2 --help) you get this interesting information.

You have invoked 'ld.so', the program interpreter for dynamically-linked ELF programs. Usually, the program interpreter is invoked automatically when a dynamically-linked executable is started.

You may invoke the program interpreter program directly from the command line to load and run an ELF executable file; this is like executing that file itself, but always uses the program interpreter you invoked, instead of the program interpreter specified in the executable file you run. Invoking the program interpreter directly provides access to additional diagnostics, and changing the dynamic linker behavior without setting environment variables (which would be inherited by subprocesses).

libc.so also happens to have an entry point, but it seems to have no other function that showing some information about itself: (/lib/x86_64-linux-gnu/libc.so.6 --version)

GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.8) stable release version 2.39.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 13.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0

Invoking ld-linux.so with --version in the same system you'll see that it's the same version as glibc, /lib64/ld-linux-x86-64.so.2 --version

ld.so (Ubuntu GLIBC 2.39-0ubuntu8.8) stable release version 2.39.

Notice that in the glibc information you can see "Minimum supported kernel: 3.2.0". Bearing in mind that my current kernel is 6.8.0, so we can say that glibc is quite little demanding with regards to kernel evolution.

Probably the ldd tool resonates with you. It's just a bash script wrapper (/usr/bin/ldd) around ld-linux.so

No comments:

Post a Comment