Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
* [PATCH 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler
@ 2026-03-16 17:22 Simon Marchi
  2026-03-16 17:22 ` [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA Simon Marchi
  0 siblings, 1 reply; 10+ messages in thread
From: Simon Marchi @ 2026-03-16 17:22 UTC (permalink / raw)
  To: gdb-patches; +Cc: Simon Marchi

Add support to the DWARF assembler for generating .debug_frame sections.
My initial use case is to reproduce a crash happening when encountering
an empty FDE, but I suppose that other use cases will pop up in the
future.

 - Generate procs for the `DW_CFA_*` constants, similar to how the
   DW_OP_* constants are handled.  These `DW_CFA_*` procs are expected
   to be used in the CIE and FDE bodies, described below.

 - Add handlers for `DW_CFA_*` operations that take arguments.  I tried
   to cover everything that is in DWARF 5.

 - Add the `frame` proc, used to generate one .debug_frame section.

 - Add the `_frame_CIE` proc (available as `CIE` in the context of the
   frame proc), used to generate one Common Information Entry.

 - Add the `_frame_FDE` proc (available as `FDE` in the context of the
   frame proc), used to generate one Frame Description Entry.

Due to the nature of the .debug_frame contents (it describes how
specific machine registers get saved), I expect that most of
the tests written using this will be arch-specific.  But  I think it
will still be useful, as it will let us craft .debug_frame sections to
look exactly how we want.

I included a test (gdb.dwarf2/debug-frame.exp), which is more like a
proof that we can build something useful using this, and can serve as an
example for whoever wants to write a test case using this in the future.

Change-Id: I048568ded53883abf52d70139e5cd3e7b4ac3841
---
 gdb/testsuite/gdb.dwarf2/debug-frame.S   | 101 ++++++
 gdb/testsuite/gdb.dwarf2/debug-frame.exp | 130 ++++++++
 gdb/testsuite/lib/dwarf.exp              | 394 ++++++++++++++++++++++-
 3 files changed, 624 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.S
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.exp

diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.S b/gdb/testsuite/gdb.dwarf2/debug-frame.S
new file mode 100644
index 000000000000..231e2dc23704
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame.S
@@ -0,0 +1,101 @@
+/* Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Hand-written x86-64 assembly with no .cfi directives.  The .debug_frame
+   section is supplied by the companion -dw.S file, generated by the DWARF
+   assembler.
+
+   The call chain: main -> caller -> callee.
+
+   caller sets some known register values, and callee saves those registers in
+   different way.  */
+
+	.text
+
+/* main */
+	.globl main
+	.type main, @function
+main:
+	pushq	%rbp
+	.globl main_after_push_rbp
+main_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl main_after_set_rbp
+main_after_set_rbp:
+	call	caller
+	xorl	%eax, %eax
+	popq	%rbp
+	ret
+	.size main, . - main
+	.globl main_end
+main_end:
+	.globl main_len
+	.set main_len, main_end - main
+
+/* caller */
+	.globl caller
+	.type caller, @function
+caller:
+	pushq	%rbp
+	.globl caller_after_push_rbp
+caller_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl caller_after_set_rbp
+caller_after_set_rbp:
+	movq	$0x11223344, %r12
+	movq	$0x55667788, %r13
+	.globl caller_call_callee
+caller_call_callee:
+	call	callee
+	popq	%rbp
+	ret
+	.size caller, . - caller
+	.globl caller_end
+caller_end:
+	.globl caller_len
+	.set caller_len, caller_end - caller
+
+/* callee */
+	.globl callee
+	.type callee, @function
+callee:
+	pushq	%rbp
+	.globl callee_after_push_rbp
+callee_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl callee_after_set_rbp
+callee_after_set_rbp:
+	/* Save r12 in the stack, then clobber it.  */
+	pushq	%r12
+	xorq	%r12, %r12
+	/* Save r13 in rax, then clobber it.  */
+	movq	%r13, %rax
+	xorq	%r13, %r13
+	/* Clobber r14.  This one is described with a DWARF expression.  */
+	xorq	%r14, %r14
+	.globl callee_body
+callee_body:
+	nop
+	movq	%rax, %r13
+	popq	%r12
+	popq	%rbp
+	ret
+	.size callee, . - callee
+	.globl callee_end
+callee_end:
+	.globl callee_len
+	.set callee_len, callee_end - callee
+
+	.section	.note.GNU-stack,"",@progbits
diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.exp b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
new file mode 100644
index 000000000000..ddadab7566ef
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
@@ -0,0 +1,130 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test that GDB can unwind using a .debug_frame section generated by
+# the DWARF assembler.
+#
+# This test is amd64-specific, but could be ported to other
+# architectures if needed.
+
+load_lib dwarf.exp
+
+require dwarf2_support is_x86_64_m64_target
+
+standard_testfile .S -dw.S
+
+# AMD64 DWARF register numbers.
+set rax 0
+set rbp 6
+set rsp 7
+set r12 12
+set r13 13
+set r14 14
+set rip 16
+
+foreach_with_prefix is_64 { false true } {
+    set asm_file [standard_output_file ${testfile}-${is_64}-dw.S]
+
+    Dwarf::assemble $asm_file {
+	frame {
+	    declare_labels cie_label
+
+	    cie_label: CIE {
+		return_address_register $::rip
+		data_alignment_factor -8
+		is_64 $::is_64
+	    } {
+		DW_CFA_def_cfa $::rsp 8
+		DW_CFA_offset $::rip 1
+	    }
+
+	    # FDE for main
+	    FDE $cie_label main main_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc main_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc main_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+	    }
+
+	    # FDE for caller
+	    FDE $cie_label caller caller_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc caller_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc caller_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+	    }
+
+	    # FDE for callee
+	    FDE $cie_label callee callee_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc callee_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc callee_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+
+		DW_CFA_set_loc callee_body
+		DW_CFA_offset $::r12 3
+		DW_CFA_register $::r13 $::rax
+
+		# r14's value is computed by an arbitrary expression.
+		DW_CFA_val_expression $::r14 {
+		    DW_OP_constu 0x99aabbcc
+		}
+	    }
+	}
+    }
+
+    if { [prepare_for_testing "failed to prepare" ${testfile}-${is_64} \
+	      [list $srcfile $asm_file] {nodebug}] } {
+	continue
+    }
+
+    # Stop in caller before the call, to capture rbp.
+    if { ![runto caller_call_callee] } {
+	continue
+    }
+
+    set caller_rbp [get_hexadecimal_valueof "\$rbp" "UNKNOWN"]
+
+    # Stop inside callee.
+    gdb_breakpoint callee_body
+    gdb_continue_to_breakpoint "callee_body"
+
+    # Verify backtrace shows the full call chain.
+    gdb_test "bt" "#0.*callee.*\r\n#1.*caller.*\r\n#2.*main.*"
+
+    # Select caller's frame and check saved registers.
+    gdb_test "frame 1" "#1.*caller.*"
+
+    # r12 was saved on the stack by callee.
+    gdb_test "p/x \$r12" "= 0x11223344"
+
+    # r13 was saved in rax by callee.
+    gdb_test "p/x \$r13" "= 0x55667788"
+
+    # r14's value is computed by a DWARF expression.
+    gdb_test "p/x \$r14" "= 0x99aabbcc"
+
+    # rbp should match what caller had.
+    gdb_test "p/x \$rbp" "= ${caller_rbp}"
+}
diff --git a/gdb/testsuite/lib/dwarf.exp b/gdb/testsuite/lib/dwarf.exp
index 9fa5c4e297da..e545019e065b 100644
--- a/gdb/testsuite/lib/dwarf.exp
+++ b/gdb/testsuite/lib/dwarf.exp
@@ -602,6 +602,10 @@ namespace eval Dwarf {
     variable _loc_addr_size
     variable _loc_offset_size
 
+    # Variables used when generating a .debug_frame section.
+    variable _frame_addr_size
+    variable _frame_offset_size
+
     proc _process_one_constant {name value} {
 	variable _constants
 	variable _FORM
@@ -618,7 +622,6 @@ namespace eval Dwarf {
 	}
 
 	# We only try to shorten some very common things.
-	# FIXME: CFA?
 	switch -exact -- $prefix {
 	    TAG {
 		# Create two procedures for the tag.  These call
@@ -667,6 +670,57 @@ namespace eval Dwarf {
 		} $name $name $handler]
 	    }
 
+	    CFA {
+		# Create procs for DW_CFA_* instructions, used in
+		# .debug_frame CIE/FDE bodies.
+
+		# DW_CFA_advance_loc, DW_CFA_offset and
+		# DW_CFA_restore encode the operand in the low 6
+		# bits of the opcode byte.  They need special
+		# handling.
+		switch -exact -- $name {
+		    DW_CFA_advance_loc {
+			proc DW_CFA_advance_loc {delta} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_advance_loc) + $delta" \
+				DW_CFA_advance_loc
+			}
+		    }
+
+		    DW_CFA_offset {
+			proc DW_CFA_offset {register offset} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_offset) + $register" \
+				DW_CFA_offset
+			    _op .uleb128 $offset "offset"
+			}
+		    }
+
+		    DW_CFA_restore {
+			proc DW_CFA_restore {register} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_restore) + $register" \
+				DW_CFA_restore
+			}
+		    }
+
+		    default {
+			# Standard CFA instruction: emit opcode
+			# byte then delegate to handler.
+			set handler _handle_default_CFA
+			if {[llength [info procs _handle_$name]] > 0} {
+			    set handler _handle_$name
+			}
+
+			# tclint-disable-next-line command-args
+			proc $name {args} [format {
+			    _op .byte $Dwarf::_constants(%s) %s
+			    %s {*}$args
+			} $name $name $handler]
+		    }
+		}
+	    }
+
 	    default {
 		return
 	    }
@@ -1456,6 +1510,127 @@ namespace eval Dwarf {
 	# error.
     }
 
+    # Helper to emit a DWARF expression block (ULEB128 length followed
+    # by the expression bytes) inside a .debug_frame CIE or FDE body.
+    # BODY is a Tcl code containing DW_OP_* calls.
+    proc _emit_cfa_expression {body} {
+	set start [new_label "cfa_expr_start"]
+	set end [new_label "cfa_expr_end"]
+	_op .uleb128 "$end - $start" "expression length"
+	define_label $start
+
+	# Pass 5 as the DWARF version, since we need to pass something, but it
+	# doesn't matter.  The DWARF version is checked only for DW_OP_* ops
+	# that don't make sense in CFI.
+	_location $body 5 $Dwarf::_frame_addr_size $Dwarf::_frame_offset_size
+	define_label $end
+    }
+
+    #
+    # Handlers for DW_CFA_* instructions.
+    #
+    # A handler is only needed if the instruction requires operands.
+    # Generic code handles emitting the opcode byte itself, so a
+    # handler should not do this.
+    #
+    # Handlers are found by name when processing the .def file.  If a
+    # handler isn't found, the default (_handle_default_CFA) is used.
+    #
+
+    proc _handle_default_CFA {} {
+	# Do nothing; if arguments are passed, Tcl will cause an
+	# error.
+    }
+
+    proc _handle_DW_CFA_set_loc {address} {
+	_op .${Dwarf::_frame_addr_size}byte $address "address"
+    }
+
+    proc _handle_DW_CFA_advance_loc1 {delta} {
+	_op .byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_advance_loc2 {delta} {
+	_op .2byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_advance_loc4 {delta} {
+	_op .4byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_offset_extended {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_restore_extended {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_undefined {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_same_value {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_register {register1 register2} {
+	_op .uleb128 $register1 "register"
+	_op .uleb128 $register2 "register"
+    }
+
+    proc _handle_DW_CFA_def_cfa {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_register {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_def_cfa_offset {offset} {
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_expression {body} {
+	_emit_cfa_expression $body
+    }
+
+    proc _handle_DW_CFA_expression {register body} {
+	_op .uleb128 $register "register"
+	_emit_cfa_expression $body
+    }
+
+    proc _handle_DW_CFA_offset_extended_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_offset_sf {offset} {
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_offset {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_offset_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_expression {register body} {
+	_op .uleb128 $register "register"
+	_emit_cfa_expression $body
+    }
+
     # This is a miniature assembler for location expressions.  It is
     # suitable for use in the attributes to a DIE.
     #
@@ -3554,6 +3729,223 @@ namespace eval Dwarf {
 	debug_str_offsets_end:
     }
 
+    # Emit a DWARF .debug_frame section.
+    #
+    # BODY is Tcl code that emits the CIEs and FDEs which make up the
+    # section.  It is evaluated in the caller's context.
+    #
+    # Within BODY, the following commands are available:
+    #
+    #   CIE options body
+    #     -- emit a Common Information Entry.  See _frame_CIE for details.
+    #
+    #   FDE cie_label initial_location address_range body
+    #     -- emit a Frame Description Entry.  See _frame_FDE for details.
+    proc frame { body } {
+	_section .debug_frame
+
+	with_override Dwarf::CIE Dwarf::_frame_CIE {
+	    with_override Dwarf::FDE Dwarf::_frame_FDE {
+		uplevel $Dwarf::_level $body
+	    }
+	}
+    }
+
+    # Available as proc CIE when in the body of proc debug_frame.
+    #
+    # OPTIONS is a list of option-name/option-value pairs.  Supported
+    # options are (default values are shown in parentheses):
+    #
+    #   is_64 (false)
+    #     -- if true, emit a 64-bit CIE.
+    #
+    #   cie_id (default)
+    #     -- the CIE id value.  When "default", uses 0xffffffff for
+    #        32-bit and 0xffffffffffffffff for 64-bit.  Should typically not be
+    #        used unless trying to craft an invalid CIE.
+    #
+    #   version (4)
+    #     -- the CIE version number.  Note that this is version independent
+    #        from the DWARF version.  DWARF 4 and 5 both use .debug_frame
+    #        version 4.
+    #
+    #   augmentation ("")
+    #     -- the augmentation string.
+    #
+    #   addr_size (default)
+    #     -- the address size in bytes.  When "default", use 8 for 64-bit
+    #        targets and 4 for 32-bit targets.
+    #
+    #   segment_selector_size (0)
+    #     -- the segment selector size in bytes.
+    #
+    #   code_alignment_factor (1)
+    #     -- the code alignment factor.
+    #
+    #   data_alignment_factor (1)
+    #     -- the data alignment factor.
+    #
+    #   return_address_register (0)
+    #     -- the number of the "column" containing the return address.
+    #
+    # BODY is Tcl code that emits the CIE's initial instructions using
+    # DW_CFA_* operations.  It is evaluated in the caller's context.
+    proc _frame_CIE {options body} {
+	parse_options {
+	    { is_64 false }
+	    { cie_id default }
+	    { version 4 }
+	    { augmentation "" }
+	    { addr_size default }
+	    { segment_selector_size 0 }
+	    { code_alignment_factor 1 }
+	    { data_alignment_factor 1 }
+	    { return_address_register 0 }
+	}
+
+	if { $is_64 } {
+	    set Dwarf::_frame_offset_size 8
+	} else {
+	    set Dwarf::_frame_offset_size 4
+	}
+
+	if { $cie_id == "default" } {
+	    if { $is_64 } {
+		set cie_id 0xffffffffffffffff
+	    } else {
+		set cie_id 0xffffffff
+	    }
+	}
+
+	if {$addr_size == "default"} {
+	    if {[is_64_target]} {
+		set Dwarf::_frame_addr_size 8
+	    } else {
+		set Dwarf::_frame_addr_size 4
+	    }
+	} else {
+	    set Dwarf::_frame_addr_size $addr_size
+	}
+
+	declare_labels cie_post_length cie_end
+
+	# Length.
+	if { $is_64 } {
+	    _op .4byte 0xffffffff "length 1/2"
+	    _op .8byte "$cie_end - $cie_post_length" "length 2/2"
+	} else {
+	    _op .4byte "$cie_end - $cie_post_length" "length"
+	}
+
+	define_label $cie_post_length
+
+	# CIE_id
+	_op .${Dwarf::_frame_offset_size}byte $cie_id "CIE_id"
+
+	# Version.
+	_op .byte $version "version"
+
+	# Augmentation string.
+	_op .ascii [_quote $augmentation] "augmentation"
+
+	# Address size.
+	_op .byte $Dwarf::_frame_addr_size "address_size"
+
+	# Segment selector size.
+	_op .byte 0 "segment_size"
+
+	# Code alignment factor.
+	_op .uleb128 $code_alignment_factor "code_alignment_factor"
+
+	# Data alignment factor.
+	_op .sleb128 $data_alignment_factor "data_alignment_factor"
+
+	# Return address register.
+	_op .uleb128 $return_address_register "return_address_register"
+
+	# Initial instructions.
+	uplevel $Dwarf::_level $body
+
+	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
+	_op .align $Dwarf::_frame_addr_size "padding"
+
+	define_label $cie_end
+    }
+
+    # Available as proc FDE when in the body of proc debug_frame.
+    #
+    # CIE_LABEL is the label of the CIE this FDE refers to.
+    #
+    # INITIAL_LOCATION is the address of the first instruction covered
+    # by this FDE.
+    #
+    # ADDRESS_RANGE is the number of bytes of instructions covered by
+    # this FDE.
+    #
+    # OPTIONS is a list of option-name/option-value pairs.  Supported
+    # options are (default values are shown in parentheses):
+    #
+    #   is_64 (false)
+    #     -- if true, emit a 64-bit CIE.
+    #
+    #   addr_size (default)
+    #     -- the address size in bytes.  When "default", use 8 for 64-bit
+    #        targets and 4 for 32-bit targets.
+    #
+    # BODY is Tcl code that emits the FDE's call frame instructions using
+    # DW_CFA_* operations.  It is evaluated in the caller's context.
+    proc _frame_FDE { cie_label initial_location address_range options
+		      body } {
+	parse_options {
+	    { is_64 false }
+	    { addr_size default }
+	}
+
+	if { $is_64 } {
+	    set Dwarf::_frame_offset_size 8
+	} else {
+	    set Dwarf::_frame_offset_size 4
+	}
+
+	if {$addr_size == "default"} {
+	    if {[is_64_target]} {
+		set Dwarf::_frame_addr_size 8
+	    } else {
+		set Dwarf::_frame_addr_size 4
+	    }
+	} else {
+	    set Dwarf::_frame_addr_size $addr_size
+	}
+
+	declare_labels fde_post_length fde_end
+
+	# Length.
+	if { $is_64 } {
+	    _op .4byte 0xffffffff "length 1/2"
+	    _op .8byte "$fde_end - $fde_post_length" "length 2/2"
+	} else {
+	    _op .4byte "$fde_end - $fde_post_length" "length"
+	}
+	define_label $fde_post_length
+
+	# CIE pointer, offset of the CIE into the .debug_frame section.
+	_op .${Dwarf::_frame_offset_size}byte $cie_label "CIE pointer"
+
+	# Initial location.
+	_op .${Dwarf::_frame_addr_size}byte $initial_location "initial_location"
+
+	# Address range.
+	_op .${Dwarf::_frame_addr_size}byte $address_range "address_range"
+
+	# Instructions.
+	uplevel $Dwarf::_level $body
+
+	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
+	_op .align $Dwarf::_frame_addr_size "padding"
+
+	define_label $fde_end
+    }
+
     # The top-level interface to the DWARF assembler.
     # OPTIONS is a list with an even number of elements containing
     # option-name and option-value pairs.

base-commit: 07caff21f90c2f0c9b7b0e79b00b774be668594c
-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA
  2026-03-16 17:22 [PATCH 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler Simon Marchi
@ 2026-03-16 17:22 ` Simon Marchi
  2026-03-17 19:33   ` Simon Marchi
  2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
  0 siblings, 2 replies; 10+ messages in thread
From: Simon Marchi @ 2026-03-16 17:22 UTC (permalink / raw)
  To: gdb-patches; +Cc: Simon Marchi

From: Simon Marchi <simon.marchi@polymtl.ca>

This patch fixes an internal error problem that happens when a frame
description entry does not defined the Canonical Frame Address (CFA).
This problem was initially reported downstream as a ROCgdb issue (see
Bug trailer below), but I wrote a reproducer that uses the .debug_frame
functionality added to the DWARF assembler in the previous patch.

The error is:

    /home/smarchi/src/binutils-gdb/gdb/dwarf2/frame.c:1046: internal-error: Unknown CFA rule.

The original bug was encountered while debugging a GPU kernel written
with Triton [1].  From what I understand, the generated kernel does not
really use a stack, so the .debug_frame contents generated is quite
bare:

    $ readelf --debug-dump=frames k
    Contents of the .debug_frame section:

    00000000 000000000000000c ffffffff CIE
      Version:               4
      Augmentation:          ""
      Pointer Size:          8
      Segment Size:          0
      Code alignment factor: 4
      Data alignment factor: 4
      Return address column: 16

      DW_CFA_nop

    00000010 0000000000000014 00000000 FDE cie=00000000 pc=0000000000001600..0000000000001704

For those who don't speak fluent .debug_frame, what we see here is a
Frame Description Entry (FDE) that doesn't define any register rule,
referring to a Common Information Entry (CIE) that also doesn't define
any initial register rule.  This is equivalent to having no unwind
information at all.  One question is: why generate these at all?  I
suppose that this is an edge case, that the compiler is written in a way
that that presumes there will always be some unwind info.  That there is
not "if unwind info is empty, skip emitting the FDE" check.  Anyway, the
important thing for us is that these can be found in the wild, so GDB
shouldn't crash.

The first part of the fix is to handle CFA_UNSET in dwarf2_frame_cache
(and do nothing).  CFA_UNSET is the initial state when we start
interpreting a CFA program, meaning that we don't know yet how the CFA
is defined.  In our case, it remains unset after interpreting the CFA
program.

Then, we would ideally want to get into this `if` below that sets
undefined_retaddr:

  if (fs.retaddr_column < fs.regs.reg.size ()
      && fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
    cache->undefined_retaddr = true;

Setting undefined_retaddr has two effects:

 - dwarf2_frame_this_id won't try to build a frame id from the CFA
 - dwarf2_frame_unwind_stop_reason will return UNWIND_OUTERMOST, which
   is the most accurate thing we can return here (there is not outer
   frame)

However, the way it is written currently, we don't get info the if.
`fs.regs.reg.size ()` is 0, so the condition always evaluates to false.
The `fs.regs.reg` is a vector that is expanded as needed: if an
operation sets a rule for register N, then we'll resize the vector so it
holds at least `N + 1` elements.  But conceptually, all register columns
initially contain "undefined".  If we arrive to this condition and the
vector hasn't been expanded to include a given column, then it means
that the rule for this column is "undefined".  Therefore, rewrite the
condition to consider the return address as undefined if the vector is
too small to include retaddr_column.

Here are some relevant references to DWARF 5:

 - Section 6.4.1. ("Structure of Call Frame Information")

     The default rule for all columns before interpretation of the initial
     instructions is the undefined rule.

 - Section 6.4.4 ("Call Frame Calling Address")

     If a Return Address register is defined in the virtual unwind
     table, and its rule is undefined (for example, by
     DW_CFA_undefined), then there is no return address and no call
     address, and the virtual unwind of stack activations is complete.

Add a test case written using the DWARF assembler that reproduces the
issue.  The user experience in this case is that the frame appears as
the outer most frame:

    (gdb) bt
    #0  0x000055555555511d in main ()
    (gdb) up
    ❌️ Initial frame selected; you cannot go up.
    (gdb) frame 1
    ❌️ No frame at level 1.

[1] https://triton-lang.org/

Change-Id: I67c717ff03a41c0630a73ce9549d88ff363e8cea
Bug: https://github.com/ROCm/ROCgdb/issues/47
---
 gdb/dwarf2/frame.c                            |  7 ++-
 .../gdb.dwarf2/debug-frame-no-cfa.exp         | 51 +++++++++++++++++++
 2 files changed, 56 insertions(+), 2 deletions(-)
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp

diff --git a/gdb/dwarf2/frame.c b/gdb/dwarf2/frame.c
index 152bebef0e30..fe74a4f65223 100644
--- a/gdb/dwarf2/frame.c
+++ b/gdb/dwarf2/frame.c
@@ -962,6 +962,9 @@ dwarf2_frame_cache (const frame_info_ptr &this_frame, void **this_cache)
       /* Calculate the CFA.  */
       switch (fs.regs.cfa_how)
 	{
+	case CFA_UNSET:
+	  break;
+
 	case CFA_REG_OFFSET:
 	  cache->cfa = read_addr_from_reg (this_frame, fs.regs.cfa_reg);
 	  if (fs.armcc_cfa_offsets_reversed)
@@ -1074,8 +1077,8 @@ incomplete CFI data; unspecified registers (e.g., %s) at %s"),
 	}
     }
 
-  if (fs.retaddr_column < fs.regs.reg.size ()
-      && fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
+  if (fs.retaddr_column >= fs.regs.reg.size ()
+      || fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
     cache->undefined_retaddr = true;
 
   dwarf2_tailcall_sniffer_first (this_frame, &cache->tailcall_cache,
diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp b/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp
new file mode 100644
index 000000000000..e8397b253e41
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp
@@ -0,0 +1,51 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test GDB against an FDE in the .debug_frame section that doesn't set a rule
+# for the CFA.
+
+load_lib dwarf.exp
+
+require dwarf2_support
+
+standard_testfile main.c -dw.S
+
+set asm_file [standard_output_file $srcfile2]
+Dwarf::assemble $asm_file {
+    get_func_info main
+
+    frame {
+	declare_labels cie_label
+
+	cie_label: CIE {
+	    return_address_register 16
+	} {}
+
+	FDE $cie_label $main_start $main_len {} {
+	}
+    }
+}
+
+if { [prepare_for_testing "failed to prepare" ${testfile} \
+	[list $srcfile $asm_file] {nodebug}] } {
+    return
+}
+
+if { ![runto_main] } {
+    return
+}
+
+# This would cause an internal error in dwarf2_frame_cache.
+gdb_test "backtrace" "#0 .* main .*"
-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA
  2026-03-16 17:22 ` [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA Simon Marchi
@ 2026-03-17 19:33   ` Simon Marchi
  2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
  1 sibling, 0 replies; 10+ messages in thread
From: Simon Marchi @ 2026-03-17 19:33 UTC (permalink / raw)
  To: Simon Marchi, gdb-patches



On 2026-03-16 13:22, Simon Marchi wrote:
> From: Simon Marchi <simon.marchi@polymtl.ca>
> 
> This patch fixes an internal error problem that happens when a frame
> description entry does not defined the Canonical Frame Address (CFA).
> This problem was initially reported downstream as a ROCgdb issue (see
> Bug trailer below), but I wrote a reproducer that uses the .debug_frame
> functionality added to the DWARF assembler in the previous patch.
> 
> The error is:
> 
>     /home/smarchi/src/binutils-gdb/gdb/dwarf2/frame.c:1046: internal-error: Unknown CFA rule.
> 
> The original bug was encountered while debugging a GPU kernel written
> with Triton [1].  From what I understand, the generated kernel does not
> really use a stack, so the .debug_frame contents generated is quite
> bare:
> 
>     $ readelf --debug-dump=frames k
>     Contents of the .debug_frame section:
> 
>     00000000 000000000000000c ffffffff CIE
>       Version:               4
>       Augmentation:          ""
>       Pointer Size:          8
>       Segment Size:          0
>       Code alignment factor: 4
>       Data alignment factor: 4
>       Return address column: 16
> 
>       DW_CFA_nop
> 
>     00000010 0000000000000014 00000000 FDE cie=00000000 pc=0000000000001600..0000000000001704
> 
> For those who don't speak fluent .debug_frame, what we see here is a
> Frame Description Entry (FDE) that doesn't define any register rule,
> referring to a Common Information Entry (CIE) that also doesn't define
> any initial register rule.  This is equivalent to having no unwind
> information at all.  One question is: why generate these at all?  I
> suppose that this is an edge case, that the compiler is written in a way
> that that presumes there will always be some unwind info.  That there is
> not "if unwind info is empty, skip emitting the FDE" check.  Anyway, the
> important thing for us is that these can be found in the wild, so GDB
> shouldn't crash.
> 
> The first part of the fix is to handle CFA_UNSET in dwarf2_frame_cache
> (and do nothing).  CFA_UNSET is the initial state when we start
> interpreting a CFA program, meaning that we don't know yet how the CFA
> is defined.  In our case, it remains unset after interpreting the CFA
> program.
> 
> Then, we would ideally want to get into this `if` below that sets
> undefined_retaddr:
> 
>   if (fs.retaddr_column < fs.regs.reg.size ()
>       && fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
>     cache->undefined_retaddr = true;
> 
> Setting undefined_retaddr has two effects:
> 
>  - dwarf2_frame_this_id won't try to build a frame id from the CFA
>  - dwarf2_frame_unwind_stop_reason will return UNWIND_OUTERMOST, which
>    is the most accurate thing we can return here (there is not outer
>    frame)
> 
> However, the way it is written currently, we don't get info the if.
> `fs.regs.reg.size ()` is 0, so the condition always evaluates to false.
> The `fs.regs.reg` is a vector that is expanded as needed: if an
> operation sets a rule for register N, then we'll resize the vector so it
> holds at least `N + 1` elements.  But conceptually, all register columns
> initially contain "undefined".  If we arrive to this condition and the
> vector hasn't been expanded to include a given column, then it means
> that the rule for this column is "undefined".  Therefore, rewrite the
> condition to consider the return address as undefined if the vector is
> too small to include retaddr_column.

This is not really true, at least in the real-world.  In GDB, registers
start as "unspecified", not "undefined".  It seems like "undefined"
means: the CFI positively told us that this register's previous value is
unknown.  Unspecified means that the CFI hasn't said anything about it,
and this can be interpreted in various ways.  See below.

> 
> Here are some relevant references to DWARF 5:
> 
>  - Section 6.4.1. ("Structure of Call Frame Information")
> 
>      The default rule for all columns before interpretation of the
>      initial instructions is the undefined rule.
> 
>  - Section 6.4.4 ("Call Frame Calling Address")
> 
>      If a Return Address register is defined in the virtual unwind
>      table, and its rule is undefined (for example, by
>      DW_CFA_undefined), then there is no return address and no call
>      address, and the virtual unwind of stack activations is complete.
> 
> Add a test case written using the DWARF assembler that reproduces the
> issue.  The user experience in this case is that the frame appears as
> the outer most frame:
> 
>     (gdb) bt #0  0x000055555555511d in main () (gdb) up ❌️ Initial
>     frame selected; you cannot go up.  (gdb) frame 1 ❌️ No frame at
>     level 1.
> 
> [1] https://triton-lang.org/
> 
> Change-Id: I67c717ff03a41c0630a73ce9549d88ff363e8cea Bug:
> https://github.com/ROCm/ROCgdb/issues/47 --- gdb/dwarf2/frame.c
> |  7 ++- .../gdb.dwarf2/debug-frame-no-cfa.exp         | 51
> +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-)
> create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp
> 
> diff --git a/gdb/dwarf2/frame.c b/gdb/dwarf2/frame.c index
> 152bebef0e30..fe74a4f65223 100644 --- a/gdb/dwarf2/frame.c +++
> b/gdb/dwarf2/frame.c @@ -962,6 +962,9 @@ dwarf2_frame_cache (const
> frame_info_ptr &this_frame, void **this_cache) /* Calculate the CFA.
> */ switch (fs.regs.cfa_how) { +	case CFA_UNSET: +	  break;
> + case CFA_REG_OFFSET: cache->cfa = read_addr_from_reg (this_frame,
> fs.regs.cfa_reg); if (fs.armcc_cfa_offsets_reversed) @@ -1074,8
> +1077,8 @@ incomplete CFI data; unspecified registers (e.g., %s) at
> %s"), } }
>  
> -  if (fs.retaddr_column < fs.regs.reg.size () -      &&
> fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED) +
> if (fs.retaddr_column >= fs.regs.reg.size () +      ||
> fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
>      cache->undefined_retaddr = true;

The Linaro CI informed me that this change causes regressions on
AArch64.  The unwind info for AArch64 doesn't appear to (always) include
information about the return address.  With my change, we get into this
if, and erroneously think that the return address is undefined.
And then we never unwind past frame 0.

More thinking needed, as well as learning about the CFI corner cases...

Simon

^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler
  2026-03-16 17:22 ` [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA Simon Marchi
  2026-03-17 19:33   ` Simon Marchi
@ 2026-03-18 20:27   ` simon.marchi
  2026-03-18 20:27     ` [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA simon.marchi
                       ` (2 more replies)
  1 sibling, 3 replies; 10+ messages in thread
From: simon.marchi @ 2026-03-18 20:27 UTC (permalink / raw)
  To: gdb-patches; +Cc: Simon Marchi

From: Simon Marchi <simon.marchi@efficios.com>

Add support to the DWARF assembler for generating .debug_frame sections.
My initial use case is to reproduce a crash happening when encountering
an empty FDE, but I suppose that other use cases will pop up in the
future.

 - Generate procs for the `DW_CFA_*` constants, similar to how the
   DW_OP_* constants are handled.  These `DW_CFA_*` procs are expected
   to be used in the CIE and FDE bodies, described below.

 - Add handlers for `DW_CFA_*` operations that take arguments.  I tried
   to cover everything that is in DWARF 5.

 - Add the `frame` proc, used to generate one .debug_frame section.

 - Add the `_frame_CIE` proc (available as `CIE` in the context of the
   frame proc), used to generate one Common Information Entry.

 - Add the `_frame_FDE` proc (available as `FDE` in the context of the
   frame proc), used to generate one Frame Description Entry.

Due to the nature of the .debug_frame contents (it describes how
specific machine registers get saved), I expect that most of
the tests written using this will be arch-specific.  But  I think it
will still be useful, as it will let us craft .debug_frame sections to
look exactly how we want.

I included a test (gdb.dwarf2/debug-frame.exp), which is more like a
proof that we can build something useful using this, and can serve as an
example for whoever wants to write a test case using this in the future.

Change-Id: I048568ded53883abf52d70139e5cd3e7b4ac3841
---
 gdb/testsuite/gdb.dwarf2/debug-frame.S   | 101 ++++++
 gdb/testsuite/gdb.dwarf2/debug-frame.exp | 130 ++++++++
 gdb/testsuite/lib/dwarf.exp              | 394 ++++++++++++++++++++++-
 3 files changed, 624 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.S
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.exp

diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.S b/gdb/testsuite/gdb.dwarf2/debug-frame.S
new file mode 100644
index 000000000000..231e2dc23704
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame.S
@@ -0,0 +1,101 @@
+/* Copyright 2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Hand-written x86-64 assembly with no .cfi directives.  The .debug_frame
+   section is supplied by the companion -dw.S file, generated by the DWARF
+   assembler.
+
+   The call chain: main -> caller -> callee.
+
+   caller sets some known register values, and callee saves those registers in
+   different way.  */
+
+	.text
+
+/* main */
+	.globl main
+	.type main, @function
+main:
+	pushq	%rbp
+	.globl main_after_push_rbp
+main_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl main_after_set_rbp
+main_after_set_rbp:
+	call	caller
+	xorl	%eax, %eax
+	popq	%rbp
+	ret
+	.size main, . - main
+	.globl main_end
+main_end:
+	.globl main_len
+	.set main_len, main_end - main
+
+/* caller */
+	.globl caller
+	.type caller, @function
+caller:
+	pushq	%rbp
+	.globl caller_after_push_rbp
+caller_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl caller_after_set_rbp
+caller_after_set_rbp:
+	movq	$0x11223344, %r12
+	movq	$0x55667788, %r13
+	.globl caller_call_callee
+caller_call_callee:
+	call	callee
+	popq	%rbp
+	ret
+	.size caller, . - caller
+	.globl caller_end
+caller_end:
+	.globl caller_len
+	.set caller_len, caller_end - caller
+
+/* callee */
+	.globl callee
+	.type callee, @function
+callee:
+	pushq	%rbp
+	.globl callee_after_push_rbp
+callee_after_push_rbp:
+	movq	%rsp, %rbp
+	.globl callee_after_set_rbp
+callee_after_set_rbp:
+	/* Save r12 in the stack, then clobber it.  */
+	pushq	%r12
+	xorq	%r12, %r12
+	/* Save r13 in rax, then clobber it.  */
+	movq	%r13, %rax
+	xorq	%r13, %r13
+	/* Clobber r14.  This one is described with a DWARF expression.  */
+	xorq	%r14, %r14
+	.globl callee_body
+callee_body:
+	nop
+	movq	%rax, %r13
+	popq	%r12
+	popq	%rbp
+	ret
+	.size callee, . - callee
+	.globl callee_end
+callee_end:
+	.globl callee_len
+	.set callee_len, callee_end - callee
+
+	.section	.note.GNU-stack,"",@progbits
diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.exp b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
new file mode 100644
index 000000000000..ddadab7566ef
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
@@ -0,0 +1,130 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test that GDB can unwind using a .debug_frame section generated by
+# the DWARF assembler.
+#
+# This test is amd64-specific, but could be ported to other
+# architectures if needed.
+
+load_lib dwarf.exp
+
+require dwarf2_support is_x86_64_m64_target
+
+standard_testfile .S -dw.S
+
+# AMD64 DWARF register numbers.
+set rax 0
+set rbp 6
+set rsp 7
+set r12 12
+set r13 13
+set r14 14
+set rip 16
+
+foreach_with_prefix is_64 { false true } {
+    set asm_file [standard_output_file ${testfile}-${is_64}-dw.S]
+
+    Dwarf::assemble $asm_file {
+	frame {
+	    declare_labels cie_label
+
+	    cie_label: CIE {
+		return_address_register $::rip
+		data_alignment_factor -8
+		is_64 $::is_64
+	    } {
+		DW_CFA_def_cfa $::rsp 8
+		DW_CFA_offset $::rip 1
+	    }
+
+	    # FDE for main
+	    FDE $cie_label main main_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc main_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc main_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+	    }
+
+	    # FDE for caller
+	    FDE $cie_label caller caller_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc caller_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc caller_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+	    }
+
+	    # FDE for callee
+	    FDE $cie_label callee callee_len {
+		is_64 $::is_64
+	    } {
+		DW_CFA_set_loc callee_after_push_rbp
+		DW_CFA_def_cfa_offset 16
+		DW_CFA_offset $::rbp 2
+		DW_CFA_set_loc callee_after_set_rbp
+		DW_CFA_def_cfa_register $::rbp
+
+		DW_CFA_set_loc callee_body
+		DW_CFA_offset $::r12 3
+		DW_CFA_register $::r13 $::rax
+
+		# r14's value is computed by an arbitrary expression.
+		DW_CFA_val_expression $::r14 {
+		    DW_OP_constu 0x99aabbcc
+		}
+	    }
+	}
+    }
+
+    if { [prepare_for_testing "failed to prepare" ${testfile}-${is_64} \
+	      [list $srcfile $asm_file] {nodebug}] } {
+	continue
+    }
+
+    # Stop in caller before the call, to capture rbp.
+    if { ![runto caller_call_callee] } {
+	continue
+    }
+
+    set caller_rbp [get_hexadecimal_valueof "\$rbp" "UNKNOWN"]
+
+    # Stop inside callee.
+    gdb_breakpoint callee_body
+    gdb_continue_to_breakpoint "callee_body"
+
+    # Verify backtrace shows the full call chain.
+    gdb_test "bt" "#0.*callee.*\r\n#1.*caller.*\r\n#2.*main.*"
+
+    # Select caller's frame and check saved registers.
+    gdb_test "frame 1" "#1.*caller.*"
+
+    # r12 was saved on the stack by callee.
+    gdb_test "p/x \$r12" "= 0x11223344"
+
+    # r13 was saved in rax by callee.
+    gdb_test "p/x \$r13" "= 0x55667788"
+
+    # r14's value is computed by a DWARF expression.
+    gdb_test "p/x \$r14" "= 0x99aabbcc"
+
+    # rbp should match what caller had.
+    gdb_test "p/x \$rbp" "= ${caller_rbp}"
+}
diff --git a/gdb/testsuite/lib/dwarf.exp b/gdb/testsuite/lib/dwarf.exp
index 9fa5c4e297da..e545019e065b 100644
--- a/gdb/testsuite/lib/dwarf.exp
+++ b/gdb/testsuite/lib/dwarf.exp
@@ -602,6 +602,10 @@ namespace eval Dwarf {
     variable _loc_addr_size
     variable _loc_offset_size
 
+    # Variables used when generating a .debug_frame section.
+    variable _frame_addr_size
+    variable _frame_offset_size
+
     proc _process_one_constant {name value} {
 	variable _constants
 	variable _FORM
@@ -618,7 +622,6 @@ namespace eval Dwarf {
 	}
 
 	# We only try to shorten some very common things.
-	# FIXME: CFA?
 	switch -exact -- $prefix {
 	    TAG {
 		# Create two procedures for the tag.  These call
@@ -667,6 +670,57 @@ namespace eval Dwarf {
 		} $name $name $handler]
 	    }
 
+	    CFA {
+		# Create procs for DW_CFA_* instructions, used in
+		# .debug_frame CIE/FDE bodies.
+
+		# DW_CFA_advance_loc, DW_CFA_offset and
+		# DW_CFA_restore encode the operand in the low 6
+		# bits of the opcode byte.  They need special
+		# handling.
+		switch -exact -- $name {
+		    DW_CFA_advance_loc {
+			proc DW_CFA_advance_loc {delta} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_advance_loc) + $delta" \
+				DW_CFA_advance_loc
+			}
+		    }
+
+		    DW_CFA_offset {
+			proc DW_CFA_offset {register offset} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_offset) + $register" \
+				DW_CFA_offset
+			    _op .uleb128 $offset "offset"
+			}
+		    }
+
+		    DW_CFA_restore {
+			proc DW_CFA_restore {register} {
+			    _op .byte \
+				"$Dwarf::_constants(DW_CFA_restore) + $register" \
+				DW_CFA_restore
+			}
+		    }
+
+		    default {
+			# Standard CFA instruction: emit opcode
+			# byte then delegate to handler.
+			set handler _handle_default_CFA
+			if {[llength [info procs _handle_$name]] > 0} {
+			    set handler _handle_$name
+			}
+
+			# tclint-disable-next-line command-args
+			proc $name {args} [format {
+			    _op .byte $Dwarf::_constants(%s) %s
+			    %s {*}$args
+			} $name $name $handler]
+		    }
+		}
+	    }
+
 	    default {
 		return
 	    }
@@ -1456,6 +1510,127 @@ namespace eval Dwarf {
 	# error.
     }
 
+    # Helper to emit a DWARF expression block (ULEB128 length followed
+    # by the expression bytes) inside a .debug_frame CIE or FDE body.
+    # BODY is a Tcl code containing DW_OP_* calls.
+    proc _emit_cfa_expression {body} {
+	set start [new_label "cfa_expr_start"]
+	set end [new_label "cfa_expr_end"]
+	_op .uleb128 "$end - $start" "expression length"
+	define_label $start
+
+	# Pass 5 as the DWARF version, since we need to pass something, but it
+	# doesn't matter.  The DWARF version is checked only for DW_OP_* ops
+	# that don't make sense in CFI.
+	_location $body 5 $Dwarf::_frame_addr_size $Dwarf::_frame_offset_size
+	define_label $end
+    }
+
+    #
+    # Handlers for DW_CFA_* instructions.
+    #
+    # A handler is only needed if the instruction requires operands.
+    # Generic code handles emitting the opcode byte itself, so a
+    # handler should not do this.
+    #
+    # Handlers are found by name when processing the .def file.  If a
+    # handler isn't found, the default (_handle_default_CFA) is used.
+    #
+
+    proc _handle_default_CFA {} {
+	# Do nothing; if arguments are passed, Tcl will cause an
+	# error.
+    }
+
+    proc _handle_DW_CFA_set_loc {address} {
+	_op .${Dwarf::_frame_addr_size}byte $address "address"
+    }
+
+    proc _handle_DW_CFA_advance_loc1 {delta} {
+	_op .byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_advance_loc2 {delta} {
+	_op .2byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_advance_loc4 {delta} {
+	_op .4byte $delta "delta"
+    }
+
+    proc _handle_DW_CFA_offset_extended {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_restore_extended {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_undefined {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_same_value {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_register {register1 register2} {
+	_op .uleb128 $register1 "register"
+	_op .uleb128 $register2 "register"
+    }
+
+    proc _handle_DW_CFA_def_cfa {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_register {register} {
+	_op .uleb128 $register "register"
+    }
+
+    proc _handle_DW_CFA_def_cfa_offset {offset} {
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_expression {body} {
+	_emit_cfa_expression $body
+    }
+
+    proc _handle_DW_CFA_expression {register body} {
+	_op .uleb128 $register "register"
+	_emit_cfa_expression $body
+    }
+
+    proc _handle_DW_CFA_offset_extended_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_def_cfa_offset_sf {offset} {
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_offset {register offset} {
+	_op .uleb128 $register "register"
+	_op .uleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_offset_sf {register offset} {
+	_op .uleb128 $register "register"
+	_op .sleb128 $offset "offset"
+    }
+
+    proc _handle_DW_CFA_val_expression {register body} {
+	_op .uleb128 $register "register"
+	_emit_cfa_expression $body
+    }
+
     # This is a miniature assembler for location expressions.  It is
     # suitable for use in the attributes to a DIE.
     #
@@ -3554,6 +3729,223 @@ namespace eval Dwarf {
 	debug_str_offsets_end:
     }
 
+    # Emit a DWARF .debug_frame section.
+    #
+    # BODY is Tcl code that emits the CIEs and FDEs which make up the
+    # section.  It is evaluated in the caller's context.
+    #
+    # Within BODY, the following commands are available:
+    #
+    #   CIE options body
+    #     -- emit a Common Information Entry.  See _frame_CIE for details.
+    #
+    #   FDE cie_label initial_location address_range body
+    #     -- emit a Frame Description Entry.  See _frame_FDE for details.
+    proc frame { body } {
+	_section .debug_frame
+
+	with_override Dwarf::CIE Dwarf::_frame_CIE {
+	    with_override Dwarf::FDE Dwarf::_frame_FDE {
+		uplevel $Dwarf::_level $body
+	    }
+	}
+    }
+
+    # Available as proc CIE when in the body of proc debug_frame.
+    #
+    # OPTIONS is a list of option-name/option-value pairs.  Supported
+    # options are (default values are shown in parentheses):
+    #
+    #   is_64 (false)
+    #     -- if true, emit a 64-bit CIE.
+    #
+    #   cie_id (default)
+    #     -- the CIE id value.  When "default", uses 0xffffffff for
+    #        32-bit and 0xffffffffffffffff for 64-bit.  Should typically not be
+    #        used unless trying to craft an invalid CIE.
+    #
+    #   version (4)
+    #     -- the CIE version number.  Note that this is version independent
+    #        from the DWARF version.  DWARF 4 and 5 both use .debug_frame
+    #        version 4.
+    #
+    #   augmentation ("")
+    #     -- the augmentation string.
+    #
+    #   addr_size (default)
+    #     -- the address size in bytes.  When "default", use 8 for 64-bit
+    #        targets and 4 for 32-bit targets.
+    #
+    #   segment_selector_size (0)
+    #     -- the segment selector size in bytes.
+    #
+    #   code_alignment_factor (1)
+    #     -- the code alignment factor.
+    #
+    #   data_alignment_factor (1)
+    #     -- the data alignment factor.
+    #
+    #   return_address_register (0)
+    #     -- the number of the "column" containing the return address.
+    #
+    # BODY is Tcl code that emits the CIE's initial instructions using
+    # DW_CFA_* operations.  It is evaluated in the caller's context.
+    proc _frame_CIE {options body} {
+	parse_options {
+	    { is_64 false }
+	    { cie_id default }
+	    { version 4 }
+	    { augmentation "" }
+	    { addr_size default }
+	    { segment_selector_size 0 }
+	    { code_alignment_factor 1 }
+	    { data_alignment_factor 1 }
+	    { return_address_register 0 }
+	}
+
+	if { $is_64 } {
+	    set Dwarf::_frame_offset_size 8
+	} else {
+	    set Dwarf::_frame_offset_size 4
+	}
+
+	if { $cie_id == "default" } {
+	    if { $is_64 } {
+		set cie_id 0xffffffffffffffff
+	    } else {
+		set cie_id 0xffffffff
+	    }
+	}
+
+	if {$addr_size == "default"} {
+	    if {[is_64_target]} {
+		set Dwarf::_frame_addr_size 8
+	    } else {
+		set Dwarf::_frame_addr_size 4
+	    }
+	} else {
+	    set Dwarf::_frame_addr_size $addr_size
+	}
+
+	declare_labels cie_post_length cie_end
+
+	# Length.
+	if { $is_64 } {
+	    _op .4byte 0xffffffff "length 1/2"
+	    _op .8byte "$cie_end - $cie_post_length" "length 2/2"
+	} else {
+	    _op .4byte "$cie_end - $cie_post_length" "length"
+	}
+
+	define_label $cie_post_length
+
+	# CIE_id
+	_op .${Dwarf::_frame_offset_size}byte $cie_id "CIE_id"
+
+	# Version.
+	_op .byte $version "version"
+
+	# Augmentation string.
+	_op .ascii [_quote $augmentation] "augmentation"
+
+	# Address size.
+	_op .byte $Dwarf::_frame_addr_size "address_size"
+
+	# Segment selector size.
+	_op .byte 0 "segment_size"
+
+	# Code alignment factor.
+	_op .uleb128 $code_alignment_factor "code_alignment_factor"
+
+	# Data alignment factor.
+	_op .sleb128 $data_alignment_factor "data_alignment_factor"
+
+	# Return address register.
+	_op .uleb128 $return_address_register "return_address_register"
+
+	# Initial instructions.
+	uplevel $Dwarf::_level $body
+
+	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
+	_op .align $Dwarf::_frame_addr_size "padding"
+
+	define_label $cie_end
+    }
+
+    # Available as proc FDE when in the body of proc debug_frame.
+    #
+    # CIE_LABEL is the label of the CIE this FDE refers to.
+    #
+    # INITIAL_LOCATION is the address of the first instruction covered
+    # by this FDE.
+    #
+    # ADDRESS_RANGE is the number of bytes of instructions covered by
+    # this FDE.
+    #
+    # OPTIONS is a list of option-name/option-value pairs.  Supported
+    # options are (default values are shown in parentheses):
+    #
+    #   is_64 (false)
+    #     -- if true, emit a 64-bit CIE.
+    #
+    #   addr_size (default)
+    #     -- the address size in bytes.  When "default", use 8 for 64-bit
+    #        targets and 4 for 32-bit targets.
+    #
+    # BODY is Tcl code that emits the FDE's call frame instructions using
+    # DW_CFA_* operations.  It is evaluated in the caller's context.
+    proc _frame_FDE { cie_label initial_location address_range options
+		      body } {
+	parse_options {
+	    { is_64 false }
+	    { addr_size default }
+	}
+
+	if { $is_64 } {
+	    set Dwarf::_frame_offset_size 8
+	} else {
+	    set Dwarf::_frame_offset_size 4
+	}
+
+	if {$addr_size == "default"} {
+	    if {[is_64_target]} {
+		set Dwarf::_frame_addr_size 8
+	    } else {
+		set Dwarf::_frame_addr_size 4
+	    }
+	} else {
+	    set Dwarf::_frame_addr_size $addr_size
+	}
+
+	declare_labels fde_post_length fde_end
+
+	# Length.
+	if { $is_64 } {
+	    _op .4byte 0xffffffff "length 1/2"
+	    _op .8byte "$fde_end - $fde_post_length" "length 2/2"
+	} else {
+	    _op .4byte "$fde_end - $fde_post_length" "length"
+	}
+	define_label $fde_post_length
+
+	# CIE pointer, offset of the CIE into the .debug_frame section.
+	_op .${Dwarf::_frame_offset_size}byte $cie_label "CIE pointer"
+
+	# Initial location.
+	_op .${Dwarf::_frame_addr_size}byte $initial_location "initial_location"
+
+	# Address range.
+	_op .${Dwarf::_frame_addr_size}byte $address_range "address_range"
+
+	# Instructions.
+	uplevel $Dwarf::_level $body
+
+	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
+	_op .align $Dwarf::_frame_addr_size "padding"
+
+	define_label $fde_end
+    }
+
     # The top-level interface to the DWARF assembler.
     # OPTIONS is a list with an even number of elements containing
     # option-name and option-value pairs.

base-commit: bd40fc073ae0c30f8ad40623e9f40da1be09b0e5
-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA
  2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
@ 2026-03-18 20:27     ` simon.marchi
  2026-04-06 18:10       ` Tom Tromey
  2026-04-04  1:25     ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler Simon Marchi
  2026-04-06 17:45     ` Tom Tromey
  2 siblings, 1 reply; 10+ messages in thread
From: simon.marchi @ 2026-03-18 20:27 UTC (permalink / raw)
  To: gdb-patches; +Cc: Simon Marchi

From: Simon Marchi <simon.marchi@polymtl.ca>

New in v2: change how undefined_retaddr is set, to avoid regressions on
AArch64 (among possibly others).

This patch fixes an internal error problem that happens when a frame
description entry does not define the Canonical Frame Address (CFA).
This problem was initially reported downstream as a ROCgdb issue (see
Bug trailer below), but I wrote a reproducer that uses the .debug_frame
functionality added to the DWARF assembler in the previous patch.

The error is:

    /home/smarchi/src/binutils-gdb/gdb/dwarf2/frame.c:1046: internal-error: Unknown CFA rule.

The original bug was encountered while debugging a GPU kernel written
with Triton [1].  From what I understand, the generated kernel does not
really use a stack, so the .debug_frame contents generated is quite
bare:

    $ readelf --debug-dump=frames k
    Contents of the .debug_frame section:

    00000000 000000000000000c ffffffff CIE
      Version:               4
      Augmentation:          ""
      Pointer Size:          8
      Segment Size:          0
      Code alignment factor: 4
      Data alignment factor: 4
      Return address column: 16

      DW_CFA_nop

    00000010 0000000000000014 00000000 FDE cie=00000000 pc=0000000000001600..0000000000001704

For those who don't speak fluent .debug_frame, what we see here is a
Frame Description Entry (FDE) that doesn't define any register rule,
referring to a Common Information Entry (CIE) that also doesn't define
any initial register rule.  This is equivalent to having no unwind
information at all.  One question is: why generate these at all?  I
suppose that this is an edge case, that the compiler is written in a way
that that presumes there will always be some unwind info.  That there is
no "if unwind info is empty, skip emitting the FDE" check.  Anyway, the
important thing for us is that these can be found in the wild, so GDB
shouldn't crash.

The fix consists of handling CFA_UNSET in the dwarf2_frame_cache switch.
CFA_UNSET is the initial state when we start interpreting a CFA program,
meaning that we don't know yet how the CFA is defined.  In our case, it
remains unset after interpreting the CFA program.

With just the fix above, we get:

    (gdb) bt
    #0  0x000055555555511d in main ()
    Backtrace stopped: previous frame identical to this frame (corrupt stack?)

Which is good (better than crashing), but it would be good to avoid the
error.  To do so, set the undefined_retaddr flag to true.  This has
two effects:

 - dwarf2_frame_this_id won't try to build a frame id from the CFA
   (which is good, we don't have a CFA)
 - dwarf2_frame_unwind_stop_reason will return UNWIND_OUTERMOST, which
   is the most accurate thing we can return here (there is no outer
   frame)

The result is the expected:

    (gdb) bt
    #0  0x000055555555511d in main ()

My initial implementation changed this condition:

  if (fs.retaddr_column < fs.regs.reg.size ()
      && fs.regs.reg[fs.retaddr_column].how == DWARF2_FRAME_REG_UNDEFINED)
    cache->undefined_retaddr = true;

such that we would enter it if

    fs.retaddr_column <+ fs.regs.reg.size ()

However, this broke the unwinding on AArch64 (and possibly others).

Add a test case written using the DWARF assembler that reproduces the
issue.

[1] https://triton-lang.org/

Change-Id: I67c717ff03a41c0630a73ce9549d88ff363e8cea
Bug: https://github.com/ROCm/ROCgdb/issues/47
---
 gdb/dwarf2/frame.c                            |  4 ++
 .../gdb.dwarf2/debug-frame-no-cfa.exp         | 54 +++++++++++++++++++
 2 files changed, 58 insertions(+)
 create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp

diff --git a/gdb/dwarf2/frame.c b/gdb/dwarf2/frame.c
index 152bebef0e30..2301d9146373 100644
--- a/gdb/dwarf2/frame.c
+++ b/gdb/dwarf2/frame.c
@@ -962,6 +962,10 @@ dwarf2_frame_cache (const frame_info_ptr &this_frame, void **this_cache)
       /* Calculate the CFA.  */
       switch (fs.regs.cfa_how)
 	{
+	case CFA_UNSET:
+	  cache->undefined_retaddr = true;
+	  return cache;
+
 	case CFA_REG_OFFSET:
 	  cache->cfa = read_addr_from_reg (this_frame, fs.regs.cfa_reg);
 	  if (fs.armcc_cfa_offsets_reversed)
diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp b/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp
new file mode 100644
index 000000000000..8442b5c3ddbb
--- /dev/null
+++ b/gdb/testsuite/gdb.dwarf2/debug-frame-no-cfa.exp
@@ -0,0 +1,54 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test GDB against an FDE in the .debug_frame section that doesn't set a rule
+# for the CFA.
+
+load_lib dwarf.exp
+
+require dwarf2_support is_x86_64_m64_target
+
+standard_testfile main.c -dw.S
+
+set asm_file [standard_output_file $srcfile2]
+Dwarf::assemble $asm_file {
+    get_func_info main
+
+    frame {
+	declare_labels cie_label
+
+	cie_label: CIE {
+	    return_address_register 16
+	} {}
+
+	FDE $cie_label $main_start $main_len {} {
+	}
+    }
+}
+
+if { [prepare_for_testing "failed to prepare" ${testfile} \
+	[list $srcfile $asm_file] {nodebug}] } {
+    return
+}
+
+if { ![runto_main] } {
+    return
+}
+
+# This would cause an internal error in dwarf2_frame_cache.
+#
+# Make sure to match a single line, so that the test fails if an error
+# about stack unwind is printed after frame 0.
+gdb_test "backtrace" "^#0 \[^\r\n\]* main \[^\r\n\]*"
-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler
  2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
  2026-03-18 20:27     ` [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA simon.marchi
@ 2026-04-04  1:25     ` Simon Marchi
  2026-04-06 17:45     ` Tom Tromey
  2 siblings, 0 replies; 10+ messages in thread
From: Simon Marchi @ 2026-04-04  1:25 UTC (permalink / raw)
  To: simon.marchi, gdb-patches; +Cc: Simon Marchi

Ping.

On 2026-03-18 16:27, simon.marchi@polymtl.ca wrote:
> From: Simon Marchi <simon.marchi@efficios.com>
> 
> Add support to the DWARF assembler for generating .debug_frame sections.
> My initial use case is to reproduce a crash happening when encountering
> an empty FDE, but I suppose that other use cases will pop up in the
> future.
> 
>  - Generate procs for the `DW_CFA_*` constants, similar to how the
>    DW_OP_* constants are handled.  These `DW_CFA_*` procs are expected
>    to be used in the CIE and FDE bodies, described below.
> 
>  - Add handlers for `DW_CFA_*` operations that take arguments.  I tried
>    to cover everything that is in DWARF 5.
> 
>  - Add the `frame` proc, used to generate one .debug_frame section.
> 
>  - Add the `_frame_CIE` proc (available as `CIE` in the context of the
>    frame proc), used to generate one Common Information Entry.
> 
>  - Add the `_frame_FDE` proc (available as `FDE` in the context of the
>    frame proc), used to generate one Frame Description Entry.
> 
> Due to the nature of the .debug_frame contents (it describes how
> specific machine registers get saved), I expect that most of
> the tests written using this will be arch-specific.  But  I think it
> will still be useful, as it will let us craft .debug_frame sections to
> look exactly how we want.
> 
> I included a test (gdb.dwarf2/debug-frame.exp), which is more like a
> proof that we can build something useful using this, and can serve as an
> example for whoever wants to write a test case using this in the future.
> 
> Change-Id: I048568ded53883abf52d70139e5cd3e7b4ac3841
> ---
>  gdb/testsuite/gdb.dwarf2/debug-frame.S   | 101 ++++++
>  gdb/testsuite/gdb.dwarf2/debug-frame.exp | 130 ++++++++
>  gdb/testsuite/lib/dwarf.exp              | 394 ++++++++++++++++++++++-
>  3 files changed, 624 insertions(+), 1 deletion(-)
>  create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.S
>  create mode 100644 gdb/testsuite/gdb.dwarf2/debug-frame.exp
> 
> diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.S b/gdb/testsuite/gdb.dwarf2/debug-frame.S
> new file mode 100644
> index 000000000000..231e2dc23704
> --- /dev/null
> +++ b/gdb/testsuite/gdb.dwarf2/debug-frame.S
> @@ -0,0 +1,101 @@
> +/* Copyright 2026 Free Software Foundation, Inc.
> +
> +   This program is free software; you can redistribute it and/or modify
> +   it under the terms of the GNU General Public License as published by
> +   the Free Software Foundation; either version 3 of the License, or
> +   (at your option) any later version.
> +
> +   This program is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +   GNU General Public License for more details.
> +
> +   You should have received a copy of the GNU General Public License
> +   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
> +
> +/* Hand-written x86-64 assembly with no .cfi directives.  The .debug_frame
> +   section is supplied by the companion -dw.S file, generated by the DWARF
> +   assembler.
> +
> +   The call chain: main -> caller -> callee.
> +
> +   caller sets some known register values, and callee saves those registers in
> +   different way.  */
> +
> +	.text
> +
> +/* main */
> +	.globl main
> +	.type main, @function
> +main:
> +	pushq	%rbp
> +	.globl main_after_push_rbp
> +main_after_push_rbp:
> +	movq	%rsp, %rbp
> +	.globl main_after_set_rbp
> +main_after_set_rbp:
> +	call	caller
> +	xorl	%eax, %eax
> +	popq	%rbp
> +	ret
> +	.size main, . - main
> +	.globl main_end
> +main_end:
> +	.globl main_len
> +	.set main_len, main_end - main
> +
> +/* caller */
> +	.globl caller
> +	.type caller, @function
> +caller:
> +	pushq	%rbp
> +	.globl caller_after_push_rbp
> +caller_after_push_rbp:
> +	movq	%rsp, %rbp
> +	.globl caller_after_set_rbp
> +caller_after_set_rbp:
> +	movq	$0x11223344, %r12
> +	movq	$0x55667788, %r13
> +	.globl caller_call_callee
> +caller_call_callee:
> +	call	callee
> +	popq	%rbp
> +	ret
> +	.size caller, . - caller
> +	.globl caller_end
> +caller_end:
> +	.globl caller_len
> +	.set caller_len, caller_end - caller
> +
> +/* callee */
> +	.globl callee
> +	.type callee, @function
> +callee:
> +	pushq	%rbp
> +	.globl callee_after_push_rbp
> +callee_after_push_rbp:
> +	movq	%rsp, %rbp
> +	.globl callee_after_set_rbp
> +callee_after_set_rbp:
> +	/* Save r12 in the stack, then clobber it.  */
> +	pushq	%r12
> +	xorq	%r12, %r12
> +	/* Save r13 in rax, then clobber it.  */
> +	movq	%r13, %rax
> +	xorq	%r13, %r13
> +	/* Clobber r14.  This one is described with a DWARF expression.  */
> +	xorq	%r14, %r14
> +	.globl callee_body
> +callee_body:
> +	nop
> +	movq	%rax, %r13
> +	popq	%r12
> +	popq	%rbp
> +	ret
> +	.size callee, . - callee
> +	.globl callee_end
> +callee_end:
> +	.globl callee_len
> +	.set callee_len, callee_end - callee
> +
> +	.section	.note.GNU-stack,"",@progbits
> diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.exp b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
> new file mode 100644
> index 000000000000..ddadab7566ef
> --- /dev/null
> +++ b/gdb/testsuite/gdb.dwarf2/debug-frame.exp
> @@ -0,0 +1,130 @@
> +# Copyright 2026 Free Software Foundation, Inc.
> +
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 3 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +
> +# Test that GDB can unwind using a .debug_frame section generated by
> +# the DWARF assembler.
> +#
> +# This test is amd64-specific, but could be ported to other
> +# architectures if needed.
> +
> +load_lib dwarf.exp
> +
> +require dwarf2_support is_x86_64_m64_target
> +
> +standard_testfile .S -dw.S
> +
> +# AMD64 DWARF register numbers.
> +set rax 0
> +set rbp 6
> +set rsp 7
> +set r12 12
> +set r13 13
> +set r14 14
> +set rip 16
> +
> +foreach_with_prefix is_64 { false true } {
> +    set asm_file [standard_output_file ${testfile}-${is_64}-dw.S]
> +
> +    Dwarf::assemble $asm_file {
> +	frame {
> +	    declare_labels cie_label
> +
> +	    cie_label: CIE {
> +		return_address_register $::rip
> +		data_alignment_factor -8
> +		is_64 $::is_64
> +	    } {
> +		DW_CFA_def_cfa $::rsp 8
> +		DW_CFA_offset $::rip 1
> +	    }
> +
> +	    # FDE for main
> +	    FDE $cie_label main main_len {
> +		is_64 $::is_64
> +	    } {
> +		DW_CFA_set_loc main_after_push_rbp
> +		DW_CFA_def_cfa_offset 16
> +		DW_CFA_offset $::rbp 2
> +		DW_CFA_set_loc main_after_set_rbp
> +		DW_CFA_def_cfa_register $::rbp
> +	    }
> +
> +	    # FDE for caller
> +	    FDE $cie_label caller caller_len {
> +		is_64 $::is_64
> +	    } {
> +		DW_CFA_set_loc caller_after_push_rbp
> +		DW_CFA_def_cfa_offset 16
> +		DW_CFA_offset $::rbp 2
> +		DW_CFA_set_loc caller_after_set_rbp
> +		DW_CFA_def_cfa_register $::rbp
> +	    }
> +
> +	    # FDE for callee
> +	    FDE $cie_label callee callee_len {
> +		is_64 $::is_64
> +	    } {
> +		DW_CFA_set_loc callee_after_push_rbp
> +		DW_CFA_def_cfa_offset 16
> +		DW_CFA_offset $::rbp 2
> +		DW_CFA_set_loc callee_after_set_rbp
> +		DW_CFA_def_cfa_register $::rbp
> +
> +		DW_CFA_set_loc callee_body
> +		DW_CFA_offset $::r12 3
> +		DW_CFA_register $::r13 $::rax
> +
> +		# r14's value is computed by an arbitrary expression.
> +		DW_CFA_val_expression $::r14 {
> +		    DW_OP_constu 0x99aabbcc
> +		}
> +	    }
> +	}
> +    }
> +
> +    if { [prepare_for_testing "failed to prepare" ${testfile}-${is_64} \
> +	      [list $srcfile $asm_file] {nodebug}] } {
> +	continue
> +    }
> +
> +    # Stop in caller before the call, to capture rbp.
> +    if { ![runto caller_call_callee] } {
> +	continue
> +    }
> +
> +    set caller_rbp [get_hexadecimal_valueof "\$rbp" "UNKNOWN"]
> +
> +    # Stop inside callee.
> +    gdb_breakpoint callee_body
> +    gdb_continue_to_breakpoint "callee_body"
> +
> +    # Verify backtrace shows the full call chain.
> +    gdb_test "bt" "#0.*callee.*\r\n#1.*caller.*\r\n#2.*main.*"
> +
> +    # Select caller's frame and check saved registers.
> +    gdb_test "frame 1" "#1.*caller.*"
> +
> +    # r12 was saved on the stack by callee.
> +    gdb_test "p/x \$r12" "= 0x11223344"
> +
> +    # r13 was saved in rax by callee.
> +    gdb_test "p/x \$r13" "= 0x55667788"
> +
> +    # r14's value is computed by a DWARF expression.
> +    gdb_test "p/x \$r14" "= 0x99aabbcc"
> +
> +    # rbp should match what caller had.
> +    gdb_test "p/x \$rbp" "= ${caller_rbp}"
> +}
> diff --git a/gdb/testsuite/lib/dwarf.exp b/gdb/testsuite/lib/dwarf.exp
> index 9fa5c4e297da..e545019e065b 100644
> --- a/gdb/testsuite/lib/dwarf.exp
> +++ b/gdb/testsuite/lib/dwarf.exp
> @@ -602,6 +602,10 @@ namespace eval Dwarf {
>      variable _loc_addr_size
>      variable _loc_offset_size
>  
> +    # Variables used when generating a .debug_frame section.
> +    variable _frame_addr_size
> +    variable _frame_offset_size
> +
>      proc _process_one_constant {name value} {
>  	variable _constants
>  	variable _FORM
> @@ -618,7 +622,6 @@ namespace eval Dwarf {
>  	}
>  
>  	# We only try to shorten some very common things.
> -	# FIXME: CFA?
>  	switch -exact -- $prefix {
>  	    TAG {
>  		# Create two procedures for the tag.  These call
> @@ -667,6 +670,57 @@ namespace eval Dwarf {
>  		} $name $name $handler]
>  	    }
>  
> +	    CFA {
> +		# Create procs for DW_CFA_* instructions, used in
> +		# .debug_frame CIE/FDE bodies.
> +
> +		# DW_CFA_advance_loc, DW_CFA_offset and
> +		# DW_CFA_restore encode the operand in the low 6
> +		# bits of the opcode byte.  They need special
> +		# handling.
> +		switch -exact -- $name {
> +		    DW_CFA_advance_loc {
> +			proc DW_CFA_advance_loc {delta} {
> +			    _op .byte \
> +				"$Dwarf::_constants(DW_CFA_advance_loc) + $delta" \
> +				DW_CFA_advance_loc
> +			}
> +		    }
> +
> +		    DW_CFA_offset {
> +			proc DW_CFA_offset {register offset} {
> +			    _op .byte \
> +				"$Dwarf::_constants(DW_CFA_offset) + $register" \
> +				DW_CFA_offset
> +			    _op .uleb128 $offset "offset"
> +			}
> +		    }
> +
> +		    DW_CFA_restore {
> +			proc DW_CFA_restore {register} {
> +			    _op .byte \
> +				"$Dwarf::_constants(DW_CFA_restore) + $register" \
> +				DW_CFA_restore
> +			}
> +		    }
> +
> +		    default {
> +			# Standard CFA instruction: emit opcode
> +			# byte then delegate to handler.
> +			set handler _handle_default_CFA
> +			if {[llength [info procs _handle_$name]] > 0} {
> +			    set handler _handle_$name
> +			}
> +
> +			# tclint-disable-next-line command-args
> +			proc $name {args} [format {
> +			    _op .byte $Dwarf::_constants(%s) %s
> +			    %s {*}$args
> +			} $name $name $handler]
> +		    }
> +		}
> +	    }
> +
>  	    default {
>  		return
>  	    }
> @@ -1456,6 +1510,127 @@ namespace eval Dwarf {
>  	# error.
>      }
>  
> +    # Helper to emit a DWARF expression block (ULEB128 length followed
> +    # by the expression bytes) inside a .debug_frame CIE or FDE body.
> +    # BODY is a Tcl code containing DW_OP_* calls.
> +    proc _emit_cfa_expression {body} {
> +	set start [new_label "cfa_expr_start"]
> +	set end [new_label "cfa_expr_end"]
> +	_op .uleb128 "$end - $start" "expression length"
> +	define_label $start
> +
> +	# Pass 5 as the DWARF version, since we need to pass something, but it
> +	# doesn't matter.  The DWARF version is checked only for DW_OP_* ops
> +	# that don't make sense in CFI.
> +	_location $body 5 $Dwarf::_frame_addr_size $Dwarf::_frame_offset_size
> +	define_label $end
> +    }
> +
> +    #
> +    # Handlers for DW_CFA_* instructions.
> +    #
> +    # A handler is only needed if the instruction requires operands.
> +    # Generic code handles emitting the opcode byte itself, so a
> +    # handler should not do this.
> +    #
> +    # Handlers are found by name when processing the .def file.  If a
> +    # handler isn't found, the default (_handle_default_CFA) is used.
> +    #
> +
> +    proc _handle_default_CFA {} {
> +	# Do nothing; if arguments are passed, Tcl will cause an
> +	# error.
> +    }
> +
> +    proc _handle_DW_CFA_set_loc {address} {
> +	_op .${Dwarf::_frame_addr_size}byte $address "address"
> +    }
> +
> +    proc _handle_DW_CFA_advance_loc1 {delta} {
> +	_op .byte $delta "delta"
> +    }
> +
> +    proc _handle_DW_CFA_advance_loc2 {delta} {
> +	_op .2byte $delta "delta"
> +    }
> +
> +    proc _handle_DW_CFA_advance_loc4 {delta} {
> +	_op .4byte $delta "delta"
> +    }
> +
> +    proc _handle_DW_CFA_offset_extended {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .uleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_restore_extended {register} {
> +	_op .uleb128 $register "register"
> +    }
> +
> +    proc _handle_DW_CFA_undefined {register} {
> +	_op .uleb128 $register "register"
> +    }
> +
> +    proc _handle_DW_CFA_same_value {register} {
> +	_op .uleb128 $register "register"
> +    }
> +
> +    proc _handle_DW_CFA_register {register1 register2} {
> +	_op .uleb128 $register1 "register"
> +	_op .uleb128 $register2 "register"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .uleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa_register {register} {
> +	_op .uleb128 $register "register"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa_offset {offset} {
> +	_op .uleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa_expression {body} {
> +	_emit_cfa_expression $body
> +    }
> +
> +    proc _handle_DW_CFA_expression {register body} {
> +	_op .uleb128 $register "register"
> +	_emit_cfa_expression $body
> +    }
> +
> +    proc _handle_DW_CFA_offset_extended_sf {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .sleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa_sf {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .sleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_def_cfa_offset_sf {offset} {
> +	_op .sleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_val_offset {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .uleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_val_offset_sf {register offset} {
> +	_op .uleb128 $register "register"
> +	_op .sleb128 $offset "offset"
> +    }
> +
> +    proc _handle_DW_CFA_val_expression {register body} {
> +	_op .uleb128 $register "register"
> +	_emit_cfa_expression $body
> +    }
> +
>      # This is a miniature assembler for location expressions.  It is
>      # suitable for use in the attributes to a DIE.
>      #
> @@ -3554,6 +3729,223 @@ namespace eval Dwarf {
>  	debug_str_offsets_end:
>      }
>  
> +    # Emit a DWARF .debug_frame section.
> +    #
> +    # BODY is Tcl code that emits the CIEs and FDEs which make up the
> +    # section.  It is evaluated in the caller's context.
> +    #
> +    # Within BODY, the following commands are available:
> +    #
> +    #   CIE options body
> +    #     -- emit a Common Information Entry.  See _frame_CIE for details.
> +    #
> +    #   FDE cie_label initial_location address_range body
> +    #     -- emit a Frame Description Entry.  See _frame_FDE for details.
> +    proc frame { body } {
> +	_section .debug_frame
> +
> +	with_override Dwarf::CIE Dwarf::_frame_CIE {
> +	    with_override Dwarf::FDE Dwarf::_frame_FDE {
> +		uplevel $Dwarf::_level $body
> +	    }
> +	}
> +    }
> +
> +    # Available as proc CIE when in the body of proc debug_frame.
> +    #
> +    # OPTIONS is a list of option-name/option-value pairs.  Supported
> +    # options are (default values are shown in parentheses):
> +    #
> +    #   is_64 (false)
> +    #     -- if true, emit a 64-bit CIE.
> +    #
> +    #   cie_id (default)
> +    #     -- the CIE id value.  When "default", uses 0xffffffff for
> +    #        32-bit and 0xffffffffffffffff for 64-bit.  Should typically not be
> +    #        used unless trying to craft an invalid CIE.
> +    #
> +    #   version (4)
> +    #     -- the CIE version number.  Note that this is version independent
> +    #        from the DWARF version.  DWARF 4 and 5 both use .debug_frame
> +    #        version 4.
> +    #
> +    #   augmentation ("")
> +    #     -- the augmentation string.
> +    #
> +    #   addr_size (default)
> +    #     -- the address size in bytes.  When "default", use 8 for 64-bit
> +    #        targets and 4 for 32-bit targets.
> +    #
> +    #   segment_selector_size (0)
> +    #     -- the segment selector size in bytes.
> +    #
> +    #   code_alignment_factor (1)
> +    #     -- the code alignment factor.
> +    #
> +    #   data_alignment_factor (1)
> +    #     -- the data alignment factor.
> +    #
> +    #   return_address_register (0)
> +    #     -- the number of the "column" containing the return address.
> +    #
> +    # BODY is Tcl code that emits the CIE's initial instructions using
> +    # DW_CFA_* operations.  It is evaluated in the caller's context.
> +    proc _frame_CIE {options body} {
> +	parse_options {
> +	    { is_64 false }
> +	    { cie_id default }
> +	    { version 4 }
> +	    { augmentation "" }
> +	    { addr_size default }
> +	    { segment_selector_size 0 }
> +	    { code_alignment_factor 1 }
> +	    { data_alignment_factor 1 }
> +	    { return_address_register 0 }
> +	}
> +
> +	if { $is_64 } {
> +	    set Dwarf::_frame_offset_size 8
> +	} else {
> +	    set Dwarf::_frame_offset_size 4
> +	}
> +
> +	if { $cie_id == "default" } {
> +	    if { $is_64 } {
> +		set cie_id 0xffffffffffffffff
> +	    } else {
> +		set cie_id 0xffffffff
> +	    }
> +	}
> +
> +	if {$addr_size == "default"} {
> +	    if {[is_64_target]} {
> +		set Dwarf::_frame_addr_size 8
> +	    } else {
> +		set Dwarf::_frame_addr_size 4
> +	    }
> +	} else {
> +	    set Dwarf::_frame_addr_size $addr_size
> +	}
> +
> +	declare_labels cie_post_length cie_end
> +
> +	# Length.
> +	if { $is_64 } {
> +	    _op .4byte 0xffffffff "length 1/2"
> +	    _op .8byte "$cie_end - $cie_post_length" "length 2/2"
> +	} else {
> +	    _op .4byte "$cie_end - $cie_post_length" "length"
> +	}
> +
> +	define_label $cie_post_length
> +
> +	# CIE_id
> +	_op .${Dwarf::_frame_offset_size}byte $cie_id "CIE_id"
> +
> +	# Version.
> +	_op .byte $version "version"
> +
> +	# Augmentation string.
> +	_op .ascii [_quote $augmentation] "augmentation"
> +
> +	# Address size.
> +	_op .byte $Dwarf::_frame_addr_size "address_size"
> +
> +	# Segment selector size.
> +	_op .byte 0 "segment_size"
> +
> +	# Code alignment factor.
> +	_op .uleb128 $code_alignment_factor "code_alignment_factor"
> +
> +	# Data alignment factor.
> +	_op .sleb128 $data_alignment_factor "data_alignment_factor"
> +
> +	# Return address register.
> +	_op .uleb128 $return_address_register "return_address_register"
> +
> +	# Initial instructions.
> +	uplevel $Dwarf::_level $body
> +
> +	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
> +	_op .align $Dwarf::_frame_addr_size "padding"
> +
> +	define_label $cie_end
> +    }
> +
> +    # Available as proc FDE when in the body of proc debug_frame.
> +    #
> +    # CIE_LABEL is the label of the CIE this FDE refers to.
> +    #
> +    # INITIAL_LOCATION is the address of the first instruction covered
> +    # by this FDE.
> +    #
> +    # ADDRESS_RANGE is the number of bytes of instructions covered by
> +    # this FDE.
> +    #
> +    # OPTIONS is a list of option-name/option-value pairs.  Supported
> +    # options are (default values are shown in parentheses):
> +    #
> +    #   is_64 (false)
> +    #     -- if true, emit a 64-bit CIE.
> +    #
> +    #   addr_size (default)
> +    #     -- the address size in bytes.  When "default", use 8 for 64-bit
> +    #        targets and 4 for 32-bit targets.
> +    #
> +    # BODY is Tcl code that emits the FDE's call frame instructions using
> +    # DW_CFA_* operations.  It is evaluated in the caller's context.
> +    proc _frame_FDE { cie_label initial_location address_range options
> +		      body } {
> +	parse_options {
> +	    { is_64 false }
> +	    { addr_size default }
> +	}
> +
> +	if { $is_64 } {
> +	    set Dwarf::_frame_offset_size 8
> +	} else {
> +	    set Dwarf::_frame_offset_size 4
> +	}
> +
> +	if {$addr_size == "default"} {
> +	    if {[is_64_target]} {
> +		set Dwarf::_frame_addr_size 8
> +	    } else {
> +		set Dwarf::_frame_addr_size 4
> +	    }
> +	} else {
> +	    set Dwarf::_frame_addr_size $addr_size
> +	}
> +
> +	declare_labels fde_post_length fde_end
> +
> +	# Length.
> +	if { $is_64 } {
> +	    _op .4byte 0xffffffff "length 1/2"
> +	    _op .8byte "$fde_end - $fde_post_length" "length 2/2"
> +	} else {
> +	    _op .4byte "$fde_end - $fde_post_length" "length"
> +	}
> +	define_label $fde_post_length
> +
> +	# CIE pointer, offset of the CIE into the .debug_frame section.
> +	_op .${Dwarf::_frame_offset_size}byte $cie_label "CIE pointer"
> +
> +	# Initial location.
> +	_op .${Dwarf::_frame_addr_size}byte $initial_location "initial_location"
> +
> +	# Address range.
> +	_op .${Dwarf::_frame_addr_size}byte $address_range "address_range"
> +
> +	# Instructions.
> +	uplevel $Dwarf::_level $body
> +
> +	# Padding up to the address size.  Fill with DW_CFA_nop (zeroes).
> +	_op .align $Dwarf::_frame_addr_size "padding"
> +
> +	define_label $fde_end
> +    }
> +
>      # The top-level interface to the DWARF assembler.
>      # OPTIONS is a list with an even number of elements containing
>      # option-name and option-value pairs.
> 
> base-commit: bd40fc073ae0c30f8ad40623e9f40da1be09b0e5


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler
  2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
  2026-03-18 20:27     ` [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA simon.marchi
  2026-04-04  1:25     ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler Simon Marchi
@ 2026-04-06 17:45     ` Tom Tromey
  2026-04-11  2:46       ` Simon Marchi
  2 siblings, 1 reply; 10+ messages in thread
From: Tom Tromey @ 2026-04-06 17:45 UTC (permalink / raw)
  To: simon.marchi; +Cc: gdb-patches, Simon Marchi

>>>>> "Simon" == simon marchi <simon.marchi@polymtl.ca> writes:

Simon> Add support to the DWARF assembler for generating .debug_frame sections.
Simon> My initial use case is to reproduce a crash happening when encountering
Simon> an empty FDE, but I suppose that other use cases will pop up in the
Simon> future.

Thanks for doing this.  I have one little note.

Simon> +		# Create procs for DW_CFA_* instructions, used in
Simon> +		# .debug_frame CIE/FDE bodies.
Simon> +
Simon> +		# DW_CFA_advance_loc, DW_CFA_offset and
Simon> +		# DW_CFA_restore encode the operand in the low 6
Simon> +		# bits of the opcode byte.  They need special
Simon> +		# handling.
Simon> +		switch -exact -- $name {
Simon> +		    DW_CFA_advance_loc {
Simon> +			proc DW_CFA_advance_loc {delta} {
Simon> +			    _op .byte \
Simon> +				"$Dwarf::_constants(DW_CFA_advance_loc) + $delta" \
Simon> +				DW_CFA_advance_loc
Simon> +			}
Simon> +		    }
Simon> +
Simon> +		    DW_CFA_offset {
Simon> +			proc DW_CFA_offset {register offset} {
Simon> +			    _op .byte \
Simon> +				"$Dwarf::_constants(DW_CFA_offset) + $register" \
Simon> +				DW_CFA_offset
Simon> +			    _op .uleb128 $offset "offset"
Simon> +			}
Simon> +		    }
Simon> +
Simon> +		    DW_CFA_restore {
Simon> +			proc DW_CFA_restore {register} {
Simon> +			    _op .byte \
Simon> +				"$Dwarf::_constants(DW_CFA_restore) + $register" \
Simon> +				DW_CFA_restore
Simon> +			}
Simon> +		    }

These procs could just be defined at the namespace level and then these
constants just skipped here, like

    DW_CFA_advance_loc -
    DW_CFA_offset -
    DW_CFA_restore {
        # These need special handling and are predefined below.
    }

I think this would be a bit clearer.

Approved-By: Tom Tromey <tom@tromey.com>

thanks,
Tom

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA
  2026-03-18 20:27     ` [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA simon.marchi
@ 2026-04-06 18:10       ` Tom Tromey
  2026-04-11  2:47         ` Simon Marchi
  0 siblings, 1 reply; 10+ messages in thread
From: Tom Tromey @ 2026-04-06 18:10 UTC (permalink / raw)
  To: simon.marchi; +Cc: gdb-patches

>>>>> "Simon" == simon marchi <simon.marchi@polymtl.ca> writes:

Simon> The fix consists of handling CFA_UNSET in the dwarf2_frame_cache switch.
Simon> CFA_UNSET is the initial state when we start interpreting a CFA program,
Simon> meaning that we don't know yet how the CFA is defined.  In our case, it
Simon> remains unset after interpreting the CFA program.

This makes sense to me, thank you.
Approved-By: Tom Tromey <tom@tromey.com>

Tom

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler
  2026-04-06 17:45     ` Tom Tromey
@ 2026-04-11  2:46       ` Simon Marchi
  0 siblings, 0 replies; 10+ messages in thread
From: Simon Marchi @ 2026-04-11  2:46 UTC (permalink / raw)
  To: Tom Tromey, simon.marchi; +Cc: gdb-patches



On 2026-04-06 13:45, Tom Tromey wrote:
>>>>>> "Simon" == simon marchi <simon.marchi@polymtl.ca> writes:
> 
> Simon> Add support to the DWARF assembler for generating .debug_frame sections.
> Simon> My initial use case is to reproduce a crash happening when encountering
> Simon> an empty FDE, but I suppose that other use cases will pop up in the
> Simon> future.
> 
> Thanks for doing this.  I have one little note.
> 
> Simon> +		# Create procs for DW_CFA_* instructions, used in
> Simon> +		# .debug_frame CIE/FDE bodies.
> Simon> +
> Simon> +		# DW_CFA_advance_loc, DW_CFA_offset and
> Simon> +		# DW_CFA_restore encode the operand in the low 6
> Simon> +		# bits of the opcode byte.  They need special
> Simon> +		# handling.
> Simon> +		switch -exact -- $name {
> Simon> +		    DW_CFA_advance_loc {
> Simon> +			proc DW_CFA_advance_loc {delta} {
> Simon> +			    _op .byte \
> Simon> +				"$Dwarf::_constants(DW_CFA_advance_loc) + $delta" \
> Simon> +				DW_CFA_advance_loc
> Simon> +			}
> Simon> +		    }
> Simon> +
> Simon> +		    DW_CFA_offset {
> Simon> +			proc DW_CFA_offset {register offset} {
> Simon> +			    _op .byte \
> Simon> +				"$Dwarf::_constants(DW_CFA_offset) + $register" \
> Simon> +				DW_CFA_offset
> Simon> +			    _op .uleb128 $offset "offset"
> Simon> +			}
> Simon> +		    }
> Simon> +
> Simon> +		    DW_CFA_restore {
> Simon> +			proc DW_CFA_restore {register} {
> Simon> +			    _op .byte \
> Simon> +				"$Dwarf::_constants(DW_CFA_restore) + $register" \
> Simon> +				DW_CFA_restore
> Simon> +			}
> Simon> +		    }
> 
> These procs could just be defined at the namespace level and then these
> constants just skipped here, like
> 
>     DW_CFA_advance_loc -
>     DW_CFA_offset -
>     DW_CFA_restore {
>         # These need special handling and are predefined below.
>     }
> 
> I think this would be a bit clearer.
>
> Approved-By: Tom Tromey <tom@tromey.com>

Ok, I see what you mean, I did that change.

Thanks,

Simon

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA
  2026-04-06 18:10       ` Tom Tromey
@ 2026-04-11  2:47         ` Simon Marchi
  0 siblings, 0 replies; 10+ messages in thread
From: Simon Marchi @ 2026-04-11  2:47 UTC (permalink / raw)
  To: Tom Tromey; +Cc: gdb-patches



On 2026-04-06 14:10, Tom Tromey wrote:
>>>>>> "Simon" == simon marchi <simon.marchi@polymtl.ca> writes:
> 
> Simon> The fix consists of handling CFA_UNSET in the dwarf2_frame_cache switch.
> Simon> CFA_UNSET is the initial state when we start interpreting a CFA program,
> Simon> meaning that we don't know yet how the CFA is defined.  In our case, it
> Simon> remains unset after interpreting the CFA program.
> 
> This makes sense to me, thank you.
> Approved-By: Tom Tromey <tom@tromey.com>
> 
> Tom

Thanks, will push both patches.

Simon

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-04-11  2:47 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-16 17:22 [PATCH 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler Simon Marchi
2026-03-16 17:22 ` [PATCH 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA Simon Marchi
2026-03-17 19:33   ` Simon Marchi
2026-03-18 20:27   ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler simon.marchi
2026-03-18 20:27     ` [PATCH v2 2/2] gdb/dwarf: fix internal error when FDEs do not describe the CFA simon.marchi
2026-04-06 18:10       ` Tom Tromey
2026-04-11  2:47         ` Simon Marchi
2026-04-04  1:25     ` [PATCH v2 1/2] gdb/testsuite: add .debug_frame support in DWARF assembler Simon Marchi
2026-04-06 17:45     ` Tom Tromey
2026-04-11  2:46       ` Simon Marchi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox