How to tell whether a binary is stripped (and what it costs you)
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
| Section | Stripped? | Why |
|---|---|---|
.symtab / .strtab | removed | local and debug symbols — the linker’s view |
.debug_* | removed | DWARF line and type information |
.dynsym / .dynstr | kept | the loader needs them to resolve imports |
.gnu.version_* | kept | symbol versioning |
.note.gnu.build-id | usually kept | identifies the exact build for debuginfod |
.eh_frame / .eh_frame_hdr | kept | C++ 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
.textwith a large.data— the real code is unpacked at runtime - An unusual number of
PT_LOADsegments with executable+writable flags - Imports of
mprotect,mmapwithPROT_EXEC,dlopenfrom 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.