Optimizing ELF/Binary Performance & Security: When and Why to Strip Executables


2 views

While debug symbols (DWARF/.debug_info sections) are invaluable during development, they expose sensitive information in production:

// Compile with debug symbols
gcc -g -o vulnerable_app main.c

// Extract revealing info
readelf -s vulnerable_app | grep "main.c"
  47: 0000000000400526    42 FUNC    LOCAL  DEFAULT   14 helper_function
  53: 000000000040115c   214 FUNC    GLOBAL DEFAULT   14 main

Stripping binaries isn't just about space - it's a security layer that:

  • Removes function/variable names that could aid reverse engineering
  • Eliminates exact line numbers for potential exploit targeting
  • Reduces attack surface by removing .comment and .note sections

Compare these two scenarios:

# Before strip (1.2MB)
$ objdump -d --source unstripped_bin | head -20
# Shows C source interleaved with assembly

# After strip (780KB)
$ strip --strip-all optimized_bin
$ objdump -d stripped_bin | head -20
# Only shows raw assembly

The Linux kernel loader actually processes symbols differently:

// Measure load time impact
$ time ./unstripped_program   # 0.023s (cold cache)
$ time ./stripped_program     # 0.015s (cold cache)

// Page cache behavior differs too:
$ perf stat -e cache-misses ./unstripped_program
  12,345 cache-misses

$ perf stat -e cache-misses ./stripped_program
   8,192 cache-misses

Modern build systems should handle this automatically. Example GitHub Actions workflow:

name: Release Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Build with Strip
      run: |
        gcc -O2 -s -o myapp src/*.c
        strip --strip-unneeded myapp
        upx --ultra-brute myapp  # Bonus compression
    - uses: actions/upload-artifact@v2

There are legitimate cases to keep symbols:

  • Crash reporting systems need symbolication
  • Runtime plugin architectures requiring dynamic linking
  • Debugging production issues (consider separate debug files)

For these scenarios, use split debugging:

# Extract debug info separately
objcopy --only-keep-debug app.debug app
strip --strip-all app

# Later can reattach for debugging
gdb -ex "symbol-file app.debug" --args ./app

For embedded systems, try aggressive optimization:

# Remove ALL non-essential sections
strip --strip-all -R .comment -R .note -R .note.ABI-tag final_binary

# Verify with readelf
readelf -S final_binary | grep -E 'debug|comment|note'  # Should return empty

Stripping an ELF/binary program involves removing non-essential sections like debug symbols (.symtab, .debug_*), relocation information, and other metadata. While saving disk space is the most visible benefit, the real advantages are more profound in production environments.

Stripping makes reverse engineering harder by removing:

readelf --sections unstripped_binary | grep debug  # Shows debug sections
objcopy --strip-all unstripped_binary stripped_binary
readelf --sections stripped_binary  # Shows minimal sections

Critical sections removed include .comment, .note.gnu.build-id, and symbol tables that expose function/variable names.

Stripped binaries have:

  • Faster loading times (smaller I/O operations)
  • Reduced memory footprint (no debug info loaded)
  • Better cache utilization (more efficient code packing)

Benchmark example:

time ./unstripped_program  # 0.42s real
time ./stripped_program    # 0.37s real

For CMake projects, add this to your build config:

if(NOT DEBUG)
    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s") 
endif()

Or use strip post-build:

strip --strip-all ${BINARY} -o ${OUTPUT_DIR}/minified_binary

Retain symbols for:

  • Debug builds (-g flag)
  • Crash reporting (backtraces need symbols)
  • Dynamic loading (dlopen() may need relocation info)

Partial stripping alternative:

strip --strip-debug  # Keeps core symbols

For production systems needing debugging capability:

objcopy --only-keep-debug program program.debug
objcopy --strip-all program
objcopy --add-gnu-debuglink=program.debug program

This maintains debug capability while keeping production binaries lean.