blob: 5c6ec7a2d4ec6ba71b764c7ea565ceae7d3f7a9d [file]
# Copyright 2024-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/>.
# Define an inline function `foo` within the function `main`. The
# function `foo` uses DW_AT_ranges to define its ranges. One of the
# sub-ranges for foo will be empty.
#
# An empty sub-range should indicate that there is no code associated
# with `foo` at that address, however, with gcc versions at least
# between 8.x and 14.x (latest at the time of writing this comment),
# it is observed that when these empty sub-ranges are created for an
# inline function, if GDB treats the sub-range as non-empty, and stops
# at that location, then this generally gives a better debug
# experience. It is often still possible to read local variables at
# that address.
#
# This function defines an inline function, places a breakpoint on its
# entry-pc, and then runs and expects GDB to stop, and report the stop
# as being inside the inline function.
#
# We then check that the next outer frame is `main` as expected, and
# that the block for `foo` has the expected sub-ranges.
#
# We compile a variety of different configurations, broadly there are
# two variables, the location of the empty sub-range, and whether the
# entry-pc points at the empty sub-range or not.
#
# The empty sub-range location, the empty sub-range can be the sub-range
# at the lowest address, highest address, or can be somewhere between a
# blocks low and high addresses.
load_lib dwarf.exp
require dwarf2_support
standard_testfile .c .S
# Lines we reference in the generated DWARF.
set main_decl_line [gdb_get_line_number "main decl line"]
set foo_call_line [gdb_get_line_number "foo call line"]
get_func_info main
# Compile the source file and load the executable into GDB so we can
# extract some addresses needed for creating the DWARF.
#
# Use `nopie` to ensure that addresses are the same across runs, in case ASLR
# can't be disabled.
if { [prepare_for_testing "failed to prepare" ${testfile} \
[list ${srcfile}] {debug nopie}] } {
return
}
# Some addresses that we need when generating the DWARF.
for { set i 0 } { $i < 9 } { incr i } {
set main_$i [get_hexadecimal_valueof "&main_$i" "UNKNOWN" \
"get address for main_$i"]
}
# Create the DWARF assembler file into ASM_FILE. Using DWARF_VERSION
# to define which style of ranges to create. FUNC_RANGES is a list of
# 6 entries, each of which is an address, used to create the ranges
# for the inline function DIE. The ENTRY_PC is also an address and is
# used for the DW_AT_entry_pc of the inlined function.
proc write_asm_file { asm_file dwarf_version func_ranges entry_pc } {
Dwarf::assemble $asm_file {
upvar entry_label entry_label
upvar dwarf_version dwarf_version
upvar func_ranges func_ranges
upvar entry_pc entry_pc
declare_labels lines_table inline_func ranges_label
cu { version $dwarf_version } {
DW_TAG_compile_unit {
DW_AT_producer "GNU C 14.1.0"
DW_AT_language @DW_LANG_C
DW_AT_name $::srcfile
DW_AT_comp_dir /tmp
DW_AT_low_pc 0 addr
DW_AT_stmt_list $lines_table DW_FORM_sec_offset
} {
inline_func: subprogram {
DW_AT_name foo
DW_AT_inline @DW_INL_declared_inlined
}
subprogram {
DW_AT_name main
DW_AT_decl_file 1 data1
DW_AT_decl_line $::main_decl_line data1
DW_AT_decl_column 1 data1
DW_AT_low_pc $::main_start addr
DW_AT_high_pc $::main_len data4
DW_AT_external 1 flag
} {
inlined_subroutine {
DW_AT_abstract_origin %$inline_func
DW_AT_call_file 1 data1
DW_AT_call_line $::foo_call_line data1
DW_AT_entry_pc $entry_pc addr
DW_AT_ranges $ranges_label DW_FORM_sec_offset
}
}
}
}
lines {version 2} lines_table {
include_dir "$::srcdir/$::subdir"
file_name "$::srcfile" 1
}
if { $dwarf_version == 5 } {
rnglists {} {
table {} {
ranges_label: list_ {
start_end [lindex $func_ranges 0] [lindex $func_ranges 1]
start_end [lindex $func_ranges 2] [lindex $func_ranges 3]
start_end [lindex $func_ranges 4] [lindex $func_ranges 5]
}
}
}
} else {
ranges { } {
ranges_label: sequence {
range [lindex $func_ranges 0] [lindex $func_ranges 1]
range [lindex $func_ranges 2] [lindex $func_ranges 3]
range [lindex $func_ranges 4] [lindex $func_ranges 5]
}
}
}
}
}
# Gobal used to give each generated binary a unique name.
set test_id 0
proc run_test { dwarf_version empty_loc entry_pc_type } {
incr ::test_id
set this_testfile $::testfile-$::test_id
set asm_file [standard_output_file $this_testfile.S]
if { $empty_loc eq "start" } {
set ranges [list \
main_1 main_1 \
main_3 main_4 \
main_6 main_7]
set entry_pc_choices [list main_1 main_3]
} elseif { $empty_loc eq "middle" } {
set ranges [list \
main_1 main_2 \
main_4 main_4 \
main_6 main_7]
set entry_pc_choices [list main_4 main_1]
} elseif { $empty_loc eq "end" } {
set ranges [list \
main_1 main_2 \
main_4 main_5 \
main_7 main_7]
set entry_pc_choices [list main_7 main_1]
} else {
error "unknown location for empty range '$empty_loc'"
}
if { $entry_pc_type eq "empty" } {
set entry_pc_label [lindex $entry_pc_choices 0]
} elseif { $entry_pc_type eq "non_empty" } {
set entry_pc_label [lindex $entry_pc_choices 1]
} else {
error "unknown entry-pc type '$entry_pc_type'"
}
write_asm_file $asm_file $dwarf_version $ranges $entry_pc_label
if {[prepare_for_testing "failed to prepare" $this_testfile \
[list $::srcfile $asm_file] {nodebug nopie}]} {
return
}
if {![runto_main]} {
return
}
# Continue until we stop in 'foo'.
gdb_breakpoint foo
gdb_test "continue" \
"Breakpoint $::decimal, $::hex in foo \\(\\)" \
"continue to b/p in foo"
# Check we stopped at the entry-pc.
set pc [get_hexadecimal_valueof "\$pc" "*UNKNOWN*" \
"get \$pc at breakpoint"]
set entry_pc [set ::$entry_pc_label]
gdb_assert { $pc == $entry_pc } "stopped at entry-pc"
# The block's expected overall low/high addresses.
set block_start [set ::[lindex $ranges 0]]
set block_end [set ::[lindex $ranges 5]]
# Setup variables r{0,1,2}s, r{0,1,2}e, to represent ranges start
# and end addresses. These are extracted from the RANGES
# variable. However, RANGES includes the empty ranges, so spot
# the empty ranges and update the end address as GDB does.
#
# Also, if the empty range is at the end of the block, then the
# block's overall end address also needs adjusting.
for { set i 0 } { $i < 3 } { incr i } {
set start [set ::[lindex $ranges [expr {$i * 2}]]]
set end [set ::[lindex $ranges [expr {$i * 2 + 1}]]]
if { $start == $end } {
set end [format "0x%x" [expr {$end + 1}]]
}
if { $block_end == $start } {
set block_end $end
}
set r${i}s $start
set r${i}e $end
}
# Check the block 'foo' has the expected ranges.
gdb_test "maintenance info blocks" \
[multi_line \
"\\\[\\(block \\*\\) $::hex\\\] $block_start\\.\\.$block_end" \
" entry pc: $entry_pc" \
" inline function: foo" \
" symbol count: $::decimal" \
" address ranges:" \
" $r0s\\.\\.$r0e" \
" $r1s\\.\\.$r1e" \
" $r2s\\.\\.$r2e"] \
"block for foo has some content"
# Check the outer frame is 'main' as expected.
gdb_test "frame 1" \
[multi_line \
"#1 main \\(\\) at \[^\r\n\]+/$::srcfile:$::foo_call_line" \
"$::foo_call_line\\s+\[^\r\n\]+/\\* foo call line \\*/"] \
"frame 1 is for main"
}
foreach_with_prefix dwarf_version { 4 5 } {
foreach_with_prefix empty_loc { start middle end } {
foreach_with_prefix entry_pc_type { empty non_empty } {
run_test $dwarf_version $empty_loc $entry_pc_type
}
}
}