How to Strip Debug Symbols from Software (and Still Keep GDB Context)
Learn how to use objcopy to strip debug symbols from an ELF executable, protecting sensitive data without losing the ability to debug your code using an external .dbg file and GDB.

When you build your software, the compiler often includes debug information if you pass flags like -g
or -g3
. That information is stored in the various .debug
sections inside the .elf
file. During development, this is essential for debugging, but in production, leaving it in can expose internal details you may not want to share.
Most of the time, when working with embedded software, devices are flashed with .hex
or .bin
files that don’t carry symbols. But if you’re archiving, shipping .elf
files, or working in environments like embedded Linux where the executable itself is deployed, separating debug symbols from the binary is an important step.
Why Bother Stripping Debug Symbols?

The primary drivers for stripping the .elf
are not device flash size reduction, but limiting information leakage and keeping production builds clean.
- Reduce Information Leakage: Debug information includes details like function names, variable names, and internal structure layouts. Shipping an un-stripped
.elf
makes reverse engineering much easier, since these names act like a roadmap to the code’s logic. Stripping symbols adds a basic layer of obfuscation. - Storage Size Reduction (for Filesystems): If the target runs embedded Linux or stores the full
.elf
on a filesystem, stripping symbols can reduce the file size on disk. - Clean Asset Management: Keeping symbol files separate enforces a clear boundary between development builds and production-ready assets. This makes archiving, distribution, and client hand-offs more organized.
Peeking Into the Symbols
What kind of information can we extract from an .elf
file that contains debug sections anyway? Lets start by first listing the sections:
arm-none-eabi-readelf --sections firmware.elf
There are 19 section headers, starting at offset 0x198e8:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 001000 00eb88 00 AX 0 0 4
[ 2] .data PROGBITS 20000000 010000 00002c 00 WA 0 0 4
[ 3] .bss NOBITS 2000002c 01002c 000be4 00 WA 0 0 4
[ 4] ._user_heap_stack NOBITS 20000c10 010c10 000080 00 WA 0 0 1
[ 5] .ARM.attributes ARM_ATTRIBUTES 00000000 01002c 00002d 00 0 0 1
[ 6] .comment PROGBITS 00000000 010059 00001e 01 MS 0 0 1
[ 7] .debug_info PROGBITS 00000000 010077 0006ea 00 0 0 1
[ 8] .debug_abbrev PROGBITS 00000000 010761 000485 00 0 0 1
[ 9] .debug_loclists PROGBITS 00000000 010be6 00033d 00 0 0 1
[10] .debug_aranges PROGBITS 00000000 010f28 0001a0 00 0 0 8
[11] .debug_rnglists PROGBITS 00000000 0110c8 000072 00 0 0 1
[12] .debug_line PROGBITS 00000000 01113a 00093c 00 0 0 1
[13] .debug_str PROGBITS 00000000 011a76 0004b4 01 MS 0 0 1
[14] .debug_frame PROGBITS 00000000 011f2c 00011c 00 0 0 4
[15] .debug_line_str PROGBITS 00000000 012048 000135 01 MS 0 0 1
[16] .symtab SYMTAB 00000000 012180 004b10 10 17 900 4
[17] .strtab STRTAB 00000000 016c90 002b83 00 0 0 1
[18] .shstrtab STRTAB 00000000 019813 0000d4 00 0 0 1
In the example above the ELF contains both DWARF sections (.debug_info
, .debug_abbrev
, .debug_line
, .debug_str
, etc.) and a full symbol table (.symtab
/ .strtab
). The DWARF sections store source-level metadata (types, function/variable DIEs, and the address to source mapping), while the symbol table provides symbol names used by linkers and disassemblers.
For example, using the flag --debug-dump=info
shows a compilation unit, which is generated by compilers to describe the source code while debugging by keeping symbols and its type, scope, file, line number, etc.
$ arm-none-eabi-readelf --debug-dump=info firmware.elf | less
Contents of the .debug_info section:
Compilation Unit @ offset 0:
Length: 0xa0 (32-bit)
Version: 5
Unit Type: DW_UT_compile (1)
Abbrev Offset: 0
Pointer Size: 4
<0><c>: Abbrev Number: 2 (DW_TAG_compile_unit)
<d> DW_AT_producer : (indirect string, offset: 0xc4): GNU C17 14.2.0 -mfloat-abi=soft -mthumb -mfloat-abi=soft -march=armv6s-m -g -Os -fno-builtin -ffunction-sections -fdata-sections
<11> DW_AT_language : 29 (C11)
<12> DW_AT_name : (indirect string, offset: 0x5e): ../../../../../../newlib-4.5.0.20241231/newlib/libc/stdlib/abs.c
<16> DW_AT_comp_dir : (indirect string, offset: 0xe): /build/arm-none-eabi-newlib/src/build-nano/arm-none-eabi/thumb/v6-m/nofp/newlib
That single Compilation Unit already reveals us the toolchain and the flags used in compiling the program, the target architecture and ABI, and exact source/library paths. Combined with the rest of the DWARF data we can extract:
- function names and address ranges
- global and static variable names and locations
- structure and type definitions and field offsets
- address to line mappings (from
.debug_line
), - compiler and build metadata (from
DWATproducer
andDWATcomp_dir
That’s why shipping an un-stripped ELF is risky (°〇°). These sections don’t contain your source text, but they provide a near-complete blueprint of program structure which accelerates reverse engineering. But how do we keep the leakage minimal, but still keep our possibility to debug the program?
Extract and Strip the Binary
The GNU Binutils suite provides the objcopy
utility, which is the standard tool for manipulating sections within an object file. We use it in two distinct steps to extract and then remove the symbols.
1. Extract the debugging symbols into a new file.
arm-none-eabi-objcopy --only-keep-debug firmware.elf target.dbg
2. Strip the final executable of the debugging symbols. This step removes all debug information from the original .elf
file.
arm-none-eabi-objcopy --strip-debug fimrware.elf
The --strip-debug
flag removes all debug sections but generally leaves essential symbols (like function entry points and global variables) that might still be useful for basic stack tracing without full source context. If you want no function names in the shipped binary you can use --strip-all
.
Debugging Using the External Symbol File
When you use the stripped version of the .elf
, you'll find that attempting to debug it normally will will not be as pleasant as before. To be able to debug with the extracted debug symbols file, we need to tell GDB where to find the missing symbols file. Once we have a GDB session to the target, we can load the symbol file like this:
(gdb) symbol-file target.dbg
This command effectively tells GDB: "Here is how to translate the raw memory addresses I see on the target back into meaningful source code lines, function names, and variable labels."
It is vital that the .dbg
file you load into GDB corresponds exactly to the stripped executable currently running on the target. If you recompile the firmware even slightly, the memory addresses shift, and the old .dbg
file becomes invalid.
Finally, if you are debugging within VS Code you can modify your launch.json
file to automatically load the symbol file:
"symbolFiles": [
"${workspaceFolder}/out/target.dbg"
],
And that's it, you now know how to reduce the size of your .elf
files and information leakage. (⌐■_■)

Including External Libraries in CMake Projects
Learn how to use CMake’s FetchContent module to automatically download and integrate libraries like CMSIS into your embedded projects, eliminating the hassle of manual copying and updates.

How to Find the Memory Address of a Symbol in an ELF File
A quick and useful tip for locating symbol memory addresses in ELF files using arm-none-eabi-nm combined with grep—perfect for embedded debugging scenarios like setting up SEGGER RTT or inspecting linker placements and runtime symbols.

Remote Debugging with OpenOCD on Raspberry Pi
Learn how to turn a Raspberry Pi into a remote debugging server for the RP2040 using OpenOCD, a complete with step-by-step setup and instructions for building OpenOCD from source to resolve hardware compatibility issues.
Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.