Skip to content

[lldb] Support stepping through C++ thunks #127419

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 10 commits into from
Feb 17, 2025
Merged

Conversation

JDevlieghere
Copy link
Member

This PR fixes LLDB stepping out, rather than stepping through a C++ thunk. The implementation is based on, and upstreams, the support for runtime thunks in the Swift fork.

Fixes #43413

This PR fixes LLDB stepping out, rather than stepping through a C++
thunk. The implementation is based on, and upstreams, the support for
runtime thunks in the Swift fork.

Fixes llvm#43413
@llvmbot
Copy link
Member

llvmbot commented Feb 16, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

This PR fixes LLDB stepping out, rather than stepping through a C++ thunk. The implementation is based on, and upstreams, the support for runtime thunks in the Swift fork.

Fixes #43413


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

7 Files Affected:

  • (modified) lldb/include/lldb/Target/LanguageRuntime.h (+2)
  • (modified) lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp (+6)
  • (modified) lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.h (+3)
  • (modified) lldb/source/Target/ThreadPlanShouldStopHere.cpp (+40-11)
  • (added) lldb/test/API/lang/cpp/thunk/Makefile (+3)
  • (added) lldb/test/API/lang/cpp/thunk/TestThunk.py (+24)
  • (added) lldb/test/API/lang/cpp/thunk/main.cpp (+36)
diff --git a/lldb/include/lldb/Target/LanguageRuntime.h b/lldb/include/lldb/Target/LanguageRuntime.h
index f9ae2dc589632..7e4c11df0da7f 100644
--- a/lldb/include/lldb/Target/LanguageRuntime.h
+++ b/lldb/include/lldb/Target/LanguageRuntime.h
@@ -201,6 +201,8 @@ class LanguageRuntime : public Runtime, public PluginInterface {
     return false;
   }
 
+  virtual bool IsSymbolARuntimeThunk(const Symbol &symbol) { return false; }
+
   // Given the name of a runtime symbol (e.g. in Objective-C, an ivar offset
   // symbol), try to determine from the runtime what the value of that symbol
   // would be. Useful when the underlying binary is stripped.
diff --git a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
index 42fa54634841c..e648b9665bef6 100644
--- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.cpp
@@ -476,3 +476,9 @@ CPPLanguageRuntime::GetStepThroughTrampolinePlan(Thread &thread,
 
   return ret_plan_sp;
 }
+
+bool CPPLanguageRuntime::IsSymbolARuntimeThunk(const Symbol &symbol) {
+  llvm::outs() << symbol.GetMangled().GetMangledName().GetStringRef() << '\n';
+  return symbol.GetMangled().GetMangledName().GetStringRef().starts_with(
+      "_ZThn");
+}
diff --git a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.h b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.h
index 57cfe28245808..05639e9798917 100644
--- a/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/CPlusPlus/CPPLanguageRuntime.h
@@ -78,6 +78,9 @@ class CPPLanguageRuntime : public LanguageRuntime {
                                                   bool stop_others) override;
 
   bool IsAllowedRuntimeValue(ConstString name) override;
+
+  bool IsSymbolARuntimeThunk(const Symbol &symbol) override;
+
 protected:
   // Classes that inherit from CPPLanguageRuntime can see and modify these
   CPPLanguageRuntime(Process *process);
diff --git a/lldb/source/Target/ThreadPlanShouldStopHere.cpp b/lldb/source/Target/ThreadPlanShouldStopHere.cpp
index e72f8d8f51a20..723d7965c3467 100644
--- a/lldb/source/Target/ThreadPlanShouldStopHere.cpp
+++ b/lldb/source/Target/ThreadPlanShouldStopHere.cpp
@@ -8,6 +8,7 @@
 
 #include "lldb/Target/ThreadPlanShouldStopHere.h"
 #include "lldb/Symbol/Symbol.h"
+#include "lldb/Target/LanguageRuntime.h"
 #include "lldb/Target/RegisterContext.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/LLDBLog.h"
@@ -76,6 +77,18 @@ bool ThreadPlanShouldStopHere::DefaultShouldStopHereCallback(
     }
   }
 
+  // Check whether the frame we are in is a language runtime thunk, only for
+  // step out:
+  if (operation == eFrameCompareOlder) {
+    Symbol *symbol = frame->GetSymbolContext(eSymbolContextSymbol).symbol;
+    if (symbol) {
+      ProcessSP process_sp(current_plan->GetThread().GetProcess());
+      for (auto *runtime : process_sp->GetLanguageRuntimes()) {
+        if (runtime->IsSymbolARuntimeThunk(*symbol))
+          should_stop_here = false;
+      }
+    }
+  }
   // Always avoid code with line number 0.
   // FIXME: At present the ShouldStop and the StepFromHere calculate this
   // independently.  If this ever
@@ -109,18 +122,34 @@ ThreadPlanSP ThreadPlanShouldStopHere::DefaultStepFromHereCallback(
 
   if (sc.line_entry.line == 0) {
     AddressRange range = sc.line_entry.range;
-
-    // If the whole function is marked line 0 just step out, that's easier &
-    // faster than continuing to step through it.
     bool just_step_out = false;
-    if (sc.symbol && sc.symbol->ValueIsAddress()) {
-      Address symbol_end = sc.symbol->GetAddress();
-      symbol_end.Slide(sc.symbol->GetByteSize() - 1);
-      if (range.ContainsFileAddress(sc.symbol->GetAddress()) &&
-          range.ContainsFileAddress(symbol_end)) {
-        LLDB_LOGF(log, "Stopped in a function with only line 0 lines, just "
-                       "stepping out.");
-        just_step_out = true;
+    if (sc.symbol) {
+      ProcessSP process_sp(current_plan->GetThread().GetProcess());
+
+      // If this is a runtime thunk, step through it, rather than stepping out
+      // because it's marked line 0.
+      bool is_thunk = false;
+      for (auto *runtime : process_sp->GetLanguageRuntimes()) {
+        if (runtime->IsSymbolARuntimeThunk(*sc.symbol)) {
+          LLDB_LOGF(log, "In runtime thunk %s - stepping out.",
+                    sc.symbol->GetName().GetCString());
+          is_thunk = true;
+        }
+      }
+
+      // If the whole function is marked line 0 just step out, that's easier &
+      // faster than continuing to step through it.
+      // FIXME: This assumes that the function is a single line range.  It could
+      // be a series of contiguous line 0 ranges.  Check for that too.
+      if (!is_thunk && sc.symbol->ValueIsAddress()) {
+        Address symbol_end = sc.symbol->GetAddress();
+        symbol_end.Slide(sc.symbol->GetByteSize() - 1);
+        if (range.ContainsFileAddress(sc.symbol->GetAddress()) &&
+            range.ContainsFileAddress(symbol_end)) {
+          LLDB_LOGF(log, "Stopped in a function with only line 0 lines, just "
+                         "stepping out.");
+          just_step_out = true;
+        }
       }
     }
     if (!just_step_out) {
diff --git a/lldb/test/API/lang/cpp/thunk/Makefile b/lldb/test/API/lang/cpp/thunk/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/lang/cpp/thunk/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/cpp/thunk/TestThunk.py b/lldb/test/API/lang/cpp/thunk/TestThunk.py
new file mode 100644
index 0000000000000..bd17401f1d935
--- /dev/null
+++ b/lldb/test/API/lang/cpp/thunk/TestThunk.py
@@ -0,0 +1,24 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class ThunkTest(TestBase):
+    def test(self):
+        self.build()
+        lldbutil.run_to_name_breakpoint(self, "testit")
+
+        self.expect(
+            "step",
+            STEP_IN_SUCCEEDED,
+            substrs=["stop reason = step in", "Derived1::doit"],
+        )
+
+        self.runCmd("continue")
+
+        self.expect(
+            "step",
+            STEP_IN_SUCCEEDED,
+            substrs=["stop reason = step in", "Derived2::doit"],
+        )
diff --git a/lldb/test/API/lang/cpp/thunk/main.cpp b/lldb/test/API/lang/cpp/thunk/main.cpp
new file mode 100644
index 0000000000000..36f2d7c2ce421
--- /dev/null
+++ b/lldb/test/API/lang/cpp/thunk/main.cpp
@@ -0,0 +1,36 @@
+#include <stdio.h>
+
+class Base1 {
+public:
+  virtual ~Base1() {}
+};
+
+class Base2 {
+public:
+  virtual void doit() = 0;
+};
+
+Base2 *b = nullptr;
+
+class Derived1 : public Base1, public Base2 {
+public:
+  virtual void doit() { printf("Derived1\n"); }
+};
+
+class Derived2 : public Base2 {
+public:
+  virtual void doit() { printf("Derived2\n"); }
+};
+
+void testit() { b->doit(); }
+
+int main() {
+
+  b = new Derived1();
+  testit();
+
+  b = new Derived2();
+  testit();
+
+  return 0;
+}

@JDevlieghere JDevlieghere changed the title [lldb] Support stepping over C++ thunks [lldb] Support stepping through C++ thunks Feb 16, 2025
@levy
Copy link

levy commented Feb 17, 2025

I'm the one who recently added an example reproducing the #43413 issue. I compiled this PR version of lldb and related tools and tested it against the simple reproduction I added to the issue, and also with our complex project where I first faced this issue. In both cases lldb works as expected with this patch.

Thank you very much!

@Michael137
Copy link
Member

Makes sense to me but I'll defer to @jimingham and co. for the stepping bits

@tedwoodward
Copy link

@JDevlieghere would you mind getting rid of the nullptr in the testcase, main.cpp line 13?
Base2 *b = nullptr;

*b is set to nullptr per language standard, and those of use stuck in c++03 on embedded targets would appreciate it!

// non-virtual base, with fixed this adjustments, use a "Th" prefix and encode
// the required adjustment offset, probably negative, indicated by a 'n'
// prefix, and the encoding of the target function.
return symbol.GetMangled().GetMangledName().GetStringRef().starts_with(
Copy link
Member

@Michael137 Michael137 Feb 17, 2025

Choose a reason for hiding this comment

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

Hmm i think we also want to check Tc and Tv?

Tv you can test by inheriting one of the bases virtually. And here's an example of a Tc encoding:

struct V1 { };                 
struct V2 : virtual V1 { };    
                               
struct A {                     
  virtual V1 *f();             
};                             
                               
struct B : A {                                                
  virtual V2 *f();             
};                             
                               
V2 *B::f() { return 0; }       
                              
$ c++filt -n _ZTch0_v0_n24_N1B1fEv
covariant return thunk to B::f()

Copy link
Member Author

Choose a reason for hiding this comment

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

I should've read the whole paragraph :-)

Copy link

github-actions bot commented Feb 17, 2025

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

@jimingham
Copy link
Collaborator

This looks fine.

The one thing that we might want to also test is that if we step through the thunk, and then stop in some code that we don't want to stop in (i.e. the target of the thunk doesn't have debug information) we step back out correctly. This should work by composition, but thunks can be funny and harder to trace out of...

I don't remember whether we had tests for this on the swift side, but it shouldn't be too hard to have the target of the thunk in a separate .cpp file, and strip that one. Then make sure we don't leave the user stranded there.

Copy link
Member

@medismailben medismailben left a comment

Choose a reason for hiding this comment

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

Nice! LGTM with comment.

Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

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

LGTM

@JDevlieghere JDevlieghere merged commit a3dc77c into llvm:main Feb 17, 2025
7 checks passed
@JDevlieghere JDevlieghere deleted the cpp-thunk branch February 17, 2025 23:44
@llvm-ci
Copy link
Collaborator

llvm-ci commented Feb 18, 2025

LLVM Buildbot has detected a new failure on builder lldb-aarch64-windows running on linaro-armv8-windows-msvc-05 while building lldb at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/141/builds/6383

Here is the relevant piece of the build log for the reference
Step 6 (test) failure: build (failure)
...
PASS: lldb-api :: lang/cpp/structured-binding/TestStructuredBinding.py (851 of 2085)
PASS: lldb-api :: lang/cpp/subst_template_type_param/TestSubstTemplateTypeParam.py (852 of 2085)
UNSUPPORTED: lldb-api :: lang/cpp/symbols/TestSymbols.py (853 of 2085)
UNSUPPORTED: lldb-api :: lang/cpp/template-function/TestTemplateFunctions.py (854 of 2085)
PASS: lldb-api :: lang/cpp/template-arguments/TestCppTemplateArguments.py (855 of 2085)
PASS: lldb-api :: lang/cpp/template-specialization-type/TestTemplateSpecializationType.py (856 of 2085)
PASS: lldb-api :: lang/cpp/template/TestTemplateArgs.py (857 of 2085)
UNSUPPORTED: lldb-api :: lang/cpp/this/TestCPPThis.py (858 of 2085)
PASS: lldb-api :: lang/cpp/this_class_type_mixing/TestThisClassTypeMixing.py (859 of 2085)
UNSUPPORTED: lldb-api :: lang/cpp/thread_local/TestThreadLocal.py (860 of 2085)
FAIL: lldb-api :: lang/cpp/thunk/TestThunk.py (861 of 2085)
******************** TEST 'lldb-api :: lang/cpp/thunk/TestThunk.py' FAILED ********************
Script:
--
C:/Users/tcwg/scoop/apps/python/current/python.exe C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/llvm-project/lldb\test\API\dotest.py -u CXXFLAGS -u CFLAGS --env LLVM_LIBS_DIR=C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./lib --env LLVM_INCLUDE_DIR=C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/include --env LLVM_TOOLS_DIR=C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./bin --arch aarch64 --build-dir C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/lldb-test-build.noindex --lldb-module-cache-dir C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/lldb-test-build.noindex/module-cache-lldb\lldb-api --clang-module-cache-dir C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/lldb-test-build.noindex/module-cache-clang\lldb-api --executable C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./bin/lldb.exe --compiler C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./bin/clang.exe --dsymutil C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./bin/dsymutil.exe --make C:/Users/tcwg/scoop/shims/make.exe --llvm-tools-dir C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./bin --lldb-obj-root C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/tools/lldb --lldb-libs-dir C:/Users/tcwg/llvm-worker/lldb-aarch64-windows/build/./lib --skip-category=watchpoint C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\llvm-project\lldb\test\API\lang\cpp\thunk -p TestThunk.py
--
Exit Code: 1

Command Output (stdout):
--
lldb version 21.0.0git (https://github.com/llvm/llvm-project.git revision a3dc77c00a012bb613cb08e669dab4fadf88e935)
  clang revision a3dc77c00a012bb613cb08e669dab4fadf88e935
  llvm revision a3dc77c00a012bb613cb08e669dab4fadf88e935
Skipping the following test categories: ['watchpoint', 'libc++', 'libstdcxx', 'dwo', 'dsym', 'gmodules', 'debugserver', 'objc', 'fork', 'pexpect']


--
Command Output (stderr):
--
UNSUPPORTED: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_out_thunk_dsym (TestThunk.ThunkTest.test_step_out_thunk_dsym) (test case does not fall in any category of interest for this run) 

FAIL: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_out_thunk_dwarf (TestThunk.ThunkTest.test_step_out_thunk_dwarf)

UNSUPPORTED: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_out_thunk_dwo (TestThunk.ThunkTest.test_step_out_thunk_dwo) (test case does not fall in any category of interest for this run) 

UNSUPPORTED: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_through_thunk_dsym (TestThunk.ThunkTest.test_step_through_thunk_dsym) (test case does not fall in any category of interest for this run) 

PASS: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_through_thunk_dwarf (TestThunk.ThunkTest.test_step_through_thunk_dwarf)

UNSUPPORTED: LLDB (C:\Users\tcwg\llvm-worker\lldb-aarch64-windows\build\bin\clang.exe-aarch64) :: test_step_through_thunk_dwo (TestThunk.ThunkTest.test_step_through_thunk_dwo) (test case does not fall in any category of interest for this run) 

======================================================================

FAIL: test_step_out_thunk_dwarf (TestThunk.ThunkTest.test_step_out_thunk_dwarf)

----------------------------------------------------------------------

Traceback (most recent call last):


JDevlieghere added a commit that referenced this pull request Feb 18, 2025
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.

LLDB Not Stepping into Multiple Inheritance Virtual Functions
8 participants