Skip to content
VLSI Mentor

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.

ldd is the Linux tool for this, and it is not here:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ command -v ldd
$ echo $?
1

The macOS equivalent is otool -L:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.

PlatformInspectVariableBaked-in search path
Linuxldd, objdump -p, readelf -dLD_LIBRARY_PATHRPATH / RUNPATH, $ORIGIN
macOSotool -L, otool -l, dyld_infoDYLD_LIBRARY_PATHLC_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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 $?
134

Five 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
simtool chip_top || echo "tool failed with $?"     # prints 134, not 1

DYLD_LIBRARY_PATH on macOS, LD_LIBRARY_PATH on Linux — a colon-separated list of directories searched before the baked-in paths:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 $?
0

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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

MessagePlatformMeans
Library not loaded: X + tried: listmacOSthe loader looked in those places and found nothing
error while loading shared libraries: X: cannot open shared object fileLinuxthe same thing
X: wrong ELF class: ELFCLASS32Linuxfound a 32-bit library for a 64-bit tool
Symbol not found: _Y + Expected in: XmacOSthe library was found and is the wrong version
undefined symbol: YLinuxthe same
GLIBC_2.34' not foundLinuxthe 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

  1. -version — if that works, this chapter is not your problem.
  2. otool -L (or ldd) on the executable. Absolute paths that do not exist are a broken installation.
  3. Read the tried: list in the error. It tells you where the loader looked.
  4. otool -l | grep -A2 LC_RPATH (or objdump -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.
  5. 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.
  6. 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

  1. Run otool -L or ldd on a tool you use. How many libraries, and how many are absolute paths?
  2. Find the baked-in search path with otool -l | grep -A2 LC_RPATH or objdump -p | grep RUNPATH.
  3. Make a copy of a small program, move one of its libraries, and run it. Record the exact message and the status.
  4. Is the status 128 + N? Which signal?
  5. Set LD_LIBRARY_PATH / DYLD_LIBRARY_PATH to rescue it. Did the tried: list change?
  6. On macOS, run DYLD_LIBRARY_PATH=/tmp /bin/sh -c 'echo $DYLD_LIBRARY_PATH'. Explain the output.
  7. Then run the same with a program you compiled yourself and explain the difference.
  8. Write a #!/bin/sh wrapper around a tool, set DYLD_LIBRARY_PATH outside it, and confirm the tool does not see it.
  9. Write libtree and run it on a tool. How deep does the tree go, and are there cycles?
  10. Remove the visited set from libtree and confirm it does not terminate.
  11. Remove the spaces from the visited-set pattern and find a library pair where it matters.
  12. Find a Symbol not found or undefined symbol error in your own history. Was it fixed by adding a directory or by removing one?
  13. Set LD_LIBRARY_PATH globally in a shell, then run a completely unrelated program. Did anything change?

Check Your Understanding

  1. What are the two kinds of entry in otool -L output, and why does the distinction matter?
  2. What does @executable_path/../lib mean, and why is it good?
  3. Who writes the Library not loaded message, and what does that rule out?
  4. Which line of the loader error is most useful?
  5. Why is the status 134 rather than 1?
  6. Why did the valid licence make no difference?
  7. What does macOS do to DYLD_* variables, and which programs are affected?
  8. Why does a #!/bin/sh wrapper break a DYLD_LIBRARY_PATH fix?
  9. What does Symbol not found mean that Library not loaded does not?
  10. Why is setting LD_LIBRARY_PATH in a flow script harmful?
  11. 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.
  • ldd is Linux; otool -L is macOS. LD_LIBRARY_PATH and DYLD_LIBRARY_PATH are the matching variables; $ORIGIN and @executable_path the matching baked-in roots.
  • Two kinds of dependency: an absolute path (a missing one is a broken install) and an @rpath name (redirectable).
  • @executable_path/../lib is a self-contained installation and needs no variable — which is why PATH order changes the library version too.
  • The loader writes the error, not the tool. dyld[pid]: Library not loaded, before main runs.
  • 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. -version splits the diagnosis in one command.
  • macOS strips DYLD_* for system binaries — measured via /bin/sh and env — so the fix vanishes through a #!/bin/sh wrapper while working when typed directly.
  • install_name_tool -add_rpath repairs the binary; the variable only diagnoses.
  • Symbol not found means the wrong version was found, not a missing file. More directories makes it worse.
  • Never set LD_LIBRARY_PATH globally in a flow. Scope it with a VAR=value cmd prefix, 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.
  • otool rejects --, producing no output rather than an error you notice. Run a command once after adding --.
  • Most /usr/lib libraries 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 -dependents knows 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.