Skip to content

[lldb][DynamicLoader] Fix lldb unable to stop at _dl_debug_state if user set it before the process launched. #88792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2024

Conversation

ZequanWu
Copy link
Contributor

If user sets a breakpoint at _dl_debug_state before the process launched, the breakpoint is not resolved yet. When lldb loads dynamic loader module, it's created with Target::GetOrCreateModule which notifies any pending breakpoint to resolve. However, the module's sections are not loaded at this time. They are loaded after returned from Target::GetOrCreateModule. This change fixes it by manually resolving breakpoints after creating dynamic loader module.

@ZequanWu ZequanWu requested review from labath and jasonmolenda April 15, 2024 20:43
@ZequanWu ZequanWu requested a review from JDevlieghere as a code owner April 15, 2024 20:43
@llvmbot llvmbot added the lldb label Apr 15, 2024
@llvmbot
Copy link
Member

llvmbot commented Apr 15, 2024

@llvm/pr-subscribers-lldb

Author: Zequan Wu (ZequanWu)

Changes

If user sets a breakpoint at _dl_debug_state before the process launched, the breakpoint is not resolved yet. When lldb loads dynamic loader module, it's created with Target::GetOrCreateModule which notifies any pending breakpoint to resolve. However, the module's sections are not loaded at this time. They are loaded after returned from Target::GetOrCreateModule. This change fixes it by manually resolving breakpoints after creating dynamic loader module.


Full diff: https://github.com/llvm/llvm-project/pull/88792.diff

4 Files Affected:

  • (modified) lldb/include/lldb/Target/Target.h (+3)
  • (modified) lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp (+5)
  • (modified) lldb/source/Target/Target.cpp (+10-7)
  • (modified) lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py (+17)
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 2c2e6b2831ccee..3554ef0ea5a140 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -878,6 +878,9 @@ class Target : public std::enable_shared_from_this<Target>,
   // the address of its previous instruction and return that address.
   lldb::addr_t GetBreakableLoadAddress(lldb::addr_t addr);
 
+  void UpdateBreakpoints(ModuleList &module_list, bool added,
+                         bool delete_locations);
+
   void ModulesDidLoad(ModuleList &module_list);
 
   void ModulesDidUnload(ModuleList &module_list, bool delete_locations);
diff --git a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
index 9baf86da4dc799..901fa53682da8e 100644
--- a/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
+++ b/lldb/source/Plugins/DynamicLoader/POSIX-DYLD/DynamicLoaderPOSIXDYLD.cpp
@@ -576,6 +576,11 @@ ModuleSP DynamicLoaderPOSIXDYLD::LoadInterpreterModule() {
     UpdateLoadedSections(module_sp, LLDB_INVALID_ADDRESS, m_interpreter_base,
                          false);
     m_interpreter_module = module_sp;
+    // Manually update breakpoints after updating loaded sections, because they
+    // cannot be resolve at the time when creating this module.
+    ModuleList module_list;
+    module_list.Append(module_sp);
+    m_process->GetTarget().UpdateBreakpoints(module_list, true, false);
     return module_sp;
   }
   return nullptr;
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 09b0ac42631d1a..ec2dea5cc238d3 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -1687,6 +1687,13 @@ void Target::NotifyModulesRemoved(lldb_private::ModuleList &module_list) {
   ModulesDidUnload(module_list, false);
 }
 
+void Target::UpdateBreakpoints(ModuleList &module_list, bool added,
+                               bool delete_locations) {
+  m_breakpoint_list.UpdateBreakpoints(module_list, added, delete_locations);
+  m_internal_breakpoint_list.UpdateBreakpoints(module_list, added,
+                                               delete_locations);
+}
+
 void Target::ModulesDidLoad(ModuleList &module_list) {
   const size_t num_images = module_list.GetSize();
   if (m_valid && num_images) {
@@ -1694,8 +1701,7 @@ void Target::ModulesDidLoad(ModuleList &module_list) {
       ModuleSP module_sp(module_list.GetModuleAtIndex(idx));
       LoadScriptingResourceForModule(module_sp, this);
     }
-    m_breakpoint_list.UpdateBreakpoints(module_list, true, false);
-    m_internal_breakpoint_list.UpdateBreakpoints(module_list, true, false);
+    UpdateBreakpoints(module_list, true, false);
     if (m_process_sp) {
       m_process_sp->ModulesDidLoad(module_list);
     }
@@ -1713,8 +1719,7 @@ void Target::SymbolsDidLoad(ModuleList &module_list) {
       }
     }
 
-    m_breakpoint_list.UpdateBreakpoints(module_list, true, false);
-    m_internal_breakpoint_list.UpdateBreakpoints(module_list, true, false);
+    UpdateBreakpoints(module_list, true, false);
     auto data_sp =
         std::make_shared<TargetEventData>(shared_from_this(), module_list);
     BroadcastEvent(eBroadcastBitSymbolsLoaded, data_sp);
@@ -1727,9 +1732,7 @@ void Target::ModulesDidUnload(ModuleList &module_list, bool delete_locations) {
     auto data_sp =
         std::make_shared<TargetEventData>(shared_from_this(), module_list);
     BroadcastEvent(eBroadcastBitModulesUnloaded, data_sp);
-    m_breakpoint_list.UpdateBreakpoints(module_list, false, delete_locations);
-    m_internal_breakpoint_list.UpdateBreakpoints(module_list, false,
-                                                 delete_locations);
+    UpdateBreakpoints(module_list, false, delete_locations);
 
     // If a module was torn down it will have torn down the 'TypeSystemClang's
     // that we used as source 'ASTContext's for the persistent variables in
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py b/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py
index ea242952e409ec..a7e23fae14a21f 100644
--- a/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py
@@ -671,3 +671,20 @@ def test_breakpoint_statistics_hitcount(self):
         self.assertNotEqual(breakpoints_stats, None)
         for breakpoint_stats in breakpoints_stats:
             self.assertIn("hitCount", breakpoint_stats)
+
+    @skipIf(oslist=no_match(["linux"]))
+    def test_break_at__dl_debug_state(self):
+        """
+        Test lldb is able to stop at _dl_debug_state if it is set before the
+        process is launched.
+        """
+        self.build()
+        exe = self.getBuildArtifact("a.out")
+        self.runCmd("target create %s" % exe)
+        bpid = lldbutil.run_break_set_by_symbol(self, "_dl_debug_state",
+                                                num_expected_locations=0)
+        self.runCmd("run")
+        self.assertIsNotNone(
+            lldbutil.get_one_thread_stopped_at_breakpoint_id(self.process(),
+                                                             bpid)
+        )

Copy link

github-actions bot commented Apr 15, 2024

✅ With the latest revision this PR passed the Python code formatter.

@jimingham
Copy link
Collaborator

When a shared library gets loaded, its sections have to have been loaded before asking the breakpoint system to try to set new breakpoints. Presumably this is how it happens for all the other shared library loads, or breakpoints in shared libraries wouldn't work.

I'm missing why the _dl_debug_state breakpoint is special here, such that it requires a force load of the section info? How does that happen?

@jasonmolenda
Copy link
Collaborator

Skimming DynamicLoaderPOSIXDYLD (as it is written today), it looks like attach/launch call SetRendezvousBreakpoint() which (1) loads ld.so into lldb if it isn't already there, and (2) sets the breakpoint to be notified about future binaries loading. It loads ld.so via LoadInterpreterModule which Finds It somehow (I stopped reading), adds it to the Target, and sets the section load addresses, returns the module_sp. Then it sets breakpoints on a half dozen notification function names in the ld.so Module. This is all special-cased for ld.so, and we do not evaluate user breakpoints because we didn't call Target::ModulesDidLoad for ld.so.

The code path for other binaries goes through DynamicLoaderPOSIXDYLD::RefreshModules which does the normal approach of adding binaries to the Target, setting the load addresses, then calling ModulesDidLoad which will evaluate breakpoints that may need to be set in the new binaries.

It seems like a simpler fix would be to have DynamicLoaderPOSIXDYLD::LoadInterpreterModule create a ModuleList with the ModuleSP of ld.so that it has just added to Target and set its section load addresses, then call ModulesDidLoad on it so that user breakpoints are evaluated and inserted. The patch as it is written right now is taking one part of ModulesDidLoad and putting it in a separate method that DynamicLoaderPOSIXDYLD::LoadInterpreterModule is calling -- why not just call ModulesDidLoad and delegate this (and possibly other binary-just-loaded) behaviors to it?

@ZequanWu
Copy link
Contributor Author

ZequanWu commented Apr 16, 2024

why not just call ModulesDidLoad and delegate this (and possibly other binary-just-loaded) behaviors to it?

That's what I did first, but it breaks the test TestModuleLoadedNotifys.ModuleLoadedNotifysTestCase.test_launch_notifications because of extra broadcaster event. So, I moved out the part just updating breakpoints.

@ZequanWu
Copy link
Contributor Author

I'm missing why the _dl_debug_state breakpoint is special here, such that it requires a force load of the section info? How does that happen?

Jason's comment explains it well. It's because ld.so's loading is special here. Normal binaries are loaded via DynamicLoaderPOSIXDYLD::RefreshModules which updates loaded section addresses.

@labath
Copy link
Collaborator

labath commented Apr 16, 2024

why not just call ModulesDidLoad and delegate this (and possibly other binary-just-loaded) behaviors to it?

That's what I did first, but it breaks the test TestModuleLoadedNotifys.ModuleLoadedNotifysTestCase.test_launch_notifications because of extra broadcaster event. So, I moved out the part just updating breakpoints.

The point of the test is to ensure you don't get a hundred notifications, one for each module. Having one notification for ld.so, and 99 for the rest of the modules is ok. It should be fine to just update the test to match new reality.

@ZequanWu
Copy link
Contributor Author

why not just call ModulesDidLoad and delegate this (and possibly other binary-just-loaded) behaviors to it?

That's what I did first, but it breaks the test TestModuleLoadedNotifys.ModuleLoadedNotifysTestCase.test_launch_notifications because of extra broadcaster event. So, I moved out the part just updating breakpoints.

The point of the test is to ensure you don't get a hundred notifications, one for each module. Having one notification for ld.so, and 99 for the rest of the modules is ok. It should be fine to just update the test to match new reality.

Updated to use Target::ModulesDisLoad and the test.

Copy link
Collaborator

@jasonmolenda jasonmolenda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, thanks for the revisions.

@labath
Copy link
Collaborator

labath commented Apr 17, 2024

why not just call ModulesDidLoad and delegate this (and possibly other binary-just-loaded) behaviors to it?

That's what I did first, but it breaks the test TestModuleLoadedNotifys.ModuleLoadedNotifysTestCase.test_launch_notifications because of extra broadcaster event. So, I moved out the part just updating breakpoints.

The point of the test is to ensure you don't get a hundred notifications, one for each module. Having one notification for ld.so, and 99 for the rest of the modules is ok. It should be fine to just update the test to match new reality.

Updated to use Target::ModulesDisLoad and the test.

Oh, so the problem is that the loaded event gets sent twice (and not that it gets sent as a separate event, as I originally thought). While it's not the end of the world (it appears the mac loader does something similar as well), this does seem like something that we could prevent. Can you check where does the second event get sent from? Is it by any chance when we go through this place ? If so, it should be fairly easy to refactor that code to avoid putting the interpreter module into the event list when it is already loaded.

@ZequanWu
Copy link
Contributor Author

ZequanWu commented Apr 17, 2024

Can you check where does the second event get sent from?

The first event is sent when creating the module for ld.so:

if (ModuleSP module_sp = target.GetOrCreateModule(module_spec,
. It gets notified at here:
ModulesDidLoad(my_module_list);
. The second event is sent because I explicitly calls ModulesDidLoad in this change (the current version) after the creating the ld.so module.

Is it by any chance when we go through this place ?

It doesn't go through that place when loading ld.so module. That's for other binaries (non-dynamic-linker binaries).

@labath
Copy link
Collaborator

labath commented Apr 17, 2024

So, could the fix be as simple as passing notify=false in the first call ?

@ZequanWu
Copy link
Contributor Author

So, could the fix be as simple as passing notify=false in the first call ?

Yeah, absolutely. Updated.

Copy link

github-actions bot commented Apr 17, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@ZequanWu ZequanWu merged commit 60b90b5 into llvm:main Apr 17, 2024
@ZequanWu ZequanWu deleted the break-_dl_debug_state branch May 14, 2024 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants