The original ELF specification defined only one way to embed a runtime library search path in a binary: the DT_RPATH tag. This worked well, but it had one significant drawback: it could NOT be overridden by LD_LIBRARY_PATH.
This became a problem when users needed to test programs with alternative library versions (e.g., debugging builds). You couldn’t override the baked-in rpath with LD_LIBRARY_PATH โ it was completely ignored.
Later ELF specifications introduced a new tag, DT_RUNPATH, which has lower priority than LD_LIBRARY_PATH. This allows users and sysadmins to override the rpath at runtime without recompiling the binary.
By default, -rpath creates a DT_RPATH entry. To create DT_RUNPATH instead, add the --enable-new-dtags linker option:
# Default (creates DT_RPATH):
gcc -o prog prog.c \
-Wl,-rpath,/home/mtk/pdir/d1 \
-L/home/mtk/pdir/d1 -lx1
objdump -p prog | grep PATH
# RPATH /home/mtk/pdir/d1 <-- only DT_RPATH
# With --enable-new-dtags (creates BOTH DT_RPATH and DT_RUNPATH):
gcc -o prog prog.c \
-Wl,--enable-new-dtags \
-Wl,-rpath,/home/mtk/pdir/d1 \
-L/home/mtk/pdir/d1 -lx1
objdump -p prog | grep PATH
# RPATH /home/mtk/pdir/d1 <-- for older dynamic linkers
# RUNPATH /home/mtk/pdir/d1 <-- for modern dynamic linkers
--enable-new-dtags is used, the linker writes the path into BOTH tags for backward compatibility. Older dynamic linkers (before glibc 2.2) that don’t understand DT_RUNPATH will fall back to DT_RPATH. Modern dynamic linkers that support DT_RUNPATH will use it and ignore DT_RPATH.--enable-new-dtags so LD_LIBRARY_PATH can override your rpath for testing and debugging. This became the default in many Linux distributions.#!/bin/bash
# dt_rpath_test.sh โ demonstrates that DT_RPATH ignores LD_LIBRARY_PATH
set -e
BASE=/tmp/dtag_demo
LIBV1=$BASE/lib_v1
LIBV2=$BASE/lib_v2
mkdir -p $BASE/bin $LIBV1 $LIBV2
# Build version 1 of the library
cat > $LIBV1/libfoo.c << 'EOF'
#include <stdio.h>
void foo(void) { printf("foo() from VERSION 1 of libfoo\n"); }
EOF
gcc -shared -fPIC -o $LIBV1/libfoo.so $LIBV1/libfoo.c
# Build version 2 of the library
cat > $LIBV2/libfoo.c << 'EOF'
#include <stdio.h>
void foo(void) { printf("foo() from VERSION 2 of libfoo\n"); }
EOF
gcc -shared -fPIC -o $LIBV2/libfoo.so $LIBV2/libfoo.c
# Build main.c
cat > $BASE/main.c << 'EOF'
#include <stdio.h>
extern void foo(void);
int main(void) {
foo();
return 0;
}
EOF
echo "=== Build prog_rpath (DT_RPATH pointing to lib_v1) ==="
gcc -o $BASE/bin/prog_rpath $BASE/main.c \
-Wl,-rpath,$LIBV1 \
-L$LIBV1 -lfoo
# Note: NO --enable-new-dtags, so DT_RPATH is used
echo ""
echo "=== Build prog_runpath (DT_RUNPATH pointing to lib_v1) ==="
gcc -o $BASE/bin/prog_runpath $BASE/main.c \
-Wl,--enable-new-dtags \
-Wl,-rpath,$LIBV1 \
-L$LIBV1 -lfoo
echo ""
echo "=== Verify ELF tags ==="
echo "prog_rpath:"
objdump -p $BASE/bin/prog_rpath | grep -E 'RPATH|RUNPATH'
echo "prog_runpath:"
objdump -p $BASE/bin/prog_runpath | grep -E 'RPATH|RUNPATH'
echo ""
echo "=== Run normally (both use lib_v1 from rpath) ==="
$BASE/bin/prog_rpath
$BASE/bin/prog_runpath
echo ""
echo "=== Try to override with LD_LIBRARY_PATH pointing to lib_v2 ==="
echo "prog_rpath (DT_RPATH โ LD_LIBRARY_PATH is IGNORED):"
LD_LIBRARY_PATH=$LIBV2 $BASE/bin/prog_rpath
# Still prints "VERSION 1" because DT_RPATH beats LD_LIBRARY_PATH
echo "prog_runpath (DT_RUNPATH โ LD_LIBRARY_PATH WORKS):"
LD_LIBRARY_PATH=$LIBV2 $BASE/bin/prog_runpath
# Prints "VERSION 2" because LD_LIBRARY_PATH beats DT_RUNPATH
#!/bin/bash
# inspect_elf_tags.sh โ use readelf to show DT_RPATH/DT_RUNPATH tags
BINARY=$1 # pass the binary as argument
echo "=== Dynamic section of: $BINARY ==="
readelf -d $BINARY | head -40
echo ""
echo "=== Filtered: RPATH/RUNPATH/NEEDED ==="
readelf -d $BINARY | grep -E 'RPATH|RUNPATH|NEEDED'
echo ""
echo "=== Same info via objdump ==="
objdump -p $BINARY | grep -E 'RPATH|RUNPATH|NEEDED'
# Example output for a binary built with --enable-new-dtags:
# (readelf -d)
# 0x000000000000001d (RUNPATH) Library runpath: [/home/mtk/pdir/d1]
# 0x000000000000000f (RPATH) Library rpath: [/home/mtk/pdir/d1]
# 0x0000000000000001 (NEEDED) Shared library: [libx1.so]
# 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
#
# Modern dynamic linkers seeing BOTH tags will use RUNPATH and ignore RPATH.
/* test_scenario.c โ scenario where DT_RUNPATH is useful for testing */
/*
* Scenario:
* - Production build of myapp has DT_RPATH pointing to /usr/lib/myapp/
* - You want to test with a DEBUG version of libfoo.so in /tmp/debug/
* - With DT_RPATH: you CANNOT override it. Must rebuild the binary.
* - With DT_RUNPATH: just set LD_LIBRARY_PATH=/tmp/debug/ before running.
*/
/* production_build.sh */
/*
gcc -o myapp main.c \
-Wl,-rpath,/usr/lib/myapp \ <-- DT_RPATH (can't override later)
-L/usr/lib/myapp -lfoo
*/
/* better_build.sh */
/*
gcc -o myapp main.c \
-Wl,--enable-new-dtags \
-Wl,-rpath,/usr/lib/myapp \ <-- DT_RUNPATH (can override)
-L/usr/lib/myapp -lfoo
# Now to test with debug library:
LD_LIBRARY_PATH=/tmp/debug_libs ./myapp <-- uses debug libfoo.so
./myapp <-- uses production libfoo.so
*/
#include <stdio.h>
extern void foo_operation(void); // from libfoo
int main(void) {
printf("Running myapp...\n");
foo_operation();
return 0;
}
| Feature | DT_RPATH | DT_RUNPATH |
|---|---|---|
| ELF standard status | Deprecated (old) | Current standard |
| glibc support since | Original | glibc 2.2 |
| LD_LIBRARY_PATH override | โ Cannot override | โ Can override |
| gcc flag to create | -Wl,-rpath,... (default) |
-Wl,--enable-new-dtags -Wl,-rpath,... |
| Transitive rpath following | Yes | No (RUNPATH is not followed for deps) |
| Recommended for new code | No | Yes |
-Wl,--enable-new-dtags to the gcc command line along with -Wl,-rpath,.... This causes the linker to write the path into BOTH the DT_RPATH and DT_RUNPATH fields. Modern dynamic linkers that understand DT_RUNPATH will use it (and ignore DT_RPATH). Older linkers (pre-glibc 2.2) that don’t know about DT_RUNPATH will fall back to DT_RPATH.--enable-new-dtags, the linker writes the rpath into both DT_RPATH and DT_RUNPATH fields for backward compatibility. Older dynamic linkers that don’t support DT_RUNPATH will use DT_RPATH. Newer dynamic linkers that support DT_RUNPATH will use it and completely ignore DT_RPATH.readelf -d binary | grep -E 'RPATH|RUNPATH' or objdump -p binary | grep -E 'RPATH|RUNPATH'. readelf uses the tag names from the ELF dynamic section; you’ll see lines like (RPATH) or (RUNPATH) indicating which tag is present.