Inspect ELF Binaries with llvm-readelf-20

Before you blame the linker or the loader, look at the file itself: Ubuntu's LLVM 20 llvm-readelf-20 inspects an ELF executable without modifying it. The examples cover the file header, sections, symbols, dynamic dependencies and machine-readable JSON. Allow about 15 minutes if the binary is already on the machine. Everything here is read-only and normally needs no elevated privileges.

1. Check the installed command

This guide targets the llvm-20 package. The installed executable on this machine reports LLVM 20.1.8. Check your own path and version before copying output into a script, because option sets and output details can differ between LLVM releases.

$ command -v llvm-readelf-20
/usr/bin/llvm-readelf-20
$ llvm-readelf-20 --version
Ubuntu LLVM version 20.1.8
  Optimized build.

The command accepts one or more object-file names. It reads an input named - from standard input, but standard input must contain a supported object format. A normal shell prompt is not an input file, so always put the filename after the options.

Checkpoint: command -v resolves the intended executable and --version identifies the installed release.

2. Read the ELF file header

Start with -h, also spelled --file-header. It shows the ELF class, byte order, machine, entry point and the locations and sizes of the program and section header tables.

$ llvm-readelf-20 --file-header /path/to/program
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x6D30

Your values will depend on the file. The first four magic bytes, 7f 45 4c 46, identify ELF. Do not interpret DYN as meaning the file is only a shared library: position-independent Linux executables commonly use that ELF type too. The header describes the file; it does not run it.

3. Map sections and segments

Use -S or --sections to list sections such as .text, .rodata, symbol tables and relocation data. This is useful when a linker, packer or build step has produced an unexpected layout.

$ llvm-readelf-20 --sections /path/to/program | sed -n '1,18p'
There are 31 section headers, starting at offset 0x22428:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000318 000318 00001c 00   A  0   0  1
  [ 2] .note.gnu.property NOTE           0000000000000338 000338 000030 00   A  0   0  8
  [ 6] .dynsym           DYNSYM          0000000000000400 000400 000c00 18   A  7   1  8

For the loadable view, use -l or --program-headers to display segments. Sections are linker and analysis units; segments are the parts the loader maps into memory. Comparing both views can explain why a section exists on disk but does not have a separate runtime mapping.

The combined -e or --headers option requests the file header, program headers and sections together. It is a useful first capture, but it can be noisy. Prefer the narrower command when you are recording a focused diagnostic.

4. Inspect symbols, relocations and dependencies

Use -s for the symbol table and -r for relocations. On ELF files in the default GNU output style, -s also displays the dynamic symbol table. Add -C or --demangle when C++ names make the output hard to read.

$ llvm-readelf-20 --symbols --demangle /path/to/program | sed -n '1,16p'

Symbol table '.dynsym' contains ... entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.34

The counts, addresses and names are file-specific, and the exact table heading can vary. A symbol marked UND is undefined in this file and is normally resolved from another object at link or load time. It is evidence about references, not proof that a particular library is installed or currently loaded.

For the dynamic table, use -d or --dynamic-table. For a shorter dependency-oriented view, use --needed-libs. These options inspect metadata only. They do not invoke the dynamic loader and do not install, unload or relink anything.

5. Dump one section when you need its contents

Use -x or --hex-dump with a section name or index for bytes. Use -p or --string-dump for strings. The argument is a comma-separated list, so quote it if you build it from a variable.

$ llvm-readelf-20 --string-dump=.comment /path/to/program

String dump of section '.comment':
  [     0]  GCC: (Ubuntu 14.2.0-4ubuntu2) 14.2.0

A section may not exist in every binary. Confirm its name with -S first. For compressed section data, -z or --decompress applies when used with -x or -p; without one of those dump options it has nothing to decompress. Treat dumped bytes and strings as untrusted data, especially when they came from an unknown binary.

6. Produce JSON for a repeatable check

Set --elf-output-style=JSON when another tool needs structured ELF information. The installed manual documents three styles: LLVM, GNU (the default) and JSON. Use --pretty-print with JSON when a person needs to review it.

$ llvm-readelf-20 --elf-output-style=JSON --file-header /path/to/program \
    > /tmp/program-readelf.json
$ python3 -m json.tool /tmp/program-readelf.json > /dev/null
$ sed -n '1,8p' /tmp/program-readelf.json
[{"FileSummary":{"File":"/path/to/program","Format":"elf64-x86-64","Arch":"x86_64","AddressSize":"64bit"},"ElfHeader":{...}}]

Do not parse the human-oriented GNU output with fragile column positions when JSON is available. Still validate the JSON and handle fields that are absent for a particular file. If the command exits non-zero, treat the capture as failed rather than consuming a partial file.

7. Handle errors without changing the binary

llvm-readelf-20 returns zero for normal operation and a non-zero status after an error. Test the status directly after a command when it matters:

$ llvm-readelf-20 --file-header /path/to/program
$ status=$?
$ printf 'exit status: %s\n' "$status"
exit status: 0

If you see no input files specified, the filename was omitted. If an input is not an object file, the command reports an error rather than producing a meaningful header. Check the path and type without using sudo:

$ test -r /path/to/program && echo readable
$ file /path/to/program
$ llvm-readelf-20 --file-header /path/to/program
llvm-readelf-20: error: ...

Recovery: the final error text depends on the input and release. Do not respond by editing or truncating the file. If permissions genuinely block reading, ask the file owner to provide a readable copy or use the normal, authorised access path. Elevated privileges are unnecessary for ordinary inspection and should not be used to bypass an access boundary casually.

Redirection creates or truncates its destination before the command runs. When saving a diagnostic, write to a new temporary pathname and inspect the exit status before replacing an existing report. The examples above write only under /tmp; remove that temporary JSON later if it contains sensitive paths or symbols.

Done means