# This testcase is part of GDB, the GNU debugger.
#
# Copyright 2021-2025 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 .
#
#
# Test shared libraries loaded into different namespaces with dlmopen().
#
# We test that GDB shows the correct number of instances of the libraries
# the test loaded while unloading them one-by-one.
require allow_dlmopen_tests
# Don't use 'dlmopen.c' as the source file name, glibc also has a file
# with that name. Within our tests, we set the source directory search
# path order to:
#
# (1) the test source directory,
# (2) the compilation directory, and then
# (3) the current working directory.
#
# Because (1) is first when we try to place a breakpoint on
# 'dlmopen.c', if the test source file has that name, then GDB will
# find both the test source file, and the source file from glibc.
#
# We could work around this by making (2) first in the source
# directory list, but that only works when the glibc source is
# installed. If it isn't then GDB will try the compilation directory,
# fail to find the source, then try the test source directory, get a
# hit, and so still confuse the two files.
#
# You might think the problem can be solved by specifying the absolute
# path to the source file. This doesn't work because the glibc file
# has its filename recorded as just "dlmopen.c", as such GDB has to
# figure out an absolute path to the file (if possible). The absolute
# path is figured out based on where GDB can find a matching file in
# the source directory list, and because of the confusion above, GDB
# will usually think the test 'dlmopen.c' and the glibc 'dlmopen.c'
# are actually the same file.
#
# The conclusion is that it is just easier to rename the test source
# file to avoid conflicts with glibc.
standard_testfile -main.c -lib.c -lib-dep.c
set basename_lib dlmopen-lib
set srcfile_lib $srcfile2
set binfile_lib1 [standard_output_file $basename_lib.1.so]
set binfile_lib2 [standard_output_file $basename_lib.2.so]
set srcfile_lib_dep $srcfile3
set binfile_lib_dep [standard_output_file $basename_lib-dep.so]
if { [build_executable "build shlib dep" $binfile_lib_dep $srcfile_lib_dep \
{debug shlib}] == -1 } {
return
}
if { [build_executable "build shlib" $binfile_lib1 $srcfile_lib \
[list debug shlib_load shlib libs=$binfile_lib_dep]] == -1 } {
return
}
if { [build_executable "build shlib" $binfile_lib2 $srcfile_lib \
[list debug shlib_load shlib libs=$binfile_lib_dep]] == -1 } {
return
}
if { [build_executable "failed to build" $testfile $srcfile \
[list additional_flags=-DDSO1_NAME=\"$binfile_lib1\" \
additional_flags=-DDSO2_NAME=\"$binfile_lib2\" \
shlib_load debug]] } {
return
}
# Some locations needed by the tests.
set bp_inc [gdb_get_line_number "bp.inc" $srcfile_lib]
set bp_main [gdb_get_line_number "bp.main" $srcfile]
# Figure out the file name for the dynamic linker.
set dyln_name [section_get $binfile .interp]
if { $dyln_name eq "" } {
unsupported "couldn't find dynamic linker name"
return
}
# Return true if FILENAME is the dynamic linker. Otherwise return false.
proc is_dyln { filename } {
return [expr {$filename eq $::dyln_name}]
}
# Check that 'info shared' show NUM occurrences of DSO.
proc check_dso_count { dso num } {
global gdb_prompt hex
set count 0
gdb_test_multiple "info shared" "info shared" {
-re "$hex $hex \(\[\[$::decimal\]\]\\s+\)\?Yes \[^\r\n\]*$dso\r\n" {
# use longer form so debug remote does not interfere
set count [expr $count + 1]
exp_continue
}
-re "$gdb_prompt " {
verbose -log "library: $dso, expected: $num, found: $count"
gdb_assert {$count == $num} "$gdb_test_name"
}
}
}
# The DSO part of the test. We run it once per DSO call.
proc test_dlmopen_one { ndso1 ndso2 exp_glob } {
global srcfile_lib srcfile_lib basename_lib bp_inc
# Try to reach the breakpoint in the dynamically loaded library.
gdb_continue_to_breakpoint "cont to bp.inc" \
".*$srcfile_lib:$bp_inc\r\n.*"
# We opened all DSOs initially and close them one by one.
with_test_prefix "dso 1" { check_dso_count $basename_lib.1.so $ndso1 }
with_test_prefix "dso 2" { check_dso_count $basename_lib.2.so $ndso2 }
# This might help debugging.
gdb_test "info breakpoints" ".*"
gdb_test "print \$pc" ".*"
# We expect different instances of GDB_DLMOPEN_GLOB per DSO.
gdb_test "print amount" "= $exp_glob"
gdb_test "print gdb_dlmopen_glob" "= $exp_glob"
# Modify that DSO's instance, which should leave the others intact.
gdb_test "print &gdb_dlmopen_glob" "= .*"
gdb_test "print gdb_dlmopen_glob = -1" "= -1"
}
# The actual test. We run it twice.
proc test_dlmopen {} {
global srcfile basename_lib bp_main
# Note that when loading dlmopen-lib.1.so and dlmopen-lib.2.so into
# the same namespace, dlmopen-lib-dep.so is loaded only once, so in
# this case, the changes to gdb_dlmopen_glob inside test_dlmopen_one
# will actually be visible.
#
# Hence, we supply the expected value of this variable as argument to
# test_dlmopen_one.
with_test_prefix "dlmopen 1" { test_dlmopen_one 3 1 1 }
with_test_prefix "dlmopen 2" { test_dlmopen_one 2 1 1 }
with_test_prefix "dlmopen 3" { test_dlmopen_one 1 1 1 }
with_test_prefix "dlmopen 4" { test_dlmopen_one 0 1 -1 }
with_test_prefix "main" {
# Try to reach the breakpoint in the dynamically loaded library.
gdb_continue_to_breakpoint "cont to bp.main" \
".*$srcfile:$bp_main\r\n.*"
# The library should not be listed.
with_test_prefix "dso 1" { check_dso_count $basename_lib.1.so 0 }
with_test_prefix "dso 2" { check_dso_count $basename_lib.2.so 0 }
}
}
# Setup for calling 'test_dlmopen', this is the version of the test
# that doesn't use 'attach'.
proc test_dlmopen_no_attach {} {
clean_restart $::binfile
if { ![runto_main] } {
return
}
# Remove the pause. We only need it for the attach test.
gdb_test "print wait_for_gdb = 0" "\\\$1 = 0"
# Break in the to-be-loaded library and at the end of main.
delete_breakpoints
gdb_breakpoint $::srcfile_lib:$::bp_inc allow-pending
gdb_breakpoint $::srcfile:$::bp_main
test_dlmopen
}
# Setup for calling 'test_dlmopen', this is the version of the test
# that does use 'attach'.
proc test_dlmopen_with_attach {} {
if { ![can_spawn_for_attach] } {
unsupported "cannot run attach tests"
return
}
clean_restart $::binfile
# Start the test program.
set test_spawn_id [spawn_wait_for_attach $::binfile]
set testpid [spawn_id_get_pid $test_spawn_id]
# Attach.
if { ![gdb_attach $testpid] } {
return
}
with_test_prefix "attach" {
# Remove the pause. We no longer need it.
gdb_test "print wait_for_gdb = 0" "\\\$1 = 0"
# Set the same breakpoints again. This time, however, we do not allow the
# breakpoint to be pending since the library has already been loaded.
gdb_breakpoint $::srcfile_lib:$::bp_inc
gdb_breakpoint $::srcfile:$::bp_main
test_dlmopen
}
}
# Run 'info sharedlibrary' and count the number of mappings that look
# like they might be the dynamic linker. This will only work for
# Linux right now.
proc get_dyld_info {} {
if { ![istarget *-linux*] } {
return [list 0 ""]
}
set dyld_count 0
set dyld_start_addr ""
gdb_test_multiple "info sharedlibrary" "" {
-re "From\\s+To\\s+\(NS\\s+\)?Syms\\s+Read\\s+Shared Object Library\r\n" {
exp_continue
}
-re "^($::hex)\\s+$::hex\\s+\(\#$::decimal\\s+\)?\[^/\]+(/\[^\r\n\]+)\r\n" {
set addr $expect_out(1,string)
set lib $expect_out(3,string)
if { [is_dyln $lib] } {
# This looks like it might be the dynamic linker.
incr dyld_count
if { $dyld_start_addr eq "" } {
set dyld_start_addr $addr
} elseif { $dyld_start_addr ne $addr } {
set dyld_start_addr "MULTIPLE"
}
}
exp_continue
}
-re "\\(\\*\\): Shared library is missing debugging information\\.\r\n" {
exp_continue
}
-re "^$::gdb_prompt $" {
}
}
if { $dyld_start_addr eq "MULTIPLE" } {
set dyld_start_addr ""
}
return [list $dyld_count $dyld_start_addr]
}
# The inferior for this test causes the dynamic linker to be appear
# multiple times in the inferior's shared library list, but (at least
# with glibc), the dynamic linker is really only mapped in once. That
# is, each of the dynamic linker instances that appear in the 'info
# sharedlibrary' output, will have the same address range.
#
# This test creates a user breakpoint in the dynamic linker, and then
# runs over the dlcose calls, which unmap all but one of the dynamic
# linker instances.
#
# The expectation is that the user breakpoint in the dynamic linker
# should still be active. Older versions of GDB had a bug where the
# breakpoint would become pending.
proc_with_prefix test_solib_unmap_events { } {
# This test relies on finding the dynamic linker library, and is
# currently written assuming Linux.
if { ![istarget *-linux*] } {
unsupport "cannot find dynamic linker library on this target"
return
}
clean_restart $::binfile
if { ![runto_main] } {
return
}
# Check that before any of our dlopen/dlmopen calls, we can find a
# single copy of the dynamic linker in the shared library list.
set dyld_info [get_dyld_info]
set dyld_count [lindex $dyld_info 0]
if { $dyld_count != 1 } {
unsupported "initial dyld state appears strange"
return
}
# Continue the inferior until all solib are loaded.
set alarm_lineno [gdb_get_line_number "alarm" $::srcfile]
gdb_breakpoint ${::srcfile}:${alarm_lineno}
gdb_continue_to_breakpoint "all solib are now loaded"
# Check that we have multiple copies of dynamic linker loaded, and
# that the dynamic linker is only loaded at a single address.
set dyld_info [get_dyld_info]
set dyld_count [lindex $dyld_info 0]
set dyld_start_addr [lindex $dyld_info 1]
# If we didn't find a suitable dynamic linker address, or we
# didn't find multiple copies of the dynamic linker, then
# something has gone wrong with the test setup.
if { $dyld_count < 2 } {
unsupported "multiple copies of the dynamic linker not found"
return
}
if { $dyld_start_addr eq "" } {
unsupported "unable to find suitable dynamic linker start address"
return
}
# Check the address we found is (likely) writable.
gdb_test_multiple "x/1i $dyld_start_addr" "check b/p address" {
-re -wrap "Cannot access memory at address \[^\r\n\]+" {
unsupported "dynamic linker address is not accessible"
return
}
-re -wrap "" {
}
}
# Create a breakpoint within the dynamic linker. It is pretty unlikely
# that this breakpoint will ever be hit, but just in case it is, make it
# conditional, with a condition that will never be true. All we really
# care about for this test is whether the breakpoint will be made
# pending or not (it should not).
gdb_test "break *$dyld_start_addr if (0)" \
"Breakpoint $::decimal at $::hex\[^\r\n\]+" \
"create breakpoint within dynamic linker"
set bpnum [get_integer_valueof "\$bpnum" INVALID "get bpnum"]
# Now continue until the 'bp.main' location, this will unload some
# copies, but not all copies, of the dynamic linker.
gdb_test "print wait_for_gdb = 0" " = 0"
set bp_main [gdb_get_line_number "bp.main" $::srcfile]
gdb_breakpoint $::srcfile:$bp_main
gdb_continue_to_breakpoint "stop at bp.main"
# At one point, GDB would incorrectly mark the breakpoints in the
# dynamic linker as pending when some instances of the library were
# unloaded, despite there really only being one copy of the dynamic
# linker actually loaded into the inferior's address space.
gdb_test_multiple "info breakpoints $bpnum" "check b/p status" {
-re -wrap "$bpnum\\s+breakpoint\\s+keep\\s+y\\s+\\s+\\*$::hex\\s*\r\n\\s+stop only if \\(0\\)" {
fail $gdb_test_name
}
-re -wrap "$bpnum\\s+breakpoint\\s+keep\\s+y\\s+$::hex\\s*\[^\r\n\]+\r\n\\s+stop only if \\(0\\)" {
pass $gdb_test_name
}
}
# With all the dlclose calls now complete, we should be back to a
# single copy of the dynamic linker.
set dyld_info [get_dyld_info]
set dyld_count [lindex $dyld_info 0]
gdb_assert { $dyld_count == 1 } \
"one dynamic linker found after dlclose calls"
}
# Check that we can 'next' over the dlclose calls without GDB giving any
# warnings or errors.
proc_with_prefix test_next_over_dlclose {} {
clean_restart $::binfile
if { ![runto_main] } {
return
}
set dlclose_lineno [gdb_get_line_number "dlclose" $::srcfile]
gdb_breakpoint $::srcfile:$dlclose_lineno
gdb_breakpoint $::srcfile:$::bp_main
# Remove the pause. We no longer need it.
gdb_test "print wait_for_gdb = 0" "\\\$1 = 0"
set loc_re [multi_line \
"\[^\r\n\]+/[string_to_regexp $::srcfile]:$dlclose_lineno" \
"$dlclose_lineno\\s+dlclose \[^\r\n\]+"]
# This loop mirrors the loop in dlmopen.c where the inferior performs
# four calls to dlclose. Here we continue to the dlclose, and then use
# 'next' to step over the call. As part of the 'next' GDB will insert
# breakpoints to catch longjmps into the multiple copies of libc which
# have been loaded. Each dlclose call will cause a copy of libc to be
# unloaded, which should disable the longjmp breakpoint that GDB
# previously inserted.
#
# At one point a bug in GDB meant that we failed to correctly disable
# the longjmp breakpoints and GDB would try to remove the breakpoint
# from a library after it had been unloaded, which would trigger a
# warning.
for { set i 0 } { $i < 4 } { incr i } {
gdb_continue_to_breakpoint "continue to dlclose $i" $loc_re
gdb_test "next" "^$::decimal\\s+for \\(dl = 0;\[^\r\n\]+\\)" \
"next over dlclose $i"
}
# Just to confirm that we are where we think we are, continue to the
# final 'return' line in main. If this isn't hit then we likely went
# wrong earlier.
gdb_continue_to_breakpoint "continue to final return" \
[multi_line \
"\[^\r\n\]+/[string_to_regexp $::srcfile]:$::bp_main" \
"$::bp_main\\s+return 0;\[^\r\n\]+"]
}
# Run the actual tests.
test_dlmopen_no_attach
test_dlmopen_with_attach
test_solib_unmap_events
test_next_over_dlclose