Use objcopy to Strip, Split and Export ELF Files Safely
Run objcopy with no output filename and it will happily overwrite your only copy of a binary through a temporary file and a rename. This guide builds a smaller executable, keeps its debugging information in a separate file, and exports an ELF file as raw binary or Motorola S-record text, all while leaving the original input untouched. Allow about 15 minutes if you already have an ELF executable and the usual Binutils tools installed.
The route
Jump straight to the step you need, or tick off Done means at the end.
Examples describe GNU Binutils 2.42 from Ubuntu package version 2.42-4ubuntu2.10 on this machine. The unprefixed objcopy, aarch64-linux-gnu-objcopy and x86_64-linux-gnu-objcopy are the installed variants, and their target defaults differ. Choose the one that matches the file you are building, not the host architecture you happen to be sitting on.
1. Check the tool and the input
Start with read-only checks. Replace the path with the executable or object file you actually intend to process:
$ objcopy --version
GNU objcopy (GNU Binutils for Ubuntu) 2.42
$ file /path/to/app
/path/to/app: ELF 64-bit LSB pie executable, x86-64, ...
Use readelf -h or objdump -f to confirm the ELF class, machine and entry point. Do not use objcopy itself to discover a file's format by trial and error when a read-only inspection tells you the same thing.
Checkpoint
You have a readable input file and know whether it is a fully linked executable or a relocatable object. The manual warns that fully linked files can generally move between supported formats, while converting relocatable objects between formats may not produce what you expect.
2. Make a separate debug file first
Keep the original executable as a recovery copy, then extract its debugging sections into a new file:
$ cp --preserve=all /path/to/app /path/to/app.full
$ objcopy --only-keep-debug /path/to/app.full /path/to/app.debug
$ file /path/to/app.debug
/path/to/app.debug: ELF 64-bit LSB shared object, ..., with debug_info, not stripped
--only-keep-debug clears the contents of sections that would normally be stripped and keeps the debugging sections behind. The result is meant to accompany a stripped executable, not replace one. Exact file wording varies with the file and installed version.
This step creates a new file and leaves app.full alone. Keep both until the stripped result has been tested. For anything valuable, put the copy in a controlled build directory and record its checksum before making further changes.
3. Strip a copy, never an irreplaceable original
Remove debugging symbols and sections from the copy:
$ objcopy --strip-debug /path/to/app.full /path/to/app.stripped
$ file /path/to/app.stripped
/path/to/app.stripped: ELF 64-bit LSB pie executable, x86-64, ..., not stripped
Here is the surprise: not stripped in that file output just means ordinary symbols remain, it says nothing about whether debug sections are gone. Check the sections directly instead:
$ readelf -S /path/to/app.stripped | grep -E 'debug|gnu_debuglink'
$ readelf -S /path/to/app.full | grep debug
The first command should list none of the normal .debug_* sections, the second should list them if the original was built with debug information. An empty result from the first command is exactly what you want at this point.
Warning
objcopy input with no output filename replaces the input using a temporary file and a destructive rename. Always give it a new output path until you have a verified backup. If a command produces a bad result, discard the new file and restore from app.full, do not try to repair a damaged binary in place.
4. Add a link to the separate debug file
Add a GNU debug-link section so debuggers can associate the stripped executable with its companion:
$ cd /path/to
$ objcopy --add-gnu-debuglink=app.debug app.stripped
$ readelf -S app.stripped | grep gnu_debuglink
[28] .gnu_debuglink PROGBITS ...
The referenced debug file must already exist when this command runs. Using a relative name such as app.debug also records the name without a build-directory path, useful once the executable and debug file are installed in recognised debug-file locations. The section carries a checksum of the debug file, so swapping in unrelated contents will not satisfy the link.
This command changes app.stripped. To undo it, regenerate that file from app.full with --strip-debug, then add the link again only after checking the debug file. There is no separate inverse operation here.
5. Export a raw image or S-record
Reach for an explicit output target when another tool needs a firmware image rather than an ELF file. A raw binary is a memory dump of the copied sections starting at the lowest copied load address; symbols and relocation information are discarded entirely:
$ objcopy -O binary /path/to/app.stripped /path/to/app.bin
$ file /path/to/app.bin
/path/to/app.bin: data
The raw output has no ELF header, so file may report only data. Check its size and compare the intended address range against the linker's memory map before programming hardware with it:
$ stat --format='%n %s bytes' /path/to/app.bin
/path/to/app.bin 123456 bytes
For a Motorola S-record file, use the srec target instead:
$ objcopy -O srec /path/to/app.stripped /path/to/app.srec
$ head -n 2 /path/to/app.srec
S0...
S1...
Exact records and addresses depend on the input sections. To keep only selected sections, repeat --only-section=PATTERN, for example --only-section=.text and --only-section=.rodata. Check the result carefully, the manual warns that selecting sections incorrectly can make the output unusable. Do not combine --only-section and --remove-section, their combined behaviour is undefined.
6. Keep architecture and endianness assumptions visible
objcopy reads and writes many BFD formats, but it cannot generally change the endianness of an input file. A format with endianness must land in an output format with the same endianness, or one with none at all, such as S-record. --reverse-bytes reorders bytes within groups for a specific conversion, it is not a general architecture conversion and belongs only where the receiving format explicitly requires it.
When the input format cannot be identified automatically, set it with --input-target=NAME, and set the destination with --output-target=NAME. For architecture-less raw input you are turning into an object file, --binary-architecture=ARCH supplies the output architecture and creates _binary_..._start, _binary_..._end and _binary_..._size symbols. Confirm available names with:
$ objcopy --info
BFD header file version (GNU Binutils for Ubuntu) 2.42
...
Do not infer a valid target just because a command exited successfully. Inspect the output with file, readelf -h, or the receiving tool, and keep the original ELF until that check passes.
Done means
- Checked the installed Binutils version and input ELF format.
- Created a debug file before the executable was stripped.
- Wrote the stripped executable to a new path and inspected its sections.
- Linked the debug-link section to the matching debug file, checkable by the debugger via its checksum.
- Inspected raw or S-record output for the expected size, format and address range.
- Kept the original input and a full recovery copy available throughout.