]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/testsuite: add .debug_frame support in DWARF assembler
authorSimon Marchi <simon.marchi@efficios.com>
Wed, 18 Mar 2026 20:27:22 +0000 (16:27 -0400)
committerSimon Marchi <simon.marchi@polymtl.ca>
Sat, 11 Apr 2026 02:45:10 +0000 (22:45 -0400)
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
Approved-By: Tom Tromey <tom@tromey.com>
gdb/testsuite/gdb.dwarf2/debug-frame.S [new file with mode: 0644]
gdb/testsuite/gdb.dwarf2/debug-frame.exp [new file with mode: 0644]
gdb/testsuite/lib/dwarf.exp

diff --git a/gdb/testsuite/gdb.dwarf2/debug-frame.S b/gdb/testsuite/gdb.dwarf2/debug-frame.S
new file mode 100644 (file)
index 0000000..231e2dc
--- /dev/null
@@ -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 (file)
index 0000000..ddadab7
--- /dev/null
@@ -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}"
+}
index b4a6ecc39ffb689aea3f83c6323adcd6eb34f4ef..5358dc317a18a37e1a397cdc3e89150cc51b965b 100644 (file)
@@ -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,37 @@ 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 and are predefined below.
+               switch -exact -- $name {
+                   DW_CFA_advance_loc -
+                   DW_CFA_offset -
+                   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 +1490,146 @@ namespace eval Dwarf {
        # error.
     }
 
+    # 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, so they are defined here rather than generated.
+    proc DW_CFA_advance_loc {delta} {
+       _op .byte \
+           "$Dwarf::_constants(DW_CFA_advance_loc) + $delta" DW_CFA_advance_loc
+    }
+
+    proc DW_CFA_offset {register offset} {
+       _op .byte \
+           "$Dwarf::_constants(DW_CFA_offset) + $register" DW_CFA_offset
+       _op .uleb128 $offset "offset"
+    }
+
+    proc DW_CFA_restore {register} {
+       _op .byte \
+           "$Dwarf::_constants(DW_CFA_restore) + $register" DW_CFA_restore
+    }
+
+    # 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.
     #
@@ -3556,6 +3730,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.