App::Ariza::ELF
NAME
App::Ariza::ELF - read what an ELF file asks the loader for, with no toolchain installed
SYNOPSIS
use App::Ariza::ELF;
my %d = elf-dynamic('native/sqlcipher/libsqlcipher.so.0');
say %d<class>; # 64
say %d<endian>; # little
say %d<needed>; # (libcrypto.so.3 libc.so.6)
say %d<runpath>; # $ORIGIN
say %d<rpath>; # (Str) -- this file has no DT_RPATH
# What the loader will search, which is what `patchelf --print-rpath`
# prints: DT_RUNPATH where there is one, DT_RPATH otherwise.
say elf-loader-rpath(%d); # $ORIGIN
# A file built with the old tag, pointing at the machine that built it:
say elf-loader-rpath(elf-dynamic('libfoo.so')); # /opt/build/lib:$ORIGIN
# A static executable has nothing to ask for, and says so:
my %s = elf-dynamic('static-tool');
say %s<dynamic>; # False
say %s<needed>; # ()
# Anything that does not parse is a death, never a guess:
elf-dynamic('truncated.so');
# ariza: cannot read the dynamic section of /ā¦/truncated.so: reading its
# dynamic section needs 400 bytes at offset 600, and the file is 700
# bytes long
DESCRIPTION
Everything App::Ariza::Native needs to know about an ELF shared
library before it will let one into a bundle is in three entries of its
dynamic section: the DT_NEEDED sonames it asks the loader for, and the
DT_RPATH or DT_RUNPATH the loader is told to search for them. This
module reads those, and nothing else, out of the file's bytes.
It exists because the tools that otherwise answer the question are not
on the machines that need the answer. readelf comes with binutils,
which a stock Linux install, a slim container image and every Mac lack;
patchelf is a package almost nothing installs by default. An audit
that dies because a tool is missing cannot run where it is wanted, and
one that answers "no dependencies" when the tool is missing passes the
bundle it exists to stop. Reading the format directly has neither
failure, and gives the same answer on every host ā an ELF bundle can be
checked from a Mac or a Windows box exactly as it is checked on Linux.
What is read, and how
The walk is the loader's, not the static linker's:
The identification bytes: the
\x7FELFmagic,EI_CLASS(32- or 64-bit),EI_DATA(little- or big-endian) andEI_VERSION. Every field after them is read at the width and in the byte order they name, so one reader covers x86_64 and i686, aarch64 and s390x, PowerPC both ways round.The ELF header, for where the program headers are, how many there are, and how big each one is.
The program headers: the
PT_LOADsegments, which are the map between the addresses a dynamic section records and the bytes in the file, and the onePT_DYNAMICsegment, which is the dynamic section.The dynamic section's entries, up to the
DT_NULLthat ends it.The string table, found through
DT_STRTABā an address, mapped to a file offset through thePT_LOADsegment that holds it ā and bounded byDT_STRSZ.
The section table is not read. It is the static linker's account of the
file, it can be stripped without changing what loads, and it can
disagree with the segments; the loader reads only the segments, so this
does too. The one exception is the format's own: when e_phnum is
PN_XNUM (0xFFFF), the real program-header count is in section
header 0, and is read from there.
$ORIGIN is not expanded and no search is performed. This reports what
the file asks for; where the asking would end up on a given machine is
ldd's question, and only that machine can answer it.
What counts as malformed
Each of these is a die that names the file and the part of it that was wrong, rather than an answer:
a file too short for the structure being read ā the commonest case, and the message says which structure wanted how many bytes where;
no ELF magic number, or an
EI_CLASS,EI_DATAorEI_VERSIONbyte that is not one the format defines;a program header size that is not the one the class defines, or program headers that overlap the ELF header;
an executable or shared object with no program headers, which no loader could map;
more than one
PT_DYNAMIC;a dynamic section with no
DT_NULLinside it;more than one
DT_RPATH,DT_RUNPATH,DT_STRTABorDT_STRSZ, which no linker writes, and between which the loader's choice is not something to guess at;DT_NEEDED,DT_RPATHorDT_RUNPATHentries and noDT_STRTAB;a
DT_STRTABaddress that is in noPT_LOAD, or in the part of one that has no bytes in the file, or aDT_STRSZthat runs past its segment;a string offset past the end of the string table, a string with no terminating NUL inside it, or a
DT_NEEDEDnaming the empty string.
Two things that look odd are not malformed, and are answered rather
than refused: a file with no PT_DYNAMIC (a static executable) or no
program headers at all (an object file) has dynamic False and needs
nothing; and a file with both DT_RPATH and DT_RUNPATH ā which older
GNU linkers wrote for --enable-new-dtags ā reports both, with
elf-loader-rpath choosing as the loader does.
FUNCTIONS
elf-dynamic(IO() $file --> Hash)
{ class, endian, dynamic, needed, rpath, runpath }, as described
above; or a die. needed is a List in file order, duplicates and
all; rpath and runpath are Str, undefined where the tag is
absent and the empty string where the tag is present and empty. Strings
are decoded as utf8-c8, so a path that is not valid UTF-8 survives the
round trip byte for byte. Exported.
elf-loader-rpath(%dynamic --> Str)
The search path the loader uses, from an elf-dynamic result:
runpath if defined, else rpath if defined, else the empty string.
That is the loader's precedence and patchelf --print-rpath's output.
Exported.
SEE ALSO
App::Ariza::Native, whose Linux audit and staging pass read ELF files
through this module, and t/fixtures/elf/, whose build.sh builds the
fixtures the tests read and records what readelf -d and
patchelf --print-rpath say about each one.
AUTHOR
Matt Doughty
COPYRIGHT AND LICENSE
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.