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.

Andre Leppik

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?

Hmm Patric

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 and DWATcomp_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. (⌐■_■)

Need help with embedded systems development?

Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.