Back to course

The .debug_* Sections

16 minbeginnerModule 1: Why Debug Info ExistsContinues from dwarf-intro
16 min · Module 1: Why Debug Info Exists · Structures

Why This Matters

"Add debug info" is one compiler flag, but it produces at least six separate sections with different jobs, different readers, and different fates. When a build is too large, teams reach for strip without knowing which sections they are discarding — and then a crash report loses the one table it needed.

Knowing the sections separately is what lets you answer practical questions: what did strip actually remove, what can be regenerated and what cannot, and why two string sections exist when one would do.

A Simple Model

Group the six sections by what they answer:

  1. Line info (.debug_line + .debug_line_str): address to source line. This is the one crash reporters need first.
  2. Structure info (.debug_info + .debug_abbrev): the tree of types, functions, parameters and variables. This is what lets a debugger show you a variable's value and its type.
  3. String pools (.debug_str, .debug_line_str): names, referenced by offset, never by inline text.
  4. Index (.debug_aranges): which address ranges belong to which compilation unit, so a tool can skip CUs it does not care about.
Simplified, and the simplification is load-bearing. There is no separate "variable location" section in this list, because for this course's programs there is not one — local variable addresses live inside .debug_info as expression opcodes. At -O2 a fifth section (.debug_rnglists or .debug_loclists) appears. Treat the four groups as a way of finding things, not as a fixed list.

The Actual Detail

Here is the real section table for shape_v5, from readelf -S -W. Read the last three columns carefully — they carry the whole lesson.

  [26] .debug_aranges    PROGBITS  0000000000000000 003036 000030 00      0   0  1
  [27] .debug_info       PROGBITS  0000000000000000 003066 000110 00      0   0  1
  [28] .debug_abbrev     PROGBITS  0000000000000000 003176 0000bf 00      0   0  1
  [29] .debug_line       PROGBITS  0000000000000000 003235 000073 00      0   0  1
  [30] .debug_str        PROGBITS  0000000000000000 0032a8 000111 01  MS  0   0  1
  [31] .debug_line_str   PROGBITS  0000000000000000 0033b9 00005e 01  MS  0   0  1

Three facts fall straight out of those rows:

  1. Every Address is 0000000000000000. No debug section is ever mapped into memory. This is the same conclusion as Addr 0 in the ELF section header table — these sections exist in the file, not in the running process.
  2. Alignment is 1 for all six. These are byte streams, not arrays of machine words. That is unusual for an ELF file and it means you can read them with any tool, on any architecture.
  3. Only the two string sections carry the MS flag and an entry size of 1.MS is SHF_MERGE | SHF_STRINGS: the linker is allowed to deduplicate them across every object file it links. That is why main and int32_t appear once in .debug_str no matter how many object files mention them.

Point 3 explains a design choice that otherwise looks pointless. .debug_line_str was added in DWARF 5 purely so that path strings live in their own mergeable pool, separate from .debug_str. Before v5, shape.c and int32_t shared one pool, so the linker merged the type-name pool into what was really a directory-path pool — both fully occupied, no overlap, pure waste in every single object file.

A Real Example

All three binaries below come from the same shape.c. Only the flags differ.

CommandSize (bytes)Debug sections.symtab
gcc -gdwarf-5 -O0 -o shape_v5 shape.c17256all sixpresent
gcc -g0 -O0 -o shape_g0 shape.c15800nonepresent
strip shape_v514328noneremoved

Two things to notice. -g0 costs 1456 bytes of debug info and leaves symbols alone. strip removes debug info and the symbol table, which is why the stripped binary is smaller than the one built with no debug info at all — 14328 against 15800. Stripping is a different operation from -g0, and it removes strictly more.

Now the string pools, to see the merge flag doing its job. The .debug_str of shape_v5 starts with the compiler's own signature:

00000000: 474e 5520 4332 3320 3135 2e32 2e30 202d  GNU C23 15.2.0 -
00000010: 6d74 756e 653d 6765 6e65 7269 6320 2d6d  mtune=generic -m
00000020: 6172 6368 3d78 3836 2d36 3420 2d67 6477  arch=x86-64 -gdw
00000030: 6172 662d 3520 2d4f 3020 2d66 6173 796e  arf-5 -O0 -fasyn

That whole string is the value of DW_AT_producer in the compilation unit. Note it records -O0 — the optimisation level is part of the debug info, because changing it changes the line table completely. And note it is stored once, at offset 0, and referenced by a 4-byte offset from .debug_info, never copied inline. readelf -p .debug_str prints the whole pool with the offsets a DIE would use:

  [     0]  GNU C23 15.2.0 -mtune=generic -march=x86-64 -gdwarf-5 -O0 ...
  [     b6]  long unsigned int
  [     c8]  __int32_t
  [     d2]  unsigned char
  [     e0]  main
  [     e5]  long int

Try It Yourself

$ gcc -gdwarf-5 -O0 -o shape_v5 shape.c
$ gcc -g0        -O0 -o shape_g0 shape.c
$ cp shape_v5 shape_stripped && strip shape_stripped

$ readelf -S -W shape_v5        | grep debug
$ readelf -S -W shape_g0        | grep debug   # nothing
$ readelf -S -W shape_stripped | grep -E 'debug|symtab'   # nothing

$ ls -l shape_v5 shape_g0 shape_stripped

Then isolate a single section, which is the cleanest way to see that it is just file bytes:

$ objcopy --dump-section .debug_line=dl.bin shape_v5
$ xxd dl.bin

What to look for: the MS flag on exactly the two string sections and nowhere else, and the surprising size ordering in the ls output. Both are explained in the REALITY unit above.

Check Your Understanding

Without looking back: a linker is combining forty object files into one executable. Why does the resulting .debug_str contain one copy of the string unsigned int rather than forty?

Apply It

Your release pipeline strips binaries to save space. Support keeps reporting crashes with ??? where the function name should be, but engineers can still see correct source file and line numbers. Which sections survived, and what single change fixes the rest?

The general habit: when a crash report is half-degraded, that pattern is diagnostic. Working line numbers but missing function names is a specific, known configuration, not a mystery.

Connect

The MS flag is an ELF section-header attribute, so this concept leans on The ELF Section Header Table. The address column that is always zero is the subject of ELF memory mapping: a section with no address is never mapped.

You now know the sections exist and what each is for. But they are not interchangeable — a v4 file and a v5 file lay the same data out differently, and a parser that assumes the wrong one reads garbage. That is next.

Next: DWARF 2, 3, 4 and 5 — what each version changed, and the two specific byte layouts that will silently break a reader.