ld.so(8)

๐Ÿ’– ld.so โ€” the dynamic linker/loader ๐Ÿ’–

ok so bestie, welcome to ld.so (and its glow-up twin ld-linux.so) โ€” the literal main character of the Linux manual pages. this is ld.so(8) from the System Manager's Manual, and she's the one who finds and loads your shared libraries. no cap.


๐ŸŒธ NAME ๐ŸŒธ

ld.so, ld-linux.so โ€” dynamic linker/loader ๐Ÿ’…


โœจ SYNOPSIS โœจ

ok so there are two ways to run her:

Way 1 โ€” the indirect one. You just run some dynamically linked program or shared object, and the dynamic linker gets pulled in automatically. when that happens you can't pass any command-line options to her, and (in the ELF case) the dynamic linker that's stored in the .interp section of the program is the one that runs. she's just there, doing her job, unseen. periodt.

Way 2 โ€” the direct one. You run her yourself, like the icon she is:

/lib/ld-linux.so.*  [OPTIONS] [PROGRAM [ARGUMENTS]]

๐Ÿฆ‹ DESCRIPTION ๐Ÿฆ‹

So what does she actually do? The programs ld.so and ld-linux.so* find and load the shared objects (shared libraries) that a program needs, get the program all prepped and ready to run, and then run it. that's it, that's the job. ๐Ÿ‘‘

Okay, quick history lesson because it's giving โœจloreโœจ. Linux binaries need dynamic linking (which is linking at run time) unless you passed the -static option to ld(1) while compiling.

  • ld.so handles a.out binaries โ€” that's a binary format from way, way back. lowkey ancient.
  • ld-linux.so* handles binaries in the much more modern ELF format โ€” that's /lib/ld-linux.so.1 for libc5, and /lib/ld-linux.so.2 for glibc2.

Both of these programs behave the same and use the same support files and programs (ldd(1), ldconfig(8), and /etc/ld.so.conf). same vibe, different era. ๐Ÿ’–

๐Ÿ”Ž How she finds your shared objects ๐Ÿ”Ž

So when she's resolving shared object dependencies, she first peeks at each dependency string to check if it has a slash in it. (this can totally happen if a shared object pathname containing slashes was specified at link time.) If she finds a slash, she reads that dependency string as a relative or absolute pathname and loads the shared object using that pathname. done. easy.

But if the dependency doesn't have a slash? Then she goes on a whole little quest, in this exact order:

  1. Using the directories specified in the DT_RPATH dynamic section attribute of the binary, if it's present and the DT_RUNPATH attribute does not exist.
  2. Using the environment variable LD_LIBRARY_PATH โ€” unless the executable is being run in secure-execution mode (see below), in which case this variable gets completely ignored. ๐Ÿšซ
  3. Using the directories specified in the DT_RUNPATH dynamic section attribute of the binary, if present. but heads up: those directories are only searched to find the objects required by DT_NEEDED (direct dependencies) entries, and they do not apply to those objects' children โ€” the children have to have their own DT_RUNPATH entries. this is unlike DT_RPATH, which applies to searches for all children in the dependency tree. not the same, bestie, do not conflate. ๐Ÿ’€
  4. From the cache file /etc/ld.so.cache, which holds a compiled list of candidate shared objects previously found in the augmented library path. BUT โ€” if the binary was linked with the -z nodefaultlib linker option, shared objects in the default paths get skipped. also, shared objects installed in hardware capability directories (see below) are preferred over other shared objects. she plays favorites and that's her right.
  5. In the default path /lib, and then /usr/lib. (on some 64-bit architectures, the default paths for 64-bit shared objects are /lib64, and then /usr/lib64.) If the binary was linked with -z nodefaultlib, this step is skipped entirely. bye.

๐ŸŽ€ Dynamic string tokens ๐ŸŽ€

In several places, the dynamic linker expands dynamic string tokens:

  • In the environment variables LD_LIBRARY_PATH, LD_PRELOAD, and LD_AUDIT,
  • inside the values of the dynamic section tags DT_NEEDED, DT_RPATH, DT_RUNPATH, DT_AUDIT, and DT_DEPAUDIT of ELF binaries,
  • in the arguments to the ld.so command line options --audit, --library-path, and --preload (see below), and
  • in the filename arguments to the dlopen(3) and dlmopen(3) functions.

And here's the token cheat sheet, bestie:

  • $ORIGIN (or equivalently ${ORIGIN}) โ€” this expands to the directory containing the program or shared object. so an application sitting in somedir/app could be compiled with
gcc -Wl,-rpath,'$ORIGIN/../lib'

and then it'll find an associated shared object in somedir/lib no matter where somedir lives in the directory hierarchy. this is the whole reason "turn-key" applications can exist โ€” apps that don't need to be installed into special directories, and can instead be unpacked into any directory and still find their own shared objects. iconic behavior honestly. ๐Ÿซถ

  • $LIB (or equivalently ${LIB}) โ€” this expands to lib or lib64 depending on the architecture (e.g., on x86-64 it expands to lib64, and on x86-32 it expands to lib).
  • $PLATFORM (or equivalently ${PLATFORM}) โ€” this expands to a string corresponding to the processor type of the host system (e.g., "x86_64"). on some architectures the Linux kernel doesn't hand a platform string to the dynamic linker at all, so like... it just isn't there. the value of this string is taken from the AT_PLATFORM value in the auxiliary vector (see getauxval(3)).
โš ๏ธ okay important: the dynamic string tokens have to be quoted properly when you set them from a shell, so they don't get expanded as shell or environment variables instead. your shell will absolutely snatch them otherwise. ๐Ÿ˜ญ

๐Ÿ’… OPTIONS ๐Ÿ’…

  • --argv0 string (since glibc 2.33) โ€” sets argv[0] to the value string before running the program. ๐ŸŽ€
  • --audit list โ€” use the objects named in list as auditors. the objects in list are delimited by colons.
  • --glibc-hwcaps-mask list โ€” only search built-in subdirectories if in list.
  • --glibc-hwcaps-prepend list โ€” search glibc-hwcaps subdirectories in list.
  • --inhibit-cache โ€” do not use /etc/ld.so.cache. nope, skipping it. ๐Ÿ™ˆ
  • --library-path path โ€” use path instead of the LD_LIBRARY_PATH environment variable setting (see below). the names ORIGIN, LIB, and PLATFORM are interpreted as they are for the LD_LIBRARY_PATH environment variable.
  • --inhibit-rpath list โ€” ignore RPATH and RUNPATH information in the object names in list. this option is ignored when running in secure-execution mode (see below). the objects in list are delimited by colons or spaces.
  • --list โ€” list all dependencies and how they are resolved. ๐Ÿ—’๏ธ
  • --list-diagnostics (since glibc 2.33) โ€” print system diagnostic information in a machine-readable format, like some internal loader variables, the auxiliary vector (see getauxval(3)), and the environment variables. on some architectures the command might print even more (like the cpu features used in GNU indirect function selection on x86). extra info, no extra charge. โœจ
  • --list-tunables (since glibc 2.33) โ€” print the names and values of all tunables, along with the minimum and maximum allowed values.
  • --preload list (since glibc 2.30) โ€” preload the objects specified in list. the objects in list are delimited by colons or spaces. the objects get preloaded exactly as explained in the description of the LD_PRELOAD environment variable below. and here's the glow-up: by contrast with LD_PRELOAD, the --preload option gives you a way to do preloading for a single executable without affecting preloading done in any child process that executes a new program. one-executable-only behavior, obsessed. ๐Ÿงš
  • --verify โ€” verify that program is dynamically linked and that this dynamic linker can handle it.

๐ŸŒˆ ENVIRONMENT ๐ŸŒˆ

Various environment variables influence how the dynamic linker operates. she has opinions.

๐Ÿ”’ Secure-execution mode ๐Ÿ”’

For security reasons, if the dynamic linker determines that a binary should run in secure-execution mode, the effects of some environment variables are voided or modified โ€” and on top of that, those environment variables get stripped from the environment entirely, so the program doesn't even see the definitions. gone. deleted. ๐Ÿ’€

Some of these environment variables affect the operation of the dynamic linker itself and are described below. The other environment variables treated this way include: GCONV_PATH, GETCONF_DIR, HOSTALIASES, LOCALDOMAIN, LD_AUDIT, LD_DEBUG, LD_DEBUG_OUTPUT, LD_DYNAMIC_WEAK, LD_HWCAP_MASK, LD_LIBRARY_PATH, LD_ORIGIN_PATH, LD_PRELOAD, LD_PROFILE, LD_SHOW_AUXV, LOCALDOMAIN, LOCPATH, MALLOC_TRACE, NIS_PATH, NLSPATH, RESOLV_HOST_CONF, RES_OPTIONS, TMPDIR, and TZDIR.

(yes, LOCALDOMAIN really is in there twice. that's just how the page is, bestie. we don't question the manpage.)

A binary is executed in secure-execution mode if the AT_SECURE entry in the auxiliary vector (see getauxval(3)) has a nonzero value. And that entry can end up nonzero for a bunch of reasons, including:

  • The process's real and effective user IDs differ, or the real and effective group IDs differ. this typically happens when you execute a set-user-ID or set-group-ID program.
  • A process with a non-root user ID executed a binary that conferred capabilities to the process.
  • A nonzero value may have been set by a Linux Security Module.

๐Ÿงš Environment variables ๐Ÿงš

Among the more important environment variables are the following:

  • LD_ASSUME_KERNEL (from glibc 2.2.3 to glibc 2.36) โ€” every shared object can tell the dynamic linker the minimum kernel ABI version it requires. (that requirement is encoded in an ELF note section that you can view via readelf -n as a section labeled NT_GNU_ABI_TAG.) At run time, the dynamic linker figures out the ABI version of the running kernel and will reject loading shared objects that specify minimum ABI versions exceeding that ABI version. she said know your place. So LD_ASSUME_KERNEL can trick the dynamic linker into assuming it's running on a system with a different kernel ABI version. for example, this command line makes the dynamic linker assume it's running on Linux 2.2.5 while loading the shared objects that myprog requires:
$ LD_ASSUME_KERNEL=2.2.5 ./myprog

On systems that provide multiple versions of a shared object (in different directories in the search path) that have different minimum kernel ABI version requirements, LD_ASSUME_KERNEL lets you pick which version of the object gets used โ€” depending on the directory search order, obviously. Historically the most common use of the LD_ASSUME_KERNEL feature was manually selecting the older LinuxThreads POSIX threads implementation on systems that offered both LinuxThreads and NPTL (NPTL was typically the default on such systems); see pthreads(7). retro choice, respect. ๐ŸŒธ

  • LD_BIND_NOW (since glibc 2.1.1) โ€” if set to a nonempty string, it makes the dynamic linker resolve all symbols at program startup instead of deferring function call resolution until the point when they're first referenced. super useful when you're using a debugger. no surprises later, everything's already resolved. ๐Ÿ”ฅ
  • LD_LIBRARY_PATH โ€” a list of directories to search for ELF libraries at execution time. the items in the list are separated by either colons or semicolons, and there is no support for escaping either separator. a zero-length directory name means the current working directory. ๐Ÿ‘‘

(to be continued, bestie โ€” we're only getting started ๐Ÿ’–)

Heads up bestie: this variable gets straight-up ignored in secure-execution mode. ๐Ÿ’€

And here's the cute part โ€” inside the pathnames you put in LD_LIBRARY_PATH, the dynamic linker expands the tokens $ORIGIN, $LIB, and $PLATFORM (or the versions with curly braces around the names) exactly like we talked about above in Dynamic string tokens. So for example, this:

$ LD_LIBRARY_PATH='$ORIGIN/$LIB' prog

would make the linker go looking for a library in either the lib or lib64 subdirectory below the directory that holds the program you're executing. ๐ŸŽ€

Note the single quotes!! They're there to stop $ORIGIN and $LIB from getting expanded as shell variables before the linker ever sees them. Double quotes would've ruined the whole moment. ๐Ÿ˜ญ

  • LD_PRELOAD โ€” a list of extra, user-specified ELF shared objects that get loaded before all the others. This is the โœจ selectively-override-functions-in-other-shared-objects โœจ feature. Main character energy, honestly.

The items in the list can be separated by spaces or colons, and nope, there is no support for escaping either separator. The objects get searched for using the rules given under DESCRIPTION, and they're searched for and added to the link map in the left-to-right order you specified them in. ๐Ÿ“‹

In secure-execution mode, preload pathnames that contain slashes are ignored. And on top of that, shared objects are preloaded only from the standard search directories โ€” and only if they have the set-user-ID mode bit enabled (which is not typical, so don't hold your breath). ๐Ÿ™ˆ

Within the names you drop in the LD_PRELOAD list, the dynamic linker understands the tokens $ORIGIN, $LIB, and $PLATFORM (or the curly-brace versions) as described above in Dynamic string tokens. (Also go peek at the discussion of quoting under the LD_LIBRARY_PATH description โ€” it applies here too.) ๐Ÿฆ‹

There are various methods of specifying libraries to preload, and they get handled in this order:

  1. The LD_PRELOAD environment variable.
  2. The --preload command-line option when you invoke the dynamic linker directly.
  3. The /etc/ld.so.preload file (described below, keep scrolling bestie).
  • LD_TRACE_LOADED_OBJECTS โ€” if you set this (to any value, doesn't matter what), it makes the program list its dynamic dependencies, as if it had been run by ldd(1), instead of running normally. Sneaky little hack, we love her. ๐Ÿ‘‘

Then there are, and I quote, lots of more or less obscure variables, many obsolete or only for internal use. Buckle up. ๐ŸŽข


  • LD_AUDIT (since glibc 2.4) โ€” a list of user-specified, ELF shared objects to be loaded before all others in a separate linker namespace (i.e., one that does not intrude upon the normal symbol bindings that would occur in the process). These objects can be used to audit the operation of the dynamic linker. Items in the list are colon-separated, and nope, there's no support for escaping the separator. ๐ŸŒธ

LD_AUDIT is ignored in secure-execution mode.

The dynamic linker will notify the audit shared objects at so-called auditing checkpoints โ€” for example, loading a new shared object, resolving a symbol, or calling a symbol from another shared object โ€” by calling an appropriate function within the audit shared object. For the full tea, see rtld-audit(7). The auditing interface is largely compatible with the one provided on Solaris, as described in its Linker and Libraries Guide, in the chapter Runtime Linker Auditing Interface. โ˜€๏ธ

Within the names you put in the LD_AUDIT list, the dynamic linker understands the tokens $ORIGIN, $LIB, and $PLATFORM (or the curly-brace versions) as described above in Dynamic string tokens. (Again, see the quoting discussion under LD_LIBRARY_PATH.)

Since glibc 2.13, in secure-execution mode, names in the audit list that contain slashes are ignored, and only shared objects in the standard search directories that have the set-user-ID mode bit enabled get loaded. ๐Ÿ”’

  • LD_BIND_NOT (since glibc 2.1.95) โ€” if this environment variable is set to a nonempty string, don't update the GOT (global offset table) and PLT (procedure linkage table) after resolving a function symbol. And here's the cute combo: pair this with LD_DEBUG (using the categories bindings and symbols) and you can observe all run-time function bindings. ๐Ÿ”
  • LD_DEBUG (since glibc 2.1) โ€” outputs verbose debugging information about the operation of the dynamic linker. The content of this variable is one or more of the following categories, separated by colons, commas, or (if the value is quoted) spaces: ๐Ÿ’…
    • help โ€” specifying help in the value of this variable does not run the specified program; instead it displays a help message about which categories can be specified in this environment variable.
    • all โ€” print all debugging information (except statistics and unused; see below).
    • bindings โ€” display information about which definition each symbol is bound to.
    • files โ€” display progress for input file.
    • libs โ€” display library search paths.
    • reloc โ€” display relocation processing.
    • scopes โ€” display scope information.
    • statistics โ€” display relocation statistics.
    • symbols โ€” display search paths for each symbol look-up.
    • unused โ€” determine unused DSOs.
    • versions โ€” display version dependencies.

Since glibc 2.3.4, LD_DEBUG is ignored in secure-execution mode, unless the file /etc/suid-debug exists (the content of that file is irrelevant โ€” literally doesn't matter what's inside it). ๐Ÿ—‚๏ธ

  • LD_DEBUG_OUTPUT (since glibc 2.1) โ€” by default, LD_DEBUG output goes to standard error. But if LD_DEBUG_OUTPUT is defined, the output is written to the pathname specified by its value, with the suffix "." (a dot) followed by the process ID appended to that pathname. ๐Ÿฃ

LD_DEBUG_OUTPUT is ignored in secure-execution mode.

  • LD_DYNAMIC_WEAK (since glibc 2.1.91) โ€” by default, when searching shared libraries to resolve a symbol reference, the dynamic linker resolves to the first definition it finds.

Old glibc versions (before glibc 2.2) provided a different behavior: if the linker found a symbol that was weak, it would remember that symbol and keep searching in the remaining shared libraries. If it subsequently found a strong definition of the same symbol, it would use that definition instead. (If no further symbol was found, the dynamic linker would use the weak symbol it initially found.) ๐Ÿง 

That old glibc behavior was nonstandard. (Standard practice is that the distinction between weak and strong symbols should have effect only at static link time.) In glibc 2.2, the dynamic linker was modified to provide the current behavior (which was the behavior provided by most other implementations at that time).

Defining the LD_DYNAMIC_WEAK environment variable (with any value) gives you the old (nonstandard) glibc behavior, whereby a weak symbol in one shared library may be overridden by a strong symbol subsequently discovered in another shared library. (Note that even when this variable is set, a strong symbol in a shared library will not override a weak definition of the same symbol in the main program.) โš ๏ธ

Since glibc 2.3.4, LD_DYNAMIC_WEAK is ignored in secure-execution mode.

  • LD_HWCAP_MASK (from glibc 2.1 to glibc 2.38) โ€” mask for hardware capabilities. Since glibc 2.26, the option might be ignored if glibc does not support tunables. ๐Ÿคท
  • LD_ORIGIN_PATH (since glibc 2.1) โ€” path where the binary is found.

Since glibc 2.4, LD_ORIGIN_PATH is ignored in secure-execution mode.

  • LD_POINTER_GUARD (from glibc 2.4 to glibc 2.22) โ€” set to 0 to disable pointer guarding. Any other value enables pointer guarding, which is also the default. Pointer guarding is a security mechanism whereby some pointers to code stored in writable program memory (return addresses saved by setjmp(3) or function pointers used by various glibc internals) are mangled semi-randomly to make it more difficult for an attacker to hijack those pointers for use in the event of a buffer overrun or stack-smashing attack. ๐Ÿ›ก๏ธ

Since glibc 2.23, LD_POINTER_GUARD can no longer be used to disable pointer guarding, which is now always enabled. So she's retired, but iconic.

  • LD_PROFILE (since glibc 2.1) โ€” the name of a (single) shared object to be profiled, specified either as a pathname or a soname. Profiling output is appended to the file whose name is: $LD_PROFILE_OUTPUT/$LD_PROFILE.profile. ๐Ÿ“ˆ

Since glibc 2.2.5, LD_PROFILE uses a different default path in secure-execution mode.

  • LD_PROFILE_OUTPUT (since glibc 2.1) โ€” directory where LD_PROFILE output should be written. If this variable is not defined, or is defined as an empty string, then the default is /var/tmp. ๐Ÿ—ƒ๏ธ

LD_PROFILE_OUTPUT is ignored in secure-execution mode; instead /var/profile is always used.

  • LD_SHOW_AUXV (since glibc 2.1) โ€” if this environment variable is defined (with any value), show the auxiliary array passed up from the kernel (see also getauxval(3)). ๐Ÿ‘€

Since glibc 2.3.4, LD_SHOW_AUXV is ignored in secure-execution mode.

  • LD_TRACE_PRELINKING (from glibc 2.4 to glibc 2.35) โ€” if this environment variable is defined, trace prelinking of the object whose name is assigned to this environment variable. (Use ldd(1) to get a list of the objects that might be traced.) If the object name is not recognized, then all prelinking activity is traced. ๐Ÿ”ญ
  • LD_USE_LOAD_BIAS (from glibc 2.3.3 to glibc 2.35) โ€” by default (i.e., if this variable is not defined), executables and prelinked shared objects will honor base addresses of their dependent shared objects, and (nonprelinked) position-independent executables (PIEs) and other shared objects will not honor them. If LD_USE_LOAD_BIAS is defined with the value 1, both executables and PIEs will honor the base addresses. If LD_USE_LOAD_BIAS is defined with the value 0, neither executables nor PIEs will honor the base addresses. โš–๏ธ

Since glibc 2.3.3, this variable is ignored in secure-execution mode.

๐ŸŒˆ LD_VERBOSE (since glibc <2.1>)

okay so if you set this to literally any nonempty string, the linker will spill symbol versioning tea about the program โ€” but ONLY if you also set the LD_TRACE_LOADED_OBJECTS environment variable. no LD_TRACE_LOADED_OBJECTS, no symbol versioning info. they're a package deal bestie ๐Ÿ’–

โš ๏ธ LD_WARN (since glibc <2.1.3>)

set it to a nonempty string and you get warned about unresolved symbols. that's it, that's the whole vibe. lowkey useful, highkey a little scary when you see the output ๐Ÿ˜ญ

๐Ÿš€ LD_PREFER_MAP_32BIT_EXEC (x86-64 only; since glibc <2.23>)

so according to the Intel Silvermont software optimization guide, for 64-bit applications, branch prediction performance can get negatively impacted when the target of a branch is more than 4 GB away from the branch. ๐Ÿ˜ฌ if this environment variable is set (to any value, doesn't matter what), the dynamic linker will first try to map executable pages using the mmap(2) MAP_32BIT flag, and fall back to mapping without that flag if that attempt fails.

NB: MAP_32BIT will map to the low 2 GB (not 4 GB!!) of the address space. read that again bestie, it's 2, not 4 ๐Ÿ’€

and because MAP_32BIT reduces the address range available for address space layout randomization (ASLR), LD_PREFER_MAP_32BIT_EXEC is always disabled in secure-execution mode. always. no exceptions. periodt ๐Ÿ‘‘


๐Ÿ’พ FILES ๐Ÿ’พ

  • /lib/ld.so โ€” the a.out dynamic linker/loader. ancient icon, we respect her ๐Ÿซถ
  • /lib/ld-linux.so.{1,2} โ€” the ELF dynamic linker/loader. the one you actually meet irl.
  • /etc/ld.so.cache โ€” a file holding a compiled list of directories to search for shared objects, plus an ordered list of candidate shared objects, plus any tunables to be applied. see ldconfig(8). she's the precomputed, pre-slay version of your search path โœจ
  • /etc/ld.so.preload โ€” a file containing a whitespace-separated list of ELF shared objects to be loaded before the program. see the discussion of LD_PRELOAD above. if both LD_PRELOAD and /etc/ld.so.preload are employed, the libraries specified by LD_PRELOAD are preloaded first.
bestie, listen: /etc/ld.so.preload has a system-wide effect, causing the specified libraries to be preloaded for all programs that are executed on the system. that is usually undesirable, and typically employed only as an emergency remedy โ€” for example, as a temporary workaround to a library misconfiguration issue. it's giving "break glass in case of emergency", not "everyday accessory" ๐Ÿšจ
  • lib*.so* โ€” shared objects. the whole reason we're here ๐Ÿ’…

๐Ÿ“ NOTES ๐Ÿ“

๐Ÿ•ฐ๏ธ Legacy Hardware capabilities (from glibc <2.5> to glibc <2.37>)

some shared objects are compiled using hardware-specific instructions which do not exist on every CPU. ๐Ÿ™ˆ such objects should be installed in directories whose names define the required hardware capabilities, like /usr/lib/sse2/. the dynamic linker checks these directories against the hardware of the machine and selects the most suitable version of a given shared object. hardware capability directories can be cascaded to combine CPU features. the list of supported hardware capability names depends on the CPU, and the following names are currently recognized:

  • Alpha โ€” ev4, ev5, ev56, ev6, ev67
  • MIPS โ€” loongson2e, loongson2f, octeon, octeon2
  • PowerPC โ€” 4xxmac, altivec, arch_2_05, arch_2_06, booke, cellbe, dfp, efpdouble, efpsingle, fpu, ic_snoop, mmu, notb, pa6t, power4, power5, power5+, power6x, ppc32, ppc601, ppc64, smt, spe, ucache, vsx
  • SPARC โ€” flush, muldiv, stbar, swap, ultra3, v9, v9v, v9v2
  • s390 โ€” dfp, eimm, esan3, etf3enh, g5, highgprs, hpage, ldisp, msa, stfle, z900, z990, z9-109, z10, zarch
  • x86 (32-bit only) โ€” acpi, apic, clflush, cmov, cx8, dts, fxsr, ht, i386, i486, i586, i686, mca, mmx, mtrr, pat, pbe, pge, pn, pse36, sep, ss, sse, sse2, tm

but here's the thing โ€” the legacy hardware capabilities support has a drawback: each new feature added grows the search path exponentially, because it has to be added to every combination of the other existing features. it's giving "invite one more person to the group chat and suddenly there's 47 new chats" ๐Ÿ˜ญ

for instance, on x86 32-bit, if the hardware supports i686 and sse2, the resulting search path will be i686/sse2:i686:sse2:... a new capability newcap will set the search path to newcap/i686/sse2:newcap/i686:newcap/sse2:newcap:i686/sse2:i686:sse2:. see how fast that got out of hand?? ๐Ÿ’€

โœจ glibc Hardware capabilities (from glibc <2.33>)

glibc 2.33 added a new hardware capability scheme, where under each CPU architecture certain levels can be defined, grouping support for certain features or special instructions. each architecture level has a fixed set of paths that it adds to the dynamic linker search list, depending on the hardware of the machine. and since each new architecture level is not combined with previously existing ones, the new scheme does not have the drawback of growing the dynamic linker search list uncontrollably. we love a glow-up that fixes the actual problem ๐ŸŒธ

for instance, on x86 64-bit, if the hardware supports x86_64-v3 (for instance Intel Haswell or AMD Excavator), the resulting search path will be glibc-hwcaps/x86-64-v3:glibc-hwcaps/x86-64-v2:. the following paths are currently supported, in priority order ๐Ÿ‘‘

  • PowerPC (64-bit little-endian only) โ€” power10, power9
  • s390 (64-bit only) โ€” z16, z15, z14, z13
  • x86 (64-bit only) โ€” x86-64-v4, x86-64-v3, x86-64-v2
heads up: glibc 2.37 removed support for the legacy hardware capabilities. she's gone, bestie. retired. we pour one out ๐Ÿฅ€

๐Ÿ”— SEE ALSO ๐Ÿ”—

ld(1), ldd(1), pldd(1), sprof(1), dlopen(3), getauxval(3), elf(5), ld.so.conf(8), capabilities(7), rtld-audit(7), ldconfig(8), sln(8)

go read the whole friend group, they all know each other ๐Ÿซถ


aaaand that's the whole page bestie!! this tea came straight from Linux man-pages 6.20, dated 2026-08-03, and it's the ld.so(8) manual. go forth, preload responsibly, and never touch /etc/ld.so.preload without a plan ๐Ÿ’…โœจ๐ŸŽ€๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from
  • arch rolling: man-pages 6.20-1 ยท BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
  • nixpkgs unstable: man-pages 6.20
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms