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.