Run llvm-strip-18 without thinking first, and its default quietly removes every symbol from your only copy. This guide gets you a smaller executable, a separate debug file, and a before-and-after check that shows exactly what it removed. The examples use LLVM 18.1.3 from the installed llvm-18 package.
llvm-strip-18, and an ELF executable or object file that you are allowed to modify.Confirm the binary and version before relying on option spelling:
$ command -v llvm-strip-18
/usr/bin/llvm-strip-18
$ llvm-strip-18 --version
llvm-strip, compatible with GNU strip
Ubuntu LLVM version 18.1.3
Optimized build.
The installed manpage describes llvm-strip as an object stripping tool and says that it is still in active development. It is generally a drop-in replacement for GNU strip, but use the versioned command when a build or release script expects LLVM 18. Do not silently switch tools just because both commands accept a similar option.
Checkpoint: if command -v finds nothing, stop here and fix the package or PATH. Do not copy a similarly named binary from an unrelated LLVM installation.
Use a known input path and record its type and symbols. llvm-readelf-18 is useful when it is installed, while the generic file command is enough to catch an obvious mistake:
$ INPUT='/path/to/app'
$ file "$INPUT"
/path/to/app: ELF 64-bit LSB pie executable, x86-64, ...
$ llvm-readelf-18 -S "$INPUT" | sed -n '1,35p'
$ llvm-nm-18 --debug-syms "$INPUT" | sed -n '1,20p'
Replace /path/to/app with your actual file. If the file is not ELF, do not assume that the ELF-specific results in this guide apply. LLVM strip can also process archives, applying the requested operation to each member, but an archive is not the same thing as a linked executable.
Make a recoverable copy before any in-place operation:
$ cp --preserve=all -- "$INPUT" "${INPUT}.before-strip"
$ sha256sum -- "$INPUT" "${INPUT}.before-strip"
Checkpoint: the two hashes should match. If they do not, stop and find out why before stripping anything. The original input is your recovery copy, so keep it until the stripped result has passed its tests.
Copy the input to a new output path, then remove its debug sections with --strip-debug, also available as -g, -d and -S:
$ cp -- "$INPUT" /path/to/app.release
$ llvm-strip-18 --strip-debug -o /path/to/app.release "$INPUT"
$ printf 'strip status: %s\n' "$?"
strip status: 0
The explicit -o form makes the intended output clear. It also avoids the most dangerous default: unless you provide another output or operation, llvm-strip-18 modifies each input in place. The command above overwrites the copied release file, not the original.
Warning: do not use sudo to make a failed strip look successful. Fix ownership or write the output somewhere you control. A non-zero exit status means an error occurred; it is not a partial-success signal you should ignore.
Verify that the result is still a usable object and compare its sections:
$ file /path/to/app.release
/path/to/app.release: ELF 64-bit LSB pie executable, x86-64, ...
$ llvm-readelf-18 -S /path/to/app.release | grep -E '\.(debug|symtab|strtab)' || true
$ /path/to/app.release --version
$ sha256sum -- "$INPUT" /path/to/app.release
The exact file output and program version are application-specific. A changed hash is expected. The executable must still start and do the job it was built to do. Removing symbols can make later debugging harder even when the program runs correctly.
If you may need symbolic backtraces, produce a debug-only file before creating the small release copy:
$ llvm-strip-18 --only-keep-debug -o /path/to/app.debug "$INPUT"
$ llvm-strip-18 --strip-debug -o /path/to/app.release "$INPUT"
$ file /path/to/app.debug /path/to/app.release
--only-keep-debug writes an output that preserves contents useful for debugging. For ELF, LLVM removes the contents of allocated sections that are not notes by turning them into SHT_NOBITS sections and shrinking program headers where possible. The resulting debug file is not a replacement executable.
The two outputs are useful only if your debugging workflow can associate them with the exact release binary. Store them as build artefacts with matching build identifiers or checksums. Do not distribute a debug file as though it were the runtime binary.
Recovery: retain /path/to/app.debug and test /path/to/app.release separately. If the release is broken, restore the original from ${INPUT}.before-strip or rebuild from the unstripped artefact:
$ cp --preserve=all -- "${INPUT}.before-strip" "$INPUT"
$ sha256sum -- "$INPUT" "${INPUT}.before-strip"
That copy is the undo operation for the workflow here. If you deliberately stripped the original instead, there is no general command that reconstructs removed symbols and section contents. Use the saved copy or rebuild it.
The default deserves attention: when no other strip or remove option is specified, the manpage says --strip-all is enabled. That removes all symbols and eligible non-allocated sections for ELF, while COFF behaviour differs. A default in-place invocation is therefore a destructive release operation, not a harmless size check.
Use the least aggressive operation that meets the release requirement:
For example, removing one known section from a copied file is narrower than stripping every symbol:
$ cp -- "$INPUT" /path/to/app.no-build-notes
$ llvm-strip-18 --remove-section=.comment -o /path/to/app.no-build-notes "$INPUT"
$ llvm-readelf-18 -S /path/to/app.no-build-notes | grep '\.comment' || true
Warning: do not remove a section merely because its name looks optional. Linkers, loaders and other tools can rely on section relationships. --allow-broken-links permits LLVM to remove sections even when invalid section references would remain, setting invalid sh_link fields to zero. Treat that as an exceptional repair decision, not a routine workaround.
When the input is an archive, the requested operation applies to each archive member. Deterministic archive mode is enabled by default, using zero for member UID, GID and timestamp fields. --disable-deterministic-archives or -U restores real values. Leave deterministic mode enabled when reproducible output matters.
Symbol rules can use wildcard syntax, enabled by default for section-related flags. For example, this keeps one named symbol while matching the rest:
$ llvm-strip-18 --wildcard \
--strip-symbol='*' \
--strip-symbol='!important_symbol' \
-o /path/to/app.filtered "$INPUT"
Quote wildcard patterns so the shell does not expand them against files in the current directory. A leading ! prevents a match even when another rule matches. --regex changes symbol and section names to extended POSIX regular expressions and cannot be combined with --wildcard. Test a rule against a copy and inspect the symbol table before using it in a release job.
Run the checks that matter to the program, not only the strip command:
$ test -x /path/to/app.release
$ /path/to/app.release --version
$ llvm-readelf-18 -h /path/to/app.release
$ llvm-nm-18 --debug-syms /path/to/app.release | sed -n '1,20p'
$ printf '%s\n' 'release copy passed the basic checks'
Adapt the smoke test to the application. A successful strip status proves that LLVM wrote an output; it does not prove that the program's plugins, stack traces, packaging metadata or startup path still work. Run the normal test suite and package the debug companion separately when diagnostics are part of your support process.
--strip-debug, --strip-unneeded or --strip-all knowingly.