# 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