BP-DEBUGGING, stopping the compiler and looking at it¶
Status: complete
Applies to: GCC 16.2.0 (tag releases/gcc-16.2.0)
Target-dependent: no
Generated sections: 2
Last verified: 2026-09-06 against releases/gcc-16.2.0
This document specifies the interfaces GCC provides for inspecting a compiler while it is running, and the two non interactive substitutes for doing so. It covers the .gdbinit that configure writes, the twenty six commands and four Python commands in it, the seventeen pretty printers, the debug counters behind -fdbg-cnt, and the pass selection options that decide what runs at all.
1. Purpose and scope¶
There are three ways to find out what a GCC pass did. Read a dump, which is BP-PIPELINE. Turn the pass off partway through and see when the symptom appears, which is -fdbg-cnt. Or stop the compiler in the middle of the pass and look at the data structures, which needs a debugger, a compiler built so a debugger can see into it, and the hooks in this document.
The subject of this document is the third of those, plus the second, because they answer the same question and the second is often the faster way to answer it.
The unit is a single cc1 process. Nothing here is about the driver, which is BP-DRIVER, except for the one driver option that puts a debugger in front of the compiler. Nothing here is about debug information in the code GCC produces: the DWARF that lets somebody debug their own program is a different subsystem with a different specification, and the fact that both are called debugging is the single most common confusion about this area. This document is about debugging GCC. It is not about debugging with GCC.
Inputs and outputs, in the terms the other blueprints use: none. This component does not transform the IR, requires no PROP_* flag and destroys none. It reads the IR and prints it. That is the reason it can be called from a debugger prompt at any point in the pipeline, and it is the reason a broken printer produces wrong output rather than a wrong compilation.
What this document does not cover: the plugin API, which is BP-PLUGIN and is the supported way to run code inside the compiler rather than to look at it; the test suite, which is BP-TESTSUITE; and valgrind, rr and the sanitizers, which are general tools that GCC accommodates rather than interfaces GCC defines. The one exception is gcc/gdbasan.in, which configure adds to .gdbinit when the compiler was itself built with -fsanitize=address, and which is named here because a reader who finds it will otherwise not know where it came from.
2. Data structures¶
The debugger hooks own no data. Everything below is a list GCC keeps in machine readable form, and all of it is generated from the pinned tree rather than typed, because all three lists change every release and none of them appears in GCC's manual.
2.1 Where .gdbinit comes from¶
configure writes it, not make. The lines are at gcc/configure.ac:7404@releases/gcc-16.2.0, and they run in the gcc subdirectory of the build tree, which is where the file lands.
dir .
dir SRCDIR
source SRCDIR/gdbinit.in
python import sys; sys.path.append('SRCDIR'); import gdbhooks
SRCDIR is the absolute path of the gcc directory of the source tree at the time configure ran. Two consequences follow and both of them bite.
The first is that the file is generated per build tree and is not installed. A debugger started anywhere other than that directory loads none of it, and an installed compiler has no .gdbinit at all. Every command in this document exists only for somebody standing in the build tree that produced the binary.
The second is that the path is absolute and baked in. A build tree moved to another directory, or copied into a container, has a .gdbinit whose dir and source lines point at a source tree that is not there, and gdb reports each one as a separate error at startup.
When the compiler was configured with -fsanitize=address in CFLAGS, a fourth line sources gcc/gdbasan.in, which sets a breakpoint on __asan_report_error (gcc/configure.ac:7423@releases/gcc-16.2.0).
2.2 The commands in gdbinit.in¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
gcc/gdbinit.in defines 26 commands. The build installs it as .gdbinit in the gcc subdirectory of the build tree, so they exist for a debugger started from there and nowhere else. 16 of them work by calling a function inside the compiler, which needs a process that is stopped somewhere the call can succeed; the rest read memory and work on a core file. The last column is the command's own document block, which is the only documentation any of them has, and the column before it is what that block claims the command is equivalent to. Where the claim and the body disagree, the body is what runs.
| Command | Alias | Equivalent to | Calls into the compiler | What it prints |
|---|---|---|---|---|
help-gcc-hooks |
gdb alone | no | GCC gdbinit file introduces several debugging shorthands: | |
pp |
debug (<multiple overloads>) |
yes | Print a representation of any GCC data structure for which an instance of overloaded function 'debug' is available. | |
pr |
debug_rtx (rtx) |
yes | Print the full structure of given rtx. | |
prl |
debug_rtx_list (rtx) |
yes | Print the full structure of all rtx insns beginning at given rtx. | |
pt |
debug_tree (tree) |
yes | Print the full structure of given tree. | |
pct |
debug_c_tree (tree) |
yes | Print given tree in C syntax. | |
pgg |
debug_gimple_stmt (gimple) |
yes | Print given GIMPLE statement in C syntax. | |
pgq |
debug_gimple_seq (gimple_seq) |
yes | Print given GIMPLE sequence in C syntax. | |
pgs |
debug_generic_stmt (tree) |
yes | Print given GENERIC statement in C syntax. | |
pge |
debug_generic_expr (tree) |
yes | Print given GENERIC expression in C syntax. | |
phrs |
debug_hard_reg_set (HARD_REG_SET) |
yes | Print given HARD_REG_SET. | |
pmz |
mpz_out_str (mpz_t) |
yes | Print given mpz value. | |
ptc |
TREE_CODE (tree) |
no | Print the tree-code of given tree node. | |
pdn |
IDENTIFIER_POINTER (DECL_NAME (tree)) |
no | Print the name of given decl-node. | |
ptn |
IDENTIFIER_POINTER (DECL_NAME (TREE_TYPE (tree))) |
no | Print the name of given type-node. | |
pdd |
debug_dwarf_die (dw_die_ref) |
yes | Print given dw_die_ref. | |
prc |
GET_CODE (rtx) |
no | Print the rtx-code and machine mode of given rtx. | |
pi |
X0EXP (rtx_insn) |
no | Print the fields of given RTL instruction. | |
pbs |
print_binding_stack () |
yes | In cc1plus, print the current binding stack, frame by frame, up to and including the global binding level. | |
pbm |
bitmap_print (bitmap) |
yes | Dump given bitmap as a comma-separated list of numbers. | |
pel |
expand_location (location_t) |
yes | Print given location. | |
pcfun |
debug_function () |
yes | Print current function. | |
trt |
TREE_TYPE (tree) |
no | Print TREE_TYPE of given tree node. | |
break-on-diagnostic |
breakpoint on diagnostics::context::maybe_show_locus |
no | Put a breakpoint on diagnostics::context::show_locus, called whenever a diagnostic is emitted (as opposed to those warnings that are suppressed by command-line options). | |
break-on-saved-diagnostic |
breakpoint on ana::diagnostic_manager::add_diagnostic |
no | Put a breakpoint on ana::diagnostic_manager::add_diagnostic, called within the analyzer whenever a diagnostic is saved for later de-duplication and possible emission. | |
reload-gdbhooks |
rh |
gdb alone | no | Load the gdbhooks.py module again in order to pick up any changes made to it. |
2.3 What the file does before any command is typed¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
Outside the command definitions, the file sets 4 breakpoints, changes 4 debugger settings, and puts 4 headers and 17 functions on the skip list. All of it happens at startup, before the reader has typed anything.
| What | Value | Why it is there |
|---|---|---|
| breakpoint | fancy_abort |
GCC's own abort, which every failed assertion goes through |
| breakpoint | internal_error |
where an ICE is reported, so the debugger stops with the stack intact |
| breakpoint | exit |
so a compiler that decided to leave does not take the session with it |
| breakpoint | abort |
the library one, in case something reached it without going through GCC's |
| setting | set pagination off |
so the skip output below does not stop for a keypress |
| setting | set unwindonsignal on |
so a call that crashes unwinds instead of leaving the process wedged |
| setting | set complaints 0 |
so gdb stops reporting symbols it does not understand in this binary |
| setting | set check type off |
so an inferior call can be made without casting an address first |
The skip list is what stops step disappearing into an accessor. The headers skipped whole are tree.h, is-a.h, line-map.h, timevar.h, and the named functions are rtx_expr_list::next, rtx_expr_list::element, rtx_insn_list::next, rtx_insn_list::insn, rtx_sequence::len, rtx_sequence::element, rtx_sequence::insn, INSN_UID, PREV_INSN, SET_PREV_INSN, NEXT_INSN, SET_NEXT_INSN, BLOCK_FOR_INSN, PATTERN, INSN_LOCATION, INSN_HAS_LOCATION, JUMP_LABEL_AS_INSN.
2.4 The commands in gdbhooks.py¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
gcc/gdbhooks.py also defines 4 commands of its own, in Python. They are separate from the shorthands above because a shorthand is a canned call and these are programs: one of them reads passes.def off disk to complete a pass name, and two of them write a dump to a file and open it.
| Command | Class | What it is for |
|---|---|---|
break-on-pass |
BreakOnPass |
A custom command for putting breakpoints on the execute hook of passes. |
dump-fn |
DumpFn |
A custom command to dump a gimple/rtl function to file. |
gcc-dot-cmd |
GCCDotCmd |
This parameter controls the command used to render dot files within GCC's dot-fn command. |
dot-fn |
DotFn |
A custom command to show a gimple/rtl function control flow graph. |
2.5 The pretty printers¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
gcc/gdbhooks.py registers 17 pretty printers, and gdbinit.in loads it. They are what makes print stmt produce a GIMPLE statement rather than a pointer, and they are the reason a debugger session against cc1 looks nothing like one against a program with no hooks installed. A printer is a Python class that reads the same fields a human would, so a printer can be wrong in a way the compiler is not.
| Printer | Applies to | Class |
|---|---|---|
tree |
tree, const_tree |
TreePrinter |
symtab_node |
cgraph_node *, varpool_node *, symtab_node * |
SymtabNodePrinter |
cgraph_edge |
cgraph_edge * |
CgraphEdgePrinter |
ipa_ref |
ipa_ref * |
IpaReferencePrinter |
dw_die_ref |
dw_die_ref |
DWDieRefPrinter |
gimple |
gimple, gimple *, gimple_cond, const_gimple_cond, gimple_statement_cond *, gimple_debug, const_gimple_debug, gimple_statement_debug *, gimple_label, const_gimple_label, gimple_statement_label *, gimple_switch, const_gimple_switch, gimple_statement_switch *, gimple_assign, const_gimple_assign, gimple_statement_assign *, gimple_bind, const_gimple_bind, gimple_statement_bind *, gimple_phi, const_gimple_phi, gimple_statement_phi * |
GimplePrinter |
basic_block |
basic_block, basic_block_def * |
BasicBlockPrinter |
ana::supernode |
ana::supernode *, const ana::supernode * |
AnaSupernodePrinter |
ana::exploded_node |
ana::exploded_node *, dedge<ana::eg_traits>::node_t * |
AnaExplodedNodePrinter |
edge |
edge, edge_def * |
CfgEdgePrinter |
rtx_def |
rtx_def * |
RtxPrinter |
opt_pass |
opt_pass * |
PassPrinter |
vec |
anything matching vec<(\S+), (\S+), (\S+)> \* |
VecPrinter |
opt_mode |
anything matching opt_mode<(\S+)> |
OptMachineModePrinter |
opt_mode |
opt_scalar_int_mode, opt_scalar_float_mode, opt_scalar_mode |
OptMachineModePrinter |
pod_mode |
anything matching pod_mode<(\S+)> |
MachineModePrinter |
pod_mode |
scalar_int_mode_pod, scalar_mode_pod |
MachineModePrinter |
A loop after the last of these registers one more printer for each of scalar_mode, scalar_int_mode, scalar_float_mode and complex_mode. It is not in the table because its arguments are a loop variable, and it is the reason the count above is lower than the number of types gdb ends up knowing about.
2.6 The debug counters¶
Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.
gcc/dbgcnt.def declares 75 debug counters. Each one is a call to dbg_cnt (name) somewhere in a pass, which returns true until the counter passes the limit given by -fdbg-cnt=name:limit and false afterwards. Setting a limit turns a pass off partway through, which makes a miscompilation bisectable without a debugger: halve the limit until the smallest number that still produces bad code is found, and that is the transformation to look at. -fdbg-cnt-list prints the list from the compiler itself.
| Counters |
|---|
asan_use_after_scope, auto_inc_dec, back_thread1, back_thread2 |
back_threadfull1, back_threadfull2, ccp, cfg_cleanup |
cprop, cse2_move2add, dce, dce_fast |
dce_ud, delete_trivial_dead, devirt, df_byte_scan |
dom_unreachable_edges, dse, dse1, dse2 |
ext_dce, form_fma, gcse2_delete, gimple_unroll |
global_alloc_at_func, global_alloc_at_reg, graphite_scop, hoist |
hoist_insn, ia64_sched2, if_after_combine, if_after_reload |
if_conversion, if_conversion_tree, if_to_switch, ipa_attr |
ipa_cp_bits, ipa_cp_values, ipa_cp_vr, ipa_mod_ref |
ipa_mod_ref_pta, ipa_sra_params, ipa_sra_retvalues, ira_move |
ivopts_loop, late_combine, lim, local_alloc_for_sched |
loop_unswitch, match, merged_ipa_icf, phiopt_edge_range |
postreload_cse, pre, pre_insn, prefetch |
registered_jump_thread, sched2_func, sched_breakdep, sched_func |
sched_insn, sched_region, sel_sched_cnt, sel_sched_insn_cnt |
sel_sched_region_cnt, sms_sched_loop, split_for_sched2, store_merging |
store_motion, stv_conversion, tail_call, tree_sra |
treepre_insert, vect_loop, vect_slp |
2.7 The state a debug counter keeps¶
Four arrays, one entry per counter, all file static in gcc/dbgcnt.cc, all living for the whole compilation.
record Counter
name symbol
count integer how many times dbg_cnt has been called for it
limits sequence of Range still to be reached, highest first
original_limits sequence of Range what the option said, kept for the listing
record Range
low integer
high integer
count is never reset. It counts calls across every function in the translation unit, in the order the functions are compiled, which is what makes a counter value a position in the whole compilation rather than a position in a pass.
limits is consumed. A range whose upper end has been reached is popped, so the sequence shrinks as compilation proceeds, and original_limits exists because -fdbg-cnt-list has to print what was asked for rather than what is left (gcc/dbgcnt.cc:235@releases/gcc-16.2.0).
The sort is descending, by the low end of each range (gcc/dbgcnt.cc:111@releases/gcc-16.2.0). The consequence is that limits is always read from its last element, which is the lowest range not yet finished.
3. Algorithms¶
3.1 Getting a debugger in front of cc1¶
The compiler a reader wants to debug is not the program they run. gcc is a driver that computes a command line and executes it, so a breakpoint set on gcc stops in the wrong program. There are two ways through, and they differ in whether the driver stays involved.
function debug_target (file: Path, options: sequence of String) -> Session
complexity: O(1)
# First way: ask the driver what it would run, then run that under gdb yourself.
printed = run("gcc", ["-###"] + options + [file])
line = the first line of printed naming a program under libexec
return gdb(program: line.program, args: line.arguments)
# Second way: leave the driver in charge and tell it to prefix every subprocess.
return run("gcc", ["-wrapper", "gdb,--args"] + options + [file])
The first way gives a command line that can be edited, re-run and pasted into a bug report, and it is the one this document recommends. -### prints the commands with every argument quoted and runs none of them, which is the difference between it and -v.
The second way is gcc/common.opt:3988@releases/gcc-16.2.0, a driver option whose value is a comma separated command that is inserted in front of whatever the driver was about to execute. The insertion is at gcc/gcc.cc:3290@releases/gcc-16.2.0, and it happens for every subprocess, so a compilation that also runs as and collect2 puts a debugger in front of those too.
There is a third route, -v, which prints the same command lines while also running them. It is the wrong tool for this because the arguments it prints are not quoted, so a command line containing a shell metacharacter cannot be pasted back.
3.2 Stopping on one pass¶
execute_one_pass runs for every pass and every function, several hundred thousand times in a real compilation, so a plain breakpoint on it is not usable. Three ways to narrow it, in increasing order of how much they need to be true.
function stop_on_pass (name: String) -> Breakpoint
complexity: O(1)
# A: a conditional breakpoint at the point where the pass is known and nothing has run.
return breakpoint(at: "passes.cc:2597", when: streq(pass.name, name))
# B: the pass object's own execute method. The argument is the class name, which is
# not the pass name, and the class has to be findable.
return breakpoint(at: "(anonymous namespace)::" + class_of(name) + "::execute")
# C: stop when the dump file for the pass is opened, which happens once per function.
return breakpoint(at: "pass_init_dump_file", when: streq(pass.name, name))
A is the one that always works. The line at gcc/passes.cc:2597@releases/gcc-16.2.0 is where current_pass is set, which is after the type assertion and before the gate is evaluated, so a stop there is a stop before the pass has decided whether to run. The condition needs gdb's $_streq, because pass->name is a const char * and comparing it with == compares addresses.
B is what break-on-pass in gdbhooks.py does (gcc/gdbhooks.py:754@releases/gcc-16.2.0). The line is sym = '(anonymous namespace)::%s::execute' % arg, so the argument is interpolated with nothing added to it and nothing checked about it. What it wants is the class name, which for the ccp pass is pass_ccp, and the reason a reader will nevertheless type the pass name is that the pass name is what every dump file, every -fdump-passes line and every other command in this document uses.
The completion is the reason the command exists, and it is also the thing that says which name is wanted: PassNames reads passes.def off disk and collects the first argument of each NEXT_PASS (gcc/gdbhooks.py:700@releases/gcc-16.2.0), which is a class name. So tab completion offers pass_ccp and typing ccp is accepted anyway, because gdb.Breakpoint on a symbol that does not resolve is a pending breakpoint rather than an error. The session then runs to completion with a breakpoint that can never fire, and the only warning is the two lines gdb prints when the breakpoint is made. Section 6 has the recorded output.
B fails for two further reasons that do produce the same pending breakpoint: a pass whose execute is inherited rather than defined, and a pass not in an anonymous namespace.
C stops once per function rather than once per pass invocation, and is the one to use when the question is about a particular function rather than a particular pass.
None of the three stops on a pass that did not run. A gate that returned false returns from execute_one_pass at gcc/passes.cc:2620@releases/gcc-16.2.0 without reaching any of them, and a reader whose breakpoint never fires should check -fdump-passes before concluding the breakpoint is wrong.
3.3 The debug counter¶
function dbg_cnt (c: Counter) -> boolean
complexity: O(1)
c.count = c.count + 1
v = c.count
if c.limits does not exist
return true # no -fdbg-cnt for this counter, allow everything
if length(c.limits) == 0
return false # every range has been used up, allow nothing
r = c.limits[length(c.limits) - 1] # the lowest range still open
if v < r.low
return false
if v == r.low
report_reached(c.name, v, closing: r.low == r.high)
if r.low == r.high
pop(c.limits)
return true
if v < r.high
return true
if v == r.high
report_reached(c.name, v, closing: true)
pop(c.limits)
return true
return false
The shape worth noticing is the first branch. A counter with no limit set returns true forever and costs one increment, which is why the calls can be left in the compiler permanently. A counter whose ranges are exhausted returns false forever, which is what makes -fdbg-cnt=name:N mean "do the first N of these and then stop".
The transformation is written so that returning false means "do not do it". A pass that ignores the return value has a counter that does nothing, and nothing checks for that.
3.4 Parsing -fdbg-cnt¶
function process_option (arg: String) -> ok or error(message)
complexity: O(k^2) in the number of ranges for one counter
for each token in split(arg, ",")
parts = split(token, ":")
name = parts[0]
for each part in parts[1..length(parts)]
if part contains "-"
low, high = split(part, "-")
else
high = number(part)
low = if high == 0 then 0 else 1
if high < low
return error("smaller upper limit than the lower")
if name is not a counter
return error("cannot find a valid counter name")
add the range, sort descending, and check it overlaps nothing
-fdbg-cnt=ccp:10 therefore means the range 1 to 10 and not the single value 10, and -fdbg-cnt=ccp:0 is the one case where the low end is zero, which is how a counter is turned off entirely (gcc/dbgcnt.cc:196@releases/gcc-16.2.0).
Overlapping ranges are rejected with an error naming both (gcc/dbgcnt.cc:140@releases/gcc-16.2.0). Adjacent ranges are not merged.
The loop over ranges stops at the first failure and the loop over counters does not, so -fdbg-cnt=nosuch:5,ccp:10 reports one error and still sets the ccp limit.
3.5 Turning a pass off without a debugger¶
-fdisable-PASS and -fenable-PASS are parsed by one function for both (gcc/passes.cc:1066@releases/gcc-16.2.0). Three forms:
-fdisable-tree-ccp every invocation, every function
-fdisable-tree-ccp=fn1,fn2 those functions, by name or by assembler name
-fdisable-tree-ccp=s1:e1,s2:e2 those function numbering ranges
The name in the option is the dump name of the pass, which is the name with the phase prefix, and -fdump-passes is what prints the list. The result is consulted from override_gate_status, so a disabled pass is a pass whose gate returned false, which means it is indistinguishable from a pass that decided not to run.
4. Invariants¶
I1. A debug_* function prints to stderr and returns nothing.
Established by: the functions themselves. Checked by: nothing. May be broken by: nobody. The commands in gdbinit.in depend on this, because gdb shows the return value of an inferior call and a function that returned something useful would print it twice.
I2. A debug_* function does not allocate garbage collected memory and does not modify the IR.
Established by: convention. Checked by: nothing. May be broken by: nobody. This is the invariant that makes it safe to call one from a breakpoint at any point in the pipeline, and it is the one with no enforcement at all. A printer that allocated would move the collector's state and change the compilation being debugged.
I3. The sixteen commands that call into the compiler need a process that is stopped in a frame where a call can be made.
Established by: gdb. Checked by: gdb, which reports the failure. May be broken by: a compiler stopped inside fancy_abort after the state the printer reads has already been corrupted, which is exactly when a reader wants to use one.
I4. dbgcnt.def is sorted by counter name.
Established by: whoever adds a counter. Checked by: a selftest, test_sorted_dbg_counters at gcc/dbgcnt.cc:268@releases/gcc-16.2.0, which runs under -fself-test. This is one of the few things in this document that anything checks. dbg_cnt_set_limit_by_name scans the array backwards and takes the first exact match, so the sort is not needed for correctness of lookup, and the selftest exists to keep the -fdbg-cnt-list output in alphabetical order.
I5. A debug counter's value is a position in the whole compilation, not in a pass or a function.
Established by: count being file static and never reset. Checked by: nothing. May be broken by: nobody, but it is broken in effect by anything that changes the order functions are compiled in, including adding a function to the file, changing an inlining decision, or enabling LTO.
I6. Every command in gdbinit.in names a function that exists.
Established by: whoever last renamed one. Checked by: nothing in GCC. The generated table in section 2.2 checks it here, and section 6 records the one place where the documentation and the body already disagree.
5. Observable behaviour¶
Two recordings are the corpus entry for all of this, and gxray.replay reads both.
corpora/replay/cc1.json is a real gdb driving a real cc1 built with -O0 -g3, twenty six commands in six groups, with every command and every byte of output kept. It is recorded rather than run because the compiler it needs is a three hundred megabyte binary from a build that does not fit on most machines, and because there is no gdb at all on macOS.
corpora/replay/counters.json is the other half and is not a transcript. It is one compilation of the same program at every limit of the match counter, fifty of them, plus the -fdbg-cnt-list table and one backtrace, so a bisection can be run over recorded outcomes rather than described.
| Claim | Where to check it |
|---|---|
A dbg_cnt call site prints a line to stderr the first time it reaches the low end of a range and again at the high end |
the stderr of every trial in corpora/replay/counters.json, which is lower limit 1 reached once and upper limit N reached per limit |
-fdbg-cnt-list prints all 75 counters with their current value and the ranges as closed intervals |
the listing in the same file, 75 rows and a two line header |
The pretty printers change what print produces, and there are more of them registered than gdbhooks.py names |
info pretty-printer global gcc in the recorded session lists 21, against the 17 in section 2.5 |
break-on-pass takes a class name, and a pass name makes a pending breakpoint with no further diagnostic |
the two adjacent break-on-pass commands in the recorded session, ccp then pass_ccp |
A breakpoint on execute_one_pass is hit hundreds of times for a nine line C file |
the same recording, where gdb's own hit count reads 351 |
pcfun called on either side of one pass shows the transformation that pass made |
the two pcfun commands in the recorded session, which bracket ccp |
Timing is observable and is worth stating because it decides whether an approach is usable. A conditional breakpoint whose condition is evaluated by gdb costs a full stop and resume of the inferior on every hit, and execute_one_pass is called often enough that a $_streq condition on it can take minutes on a large file. A breakpoint on the pass's own execute costs nothing until it fires.
6. Edge cases and error paths¶
The compiler is optimized. At -O2 the locals a reader wants are gone and the line table runs backwards. The dbg configuration exists for this reason and nothing else. -g3 rather than -g because half of what a reader wants to look at in GCC is a macro, and only -g3 records macro definitions.
The compiler was stripped after it was built with -g3. make install-strip overrides INSTALL_PROGRAM to strip, so the installed cc1 keeps none of the debug info the build spent its time producing, and the build reports nothing wrong because nothing is. The build tree still has an unstripped cc1 in gcc/, which is what a reader debugging their own build is pointed at anyway, so this only bites somebody debugging an installed compiler or an image built by somebody else. This project shipped exactly that image for three weeks. containers/matrix.toml now overrides the shared install-strip on the two configurations built with -g, and tools.matrix.validate rejects the combination outright, because the symptom arrives as No symbol table is loaded in a terminal a long way from the build that caused it.
gdb declines to load .gdbinit. Modern gdb refuses to source a .gdbinit from the current directory unless the directory is on the auto-load safe path, and reports it as a warning that scrolls past. Every command in this document is then missing and the failure looks like the commands not existing. The diagnostic to look for contains the words auto-loading has been declined, and gdb prints the two remedies with it: add-auto-load-safe-path naming this one build tree, or set auto-load safe-path / turning the protection off, either of them in the user's own configuration file, which on a current gdb is ~/.config/gdb/gdbinit rather than ~/.gdbinit.
Setting it on the command line instead needs -iex and not -ex. The local .gdbinit is read during startup, before any -ex runs, so a -ex "set auto-load safe-path /" is evaluated after the decision it was meant to change. This is worth knowing for anything that drives gdb from a script.
The build tree moved. Section 2.1 has the reason. gdb reports one error per dir line and one for the source line, then continues without the hooks.
break-on-pass was given a pass name. The argument has to be the class name. Given the pass name the symbol does not resolve, and gdb makes a pending breakpoint instead of refusing, which is the correct thing for gdb to do and the wrong thing to happen here, because a pending breakpoint on a symbol that no shared library will ever provide never becomes anything. What is printed is two lines, both of which read as informational:
(gdb) break-on-pass ccp
Function "(anonymous namespace)::ccp::execute" not defined.
Breakpoint 7 ((anonymous namespace)::ccp::execute) pending.
(gdb) break-on-pass pass_ccp
Breakpoint 8 at 0x1da5638: file /src/gcc/tree-ssa-ccp.cc, line 3072.
The difference between a breakpoint that will fire and one that cannot is the word pending, and info breakpoints is the way to see it after the fact. Section 3.2 has why the wanted name is the class name.
reload-gdbhooks does not work. The command runs python import imp; imp.reload(gdbhooks) at gcc/gdbinit.in:316@releases/gcc-16.2.0. The imp module was deprecated in Python 3.4 and removed in 3.12, so on any gdb linked against a current Python the command raises ModuleNotFoundError: No module named 'imp' and the module is not reloaded. The replacement is importlib.reload. This is in the tree as of 16.2.0 and the recorded session has the failure in it.
break-on-diagnostic documents the wrong function. The body breaks on diagnostics::context::maybe_show_locus and the documentation says diagnostics::context::show_locus (gcc/gdbinit.in:299@releases/gcc-16.2.0). The body is what runs. The generated table in section 2.2 shows both, which is why it prints the body's target in one column and the documentation in another.
A printer is handed a bad pointer. The pretty printers run in gdb's Python and read memory through gdb, so a corrupt tree produces a Python traceback rather than a crash, and gdb then prints the raw value. This is the behaviour to want and it is worth knowing that the traceback is not a sign that the debugger is broken.
An inferior call crashes. set unwindonsignal on in gdbinit.in is what makes this survivable: the call unwinds and the session continues. Without it a pr on a bad pointer leaves the inferior stopped inside the failed call, and the session has to be restarted.
The compiler stopped in fancy_abort. Four breakpoints are set at startup, and this is the one that fires. stdio may already be in a state where the debug_* functions cannot print, which is the reason abort is on the breakpoint list at all: the comment in the file says the breakpoint is there to stop abort running and taking stdio with it (gcc/gdbinit.in:349@releases/gcc-16.2.0).
-fdbg-cnt on a counter that does not exist. An error, and the rest of the option is still processed. Section 3.4.
A debug counter with no call site. dbgcnt.def declares the counter and nothing calls dbg_cnt for it. Setting a limit then has no effect and produces no diagnostic, because the option parser checks the name against the declaration and not against the call sites.
-fdisable-PASS on a pass that is already off. No diagnostic. The gate returns false either way.
7. Interactions¶
gcc/passes.cc. The pass manager is what a breakpoint is set relative to, and current_pass is the global that says where the compiler is. It is set at gcc/passes.cc:2597@releases/gcc-16.2.0 and cleared on every exit from execute_one_pass, so it is nothing between passes and a reader who reads it at the wrong moment gets a stale answer.
cfun and current_function_decl. Both globals, both what pcfun reads. cfun is nothing during an IPA pass, which is asserted at the top of execute_one_pass, and pcfun falls back to current_function_decl for that case.
gcc/ggc-page.cc. The collector moves nothing, so a pointer printed at one breakpoint is still valid at the next. What it does do is free, so a tree from a previous pass may have been collected, and printing it gives a plausible looking structure made of reused memory. --param ggc-min-expand=0 --param ggc-min-heapsize=0 collects as often as possible and turns this from a rare confusion into a reliable crash, which is the point of doing it.
gcc/dumpfile.cc. dump-fn and dot-fn call the dump machinery from the debugger prompt, so they produce the same text a -fdump-tree-* would, for the function that is live right now. This is the bridge between this document and BP-PIPELINE, and it is the reason a reader who knows how to read a dump does not have to learn a second output format.
The plugin API. PLUGIN_PASS_EXECUTION fires once per executed pass at gcc/passes.cc:2631@releases/gcc-16.2.0, which makes a plugin an alternative to a conditional breakpoint for anything that can be decided programmatically. BP-PLUGIN specifies it.
-fself-test. The only automated coverage anything in this document has. It runs test_sorted_dbg_counters and nothing else that touches debugging.
8. Conformance¶
There is almost nothing here, and saying so is the useful part. gdbinit.in, gdbhooks.py and the debug_* family have no tests. Nothing builds them, nothing loads them, and a rename that breaks a command is found by the next person who types it.
| What | Test | What it proves |
|---|---|---|
| The counter list is sorted | test_sorted_dbg_counters, run by -fself-test |
I4 |
-fdbg-cnt reaches the counter |
three tests use it, and none of them is a test of it: gcc.dg/pr68766.c, gcc.dg/pr113693.c, gcc.dg/ipa/ipa-icf-39.c |
that the option still parses, as a side effect |
-fdisable-PASS is honoured |
158 tests use it and 4 use -fenable-, all of them to pin a pass on or off so the test can check something else |
3.5, for the passes those tests name |
| The pretty printers | nothing | nothing |
| The gdbinit commands | nothing | nothing |
dump-fn and dot-fn |
nothing | nothing |
Restating section 4 as assertions an implementation can be checked against:
- I1: every
debug_*entry point returnsvoidand writes tostderr. - I4:
dbgcnt.defis sorted, and the selftest above is the assertion. - I6: every function named in a
callinsidegdbinit.inresolves in a linkedcc1. This project checks the source level half of it inbpc show gdb-commandsand the link level half in the recorded session.
9. Port notes¶
Almost everything in this document is a convention rather than a requirement, and a reimplementation is free to ignore all of it. Two things are worth separating out.
Forced. A compiler that wants to be debuggable at the IR level needs printing functions that are callable from a debugger, which means they must be real functions with external linkage rather than templates, macros or lambdas, and they must not be inlined away. GCC achieves the last of those by giving them no callers in the compiler at all, which means they are removed by any link time garbage collection and are kept only because GCC does not use one on itself. An implementation that does needs to keep them alive deliberately.
Arbitrary. The names of every command. Two letter names beginning with p are a convention from the 1990s and they collide with nothing because the file is loaded only in this one build tree. The choice of gdb: everything in gdbinit.in is gdb specific and there is no lldb equivalent in the tree, which matters on macOS, where the system debugger is lldb and this file is unusable. The debug counter mechanism itself: the idea of a global counter that a pass consults before each transformation is a good one, but nothing forces the counting to be global rather than per function, and per function counting would make I5 hold rather than fail.
Target dependent. Nothing in this document, with one exception that is host dependent rather than target dependent: gcc/gdbasan.in is added to .gdbinit only when the host compiler that built GCC supported -fsanitize=address, so the same source tree produces a different .gdbinit on two machines.