Linux & Bash · Part II · Module 25 · EDA Environment and Tool Access
Shared Libraries and LD_LIBRARY_PATH
The executable is found and the tool still does not run, because it loads a dozen libraries at startup and one of them is not where it was built to expect. The message comes from the dynamic loader rather than the tool, the status is a signal death, and on macOS the variable that fixes it is deleted by the operating system between your shell and the program.
Advanced22 min readLinuxPart IIShared LibrariesdyldPortability
Part II · Module 25 · Chapter 25.3 · Shared Libraries and LD_LIBRARY_PATH
A commercial simulator is not one program. It is a small executable and tens or hundreds of shared libraries, loaded at startup from paths decided when the tool was built. Precondition two from Chapter 25.1 is that every one of them resolves.
1. What a Tool Links Against
ldd is the Linux tool for this, and it is not here:
$ command -v ldd
$ echo $?
1The macOS equivalent is otool -L:
$ otool -L tools/simtool-2024.09/bin/simtool
tools/simtool-2024.09/bin/simtool:
@rpath/libsim.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)Two dependencies, and they are two different kinds:
/usr/lib/libSystem.B.dylib is an absolute path. The loader opens exactly that file. If it is missing the installation is broken in a way no environment variable will fix.
@rpath/libsim.dylib is relative to a search path baked into the executable, which you can read:
$ otool -l tools/simtool-2024.09/bin/simtool | grep -A2 LC_RPATH | grep path
path @executable_path/../lib (offset 12)@executable_path/../lib — so the tool looks for its libraries in the lib directory beside its own bin. That is how a self-contained tool installation works, and it is why PATH order in Chapter 25.2 changed the library version as well as the executable: each installation's bin has its own ../lib.
| Platform | Inspect | Variable | Baked-in search path |
|---|---|---|---|
| Linux | ldd, objdump -p, readelf -d | LD_LIBRARY_PATH | RPATH / RUNPATH, $ORIGIN |
| macOS | otool -L, otool -l, dyld_info | DYLD_LIBRARY_PATH | LC_RPATH, @executable_path |
The concepts map one to one. $ORIGIN on Linux is @executable_path on macOS, and RUNPATH is LC_RPATH. Only the macOS half is demonstrated here, because that is the machine these measurements were taken on; the Linux spellings are attributed, not shown.
2. The Failure, and Who Reports It
Move the library out of the way — which is what a half-finished installation, a wrong PATH, or a tool directory mounted on one machine and not another amounts to:
$ mv tools/simtool-2024.09/lib/libsim.dylib /tmp/
$ SIMTOOL_LICENSE_FILE=$LIC ./tools/simtool-2024.09/bin/simtool chip_top
dyld[20842]: Library not loaded: @rpath/libsim.dylib
Referenced from: <3256BA32-DEF8-3267-9CDB-9E1893613927> /home/engineer/eda/tools/simtool-2024.09/bin/simtool
Reason: tried: '/home/engineer/eda/tools/simtool-2024.09/lib/libsim.dylib' (no such file), '/home/engineer/eda/tools/simtool-2024.09/lib/libsim.dylib' (no such file)
$ echo $?
134Five things to read out of that:
dyld[20842] is the dynamic loader, not the tool. The tool's main has not run. So nothing in the tool's documentation, its log, its configuration or your design can be the cause — and searching the vendor's knowledge base for "Library not loaded" finds nothing useful.
The library name is the build-time name, @rpath/libsim.dylib, not a filesystem path. That is what the executable records.
Referenced from: names which binary wanted it, which matters when the failing library is needed by another library rather than by the executable.
Reason: tried: lists every path it attempted. This is the most useful line in the message, and the one people stop reading before. It tells you exactly where the loader looked, so you can see whether your fix changed anything.
And the status is 134, which is 128 + 6 — killed by SIGABRT. Chapter 7.6 established that arithmetic for Ctrl-C; here it appears where you would not expect a signal, and it is the reason a || in a flow script behaves differently from a tool's own non-zero exit:
simtool chip_top || echo "tool failed with $?" # prints 134, not 13. The Variable That Overrides the Search
DYLD_LIBRARY_PATH on macOS, LD_LIBRARY_PATH on Linux — a colon-separated list of directories searched before the baked-in paths:
$ DYLD_LIBRARY_PATH=/tmp SIMTOOL_LICENSE_FILE=$LIC ./tools/simtool-2024.09/bin/simtool chip_top
simtool 2024.09 starting (simlib 1.4.2)
simlib: elaborating chip_top
$ echo $?
0The library is in /tmp, the loader found it there, and the tool ran. That is the fix, and it is also a trap, because it overrides the tool's own correct answer — point it at the wrong version's lib and you get a tool whose executable and library are from different releases, which fails in ways no message explains.
Prefer fixing the installation. Use the variable to diagnose.
4. The Second-Order Case: a Library Needing a Library
The failure in §2 was the executable missing its own dependency. More often in EDA the chain is longer: the tool loads its own library, which loads a system library, which is the one that is missing — frequently a C++ runtime or a graphics library on a compute node with no display.
otool -L on the library rather than the executable walks one step. The recursive version is the thing to run when a message names a library you have never heard of:
#!/usr/bin/env bash
# libtree — every library a binary needs, transitively
# usage: libtree <binary>
# exit: 0 walked · 2 usage or no such file
set -o nounset
[ $# -ge 1 ] || { printf 'usage: libtree <binary>\n' >&2; exit 2; }
f=$1
[ -e "$f" ] || { printf 'libtree: no such file: %s\n' "$f" >&2; exit 2; }
seen=" "
walk() {
local f=$1 depth=$2 dep
case $seen in (*" $f "*) return ;; esac
seen="$seen$f "
while IFS= read -r dep; do
case $dep in
(@*) printf '%*s%s [rpath-relative, not followed]\n' "$depth" "" "$dep"; continue ;;
(/usr/lib/*|/System/Library/*)
printf '%*s%s [system, in the dyld cache]\n' "$depth" "" "$dep"; continue ;;
esac
if [ -e "$dep" ]; then
printf '%*s%s\n' "$depth" "" "$dep"
walk "$dep" $(( depth + 2 ))
else
printf '%*s%s ** NOT FOUND\n' "$depth" "" "$dep"
fi
done < <(otool -L "$f" 2>/dev/null | sed '1d' | awk '{print $1}')
}
printf '%s\n' "$f"
walk "$f" 2$ ./libtree "$(command -v nvc)"
/opt/homebrew/bin/nvc
/usr/lib/libSystem.B.dylib [system, in the dyld cache]
/usr/lib/libz.1.dylib [system, in the dyld cache]
/usr/lib/libffi.dylib [system, in the dyld cache]
/opt/homebrew/opt/zstd/lib/libzstd.1.dylib
/opt/homebrew/opt/zstd/lib/libzstd.1.dylib
/usr/lib/libSystem.B.dylib [system, in the dyld cache]
/opt/homebrew/opt/llvm/lib/libLLVM.23.1.dylib
/opt/homebrew/opt/llvm/lib/libLLVM.23.1.dylib
/usr/lib/libffi.dylib [system, in the dyld cache]
/usr/lib/libedit.3.dylib [system, in the dyld cache]
/usr/lib/libSystem.B.dylib [system, in the dyld cache]
/opt/homebrew/opt/z3/lib/libz3.5.1.dylib
/opt/homebrew/opt/z3/lib/libz3.5.1.dylib
/usr/lib/libc++.1.dylib [system, in the dyld cache]
/usr/lib/libSystem.B.dylib [system, in the dyld cache]
/usr/lib/libz.1.dylib [system, in the dyld cache]
/opt/homebrew/opt/zstd/lib/libzstd.1.dylib
/usr/lib/libxml2.2.dylib [system, in the dyld cache]
/usr/lib/libc++.1.dylib [system, in the dyld cache]
/usr/lib/libc++.1.dylib [system, in the dyld cache]A real VHDL simulator needs LLVM, which needs Z3, which needs the C++ runtime. That is the chain a "Library not loaded" message is somewhere inside, and seeing it laid out is usually enough to locate the break.
And on a binary with a genuinely missing dependency:
$ ./libtree /tmp/ghostuser
/tmp/ghostuser
/tmp/libghost-MOVED.dylib ** NOT FOUND
/usr/lib/libSystem.B.dylib [system, in the dyld cache]Four more decisions in that script:
case $seen in (*" $f "*) — a visited set held in a string, with the pattern parenthesised for the Bash 3.2 reason Chapter 25.1's callout measured. The set is not optional: with it, nvc produces 21 lines; with the check removed, the walk was still running after five seconds and had produced 114, because system libraries reference each other in cycles.
The spaces around $f in both the set and the pattern, so libfoo does not match inside libfoobar. The same bug as the PATH guard in Chapter 25.6 §3, and the same fix.
printf '%*s%s\n' "$depth" "" for indentation — a * field width takes its value from an argument, which indents by a variable amount with no string building.
And the usage check is explicit rather than ${1:?}, because the documented exit code is 2 and :? gives 1 — measured in Chapter 23.4 §5.
5. The Errors, and What Each One Means
| Message | Platform | Means |
|---|---|---|
Library not loaded: X + tried: list | macOS | the loader looked in those places and found nothing |
error while loading shared libraries: X: cannot open shared object file | Linux | the same thing |
X: wrong ELF class: ELFCLASS32 | Linux | found a 32-bit library for a 64-bit tool |
Symbol not found: _Y + Expected in: X | macOS | the library was found and is the wrong version |
undefined symbol: Y | Linux | the same |
GLIBC_2.34' not found | Linux | the tool is newer than the operating system |
The fourth and fifth rows are the ones worth recognising, because they are not "missing library" at all — the file was found and opened, and it does not contain a function the tool needs. That is a version mismatch, and pointing LD_LIBRARY_PATH at more directories usually makes it worse by finding yet another wrong copy.
The last row is the one that cannot be fixed with a variable: a tool built against a newer C library than the machine provides will not run on that machine, and the answer is a different machine or a different tool build. On a farm with mixed operating-system versions, that is a scheduler constraint rather than an environment problem — Chapter 30.5's territory.
6. What To Do, In Order
-version— if that works, this chapter is not your problem.otool -L(orldd) on the executable. Absolute paths that do not exist are a broken installation.- Read the
tried:list in the error. It tells you where the loader looked. otool -l | grep -A2 LC_RPATH(orobjdump -p | grep RUNPATH) to see the baked-in search path. If it is@executable_path-relative, the tool expects to be self-contained and something has moved.- Only then set the variable, as a diagnosis — and note whether the
tried:list changes, which tells you whether the variable reached the loader at all. - Fix the installation, not the environment. A variable that must be set for a tool to run is a tool that will fail for the next person.
Practice
- Run
otool -Lorlddon a tool you use. How many libraries, and how many are absolute paths? - Find the baked-in search path with
otool -l | grep -A2 LC_RPATHorobjdump -p | grep RUNPATH. - Make a copy of a small program, move one of its libraries, and run it. Record the exact message and the status.
- Is the status
128 + N? Which signal? - Set
LD_LIBRARY_PATH/DYLD_LIBRARY_PATHto rescue it. Did thetried:list change? - On macOS, run
DYLD_LIBRARY_PATH=/tmp /bin/sh -c 'echo $DYLD_LIBRARY_PATH'. Explain the output. - Then run the same with a program you compiled yourself and explain the difference.
- Write a
#!/bin/shwrapper around a tool, setDYLD_LIBRARY_PATHoutside it, and confirm the tool does not see it. - Write
libtreeand run it on a tool. How deep does the tree go, and are there cycles? - Remove the visited set from
libtreeand confirm it does not terminate. - Remove the spaces from the visited-set pattern and find a library pair where it matters.
- Find a
Symbol not foundorundefined symbolerror in your own history. Was it fixed by adding a directory or by removing one? - Set
LD_LIBRARY_PATHglobally in a shell, then run a completely unrelated program. Did anything change?
Check Your Understanding
- What are the two kinds of entry in
otool -Loutput, and why does the distinction matter? - What does
@executable_path/../libmean, and why is it good? - Who writes the
Library not loadedmessage, and what does that rule out? - Which line of the loader error is most useful?
- Why is the status 134 rather than 1?
- Why did the valid licence make no difference?
- What does macOS do to
DYLD_*variables, and which programs are affected? - Why does a
#!/bin/shwrapper break aDYLD_LIBRARY_PATHfix? - What does
Symbol not foundmean thatLibrary not loadeddoes not? - Why is setting
LD_LIBRARY_PATHin a flow script harmful? - In
libtree, why is a visited set required?
7. Remember
- A tool is an executable plus many libraries, resolved at startup from paths decided at build time.
lddis Linux;otool -Lis macOS.LD_LIBRARY_PATHandDYLD_LIBRARY_PATHare the matching variables;$ORIGINand@executable_paththe matching baked-in roots.- Two kinds of dependency: an absolute path (a missing one is a broken install) and an
@rpathname (redirectable). @executable_path/../libis a self-contained installation and needs no variable — which is whyPATHorder changes the library version too.- The loader writes the error, not the tool.
dyld[pid]: Library not loaded, beforemainruns. - Read the
tried:list. It is the only line that says where it looked. - The status is 134 = 128 + 6, a
SIGABRT, so||sees 134. - A valid licence is irrelevant at this layer.
-versionsplits the diagnosis in one command. - macOS strips
DYLD_*for system binaries — measured via/bin/shandenv— so the fix vanishes through a#!/bin/shwrapper while working when typed directly. install_name_tool -add_rpathrepairs the binary; the variable only diagnoses.Symbol not foundmeans the wrong version was found, not a missing file. More directories makes it worse.- Never set
LD_LIBRARY_PATHglobally in a flow. Scope it with aVAR=value cmdprefix, or record it. - A transitive walk needs a visited set — library cycles are normal. Measured: 21 lines with it, still running after five seconds without.
otoolrejects--, producing no output rather than an error you notice. Run a command once after adding--.- Most
/usr/liblibraries are not files on modern macOS — they live in the dyld shared cache, so[ -e ]is false for a loadable library. Classify by path;dyld_info -dependentsknows the truth.
8. What Is Next
Chapter 25.4 takes precondition three: the licence variables, what each spelling means, and how to tell a licence failure apart from a tool failure and a design failure by what the log says.
Where this fits
Part of the Linux & Bash curriculum.
