guides / is-binary-stripped scenariosupdated 2026-10-25 readelf -h ./app

How to tell whether a binary is stripped (and what it costs you)

2026-10-25 By luckyray Debian 12 · Ubuntu 24.04 · Alpine 3.20

Stripping a binary removes its symbol table. It is routine, expected, and almost always correct for anything shipped to production — and it is also the reason a stack trace from production looks like a list of addresses.

Confirming it in one command

$ file /usr/bin/nginx
/usr/bin/nginx: ELF 64-bit LSB pie executable, x86-64, ..., stripped

file reports stripped or not stripped. To see the structural reason:

$ readelf -S /usr/bin/nginx | grep -E 'symtab|dynsym'
  [ 5] .dynsym           DYNSYM           0000000000003b20 ...

.dynsym is present, .symtab is absent. That is exactly what strip does.

What strip removes, and what it must keep

SectionStripped?Why
.symtab / .strtabremovedlocal and debug symbols — the linker’s view
.debug_*removedDWARF line and type information
.dynsym / .dynstrkeptthe loader needs them to resolve imports
.gnu.version_*keptsymbol versioning
.note.gnu.build-idusually keptidentifies the exact build for debuginfod
.eh_frame / .eh_frame_hdrkeptC++ exception unwinding and backtraces

The distinction in the first three rows is the thing to internalise. A stripped binary still has names for everything it imports and exports. What it loses is the internal functions — the ones that would name your frames in a backtrace.

Does it matter?

Usually for three reasons, in this order:

1. Stack traces. Without .symtab and without line information, a crash in production gives you nginx + 0x4a1c3e. With a matching debuginfo package, addr2line turns that into ngx_http_core_find_config_phase + 0x3e / src/http/ngx_http_core_module.c:1187. The usual mechanism is build-id:

$ readelf -n /usr/bin/nginx | grep -A1 'Build ID'
    Build ID: 4f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a

That hash identifies the exact binary. Distributions publish matching -dbgsym / -debuginfo packages, and debuginfod fetches them automatically if the client is configured.

2. Security review. nm -D on a stripped binary still shows imported functions, which is often enough to draw conclusions: an executable importing system and popen but not snprintf is worth a closer look. Absence of local symbols does not hide the import table.

3. Nothing, if you are just running it. Stripping does not make software faster or safer. It makes the file smaller — often 30–60% smaller for C++ with templates — and that is the entire benefit.

Stripped is not obfuscated

A recurring misreading: “the binary is stripped, so it is hiding something.” Distribution packages are stripped. Every -release build of every Go and Rust project is stripped by default. The absence of .symtab is a build hygiene decision, not evidence of anything.

The actual signals of obfuscation are different and detectable:

  • Sections whose entropy is near 8 bits per byte throughout (packed or encrypted payload)
  • A tiny .text with a large .data — the real code is unpacked at runtime
  • An unusual number of PT_LOAD segments with executable+writable flags
  • Imports of mprotect, mmap with PROT_EXEC, dlopen from a string built at runtime

If you want to check the first one, the Strings & Symbols extractor gives you a fast read on whether readable paths and library names exist at all, and the ELF Inspector shows segment permissions — a PT_LOAD that is both writable and executable is a legitimate finding.

Getting symbols back

Distributions: install the debug package, keyed by build-id.

# Debian/Ubuntu
sudo apt install nginx-dbgsym            # needs a debug repo enabled
# RHEL family
sudo debuginfo-install nginx
# or fetch by build-id directly
debuginfod-find debuginfo 4f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a

Your own builds: do not ship the symbols, but do archive the unstripped binary per release and keep it keyed by build-id. That is the difference between a five-minute incident and a two-hour one.

objcopy --only-keep-debug app app.debug
strip --strip-debug --strip-unneeded app
objcopy --add-gnu-debuglink=app.debug app

--add-gnu-debuglink records the debug file’s name and CRC in a .gnu_debuglink section, so gdb finds it automatically when it sits next to the binary.

Go binaries: -ldflags="-s -w" strips both the symbol table and DWARF. Drop -w and you keep DWARF, which is usually the better trade for a service you will have to debug:

go build -ldflags="-s" -o app ./cmd/app

A note on strip and reproducibility

Stripping changes the file hash, so two builds that differ only in whether they were stripped will have different checksums. If you publish checksums for download verification, document which variant you publish. Conversely, if you are diffing two releases and the only difference is symbol count, check whether one of them was stripped before concluding the code changed — compare .text sizes rather than total file size.


Every command in this guide was run against the distribution named in the output. If your output differs, the mechanism section explains why — and the ELF Inspector shows the same fields without needing a shell.

Related

guides
  1. How to check whether a binary is 32-bit or 64-bit (and for which CPU) Four commands that tell you the word size, instruction set and ABI of any executable — plus why file's one-line answer is sometimes all you need.
  2. How to check PIE, RELRO, NX and stack canaries without installing checksec Five readelf commands that reproduce a checksec report, what each field really proves, and the cases where the answer is misleading.
  3. cannot execute binary file: Exec format error — causes and fixes Why Linux refuses to run a binary with Exec format error, how to confirm the cause in three commands, and the five fixes that actually apply.