Skip to content

Conversation

jasonmolenda
Copy link
Collaborator

@jasonmolenda jasonmolenda commented Aug 16, 2025

When a processor faults/is interrupted/gets an exception, it will stop running code and jump to an exception catcher routine. Most processors will store the pc that was executing in a system register, and the catcher functions have special instructions to retrieve that & possibly other registers. It may then save those values to stack, and the author can add .cfi directives to tell lldb's unwinder where to find those saved values.

ARM Cortex-M (microcontroller) processors have a simpler mechanism where a fixed set of registers are saved to the stack on an exception, and a unique value is put in the link register to indicate to the caller that this has taken place. No special handling needs to be written into the exception catcher, unless it wants to inspect these preserved values. And it is possible for a general stack walker to walk the stack with no special knowledge about what the catch function does.

This patch adds an Architecture plugin method to allow an Architecture to override/augment the UnwindPlan that lldb would use for a stack frame, given the contents of the return address register. It resembles a feature where the LanguageRuntime can replace/augment the unwind plan for a function, but it is doing it at offset by one level. The LanguageRuntime is looking at the local register context and/or symbol name to decide if it will override the unwind rules. For the Cortex-M exception unwinds, we need to modify THIS frame's unwind plan if the CALLER's LR had a specific value. RegisterContextUnwind has to retrieve the caller's LR value before it has completely decided on the UnwindPlan it will use for THIS stack frame.

This does mean that we will need one additional read of stack memory than we currently do when unwinding, on Armv7 Cortex-M targets. The unwinder walks the stack lazily, as stack frames are requested, and so now if you ask for 2 stack frames, we will read enough stack to walk 2 frames, plus we will read one extra word of memory, the spilled LR value from the stack. In practice, with 512-byte memory cache reads, this is unlikely to be a real performance hit.

This PR includes a test with a yaml corefile description and a JSON ObjectFile, incorporating all of the necessary stack memory and symbol names from a real debug session I worked on. The architectural default unwind plans are used for all stack frames except the 0th because there's no instructions for the functions, and no unwind info. I may need to add an encoding of unwind fules to ObjectFileJSON in the future as we create more test cases like this.

This PR depends on the yaml2macho-core utility from #153911 to run its API test.

rdar://110663219

When a processor faults/is interrupted/gets an exception, it will
stop running code and jump to an exception catcher routine.  Most
processors will store the pc that was executing in a system register,
and the catcher functions have special instructions to retrieve
that & possibly other registers.  It may then save those values to
stack, and the author can add .cfi directives to tell lldb's unwinder
where to find those saved values.

ARM Cortex-M (microcontroller) processors have a simpler mechanism
where a fixed set of registers are saved to the stack on an exception,
and a unique value is put in the link register to indicate to the
caller that this has taken place.  No special handling needs to be
written into the exception catcher, unless it wants to inspect these
preserved values.  And it is possible for a general stack walker to
walk the stack with no special knowledge about what the catch function
does.

This patch adds an Architecture plugin method to allow an Architecture
to override/augment the UnwindPlan that lldb would use for a stack
frame, given the contents of the return address register.  It
resembles a feature where the LanguageRuntime can replace/augment
the unwind plan for a function, but it is doing it at offset by one
level.  The LanguageRuntime is looking at the local register context
and/or symbol name to decide if it will override the unwind rules.
For the Cortex-M exception unwinds, we need to modify THIS frame's
unwind plan if the CALLER's LR had a specific value.  RegisterContextUnwind
has to retrieve the caller's LR value before it has completely
decided on the UnwindPlan it will use for THIS stack frame.

This does mean that we will need one additional read of stack memory
than we currently use when unwinding.  The unwinder walks the stack
lazily, as stack frames are requested, and so now if you ask for 2
stack frames, we will read enough stack to walk 2 frames, plus we
will read one extra word of memory, the spilled RA value from the
stack (see RegisterContextUnwind::AdoptArchitectureUnwindPlan()).
In practice, with 512-byte memory cache reads, this is unlikely to be
a problem, but I'm wondering if I should add an Architecture method
of "does this Architecture implement `GetArchitectureUnwindPlan`"
method -- and only do the memory read if it does.  So the performance
impact would be limited to armv7/Cortex-M debug sessions.

This PR includes a test with a yaml corefile description and a JSON
ObjectFile, incorporating all of the necessary stack memory and
symbol names from a real debug session I worked on.  The architectural
default unwind plans are used for all stack frames except the 0th
because there's no instructions for the functions, and no unwind
info.  I may need to add an encoding of unwind fules to ObjectFileJSON
in the future as we create more test cases like this.

This PR depends on the yaml2macho-core utility from
llvm#153911

rdar://110663219
@llvmbot
Copy link
Member

llvmbot commented Aug 16, 2025

@llvm/pr-subscribers-lldb

Author: Jason Molenda (jasonmolenda)

Changes

When a processor faults/is interrupted/gets an exception, it will stop running code and jump to an exception catcher routine. Most processors will store the pc that was executing in a system register, and the catcher functions have special instructions to retrieve that & possibly other registers. It may then save those values to stack, and the author can add .cfi directives to tell lldb's unwinder where to find those saved values.

ARM Cortex-M (microcontroller) processors have a simpler mechanism where a fixed set of registers are saved to the stack on an exception, and a unique value is put in the link register to indicate to the caller that this has taken place. No special handling needs to be written into the exception catcher, unless it wants to inspect these preserved values. And it is possible for a general stack walker to walk the stack with no special knowledge about what the catch function does.

This patch adds an Architecture plugin method to allow an Architecture to override/augment the UnwindPlan that lldb would use for a stack frame, given the contents of the return address register. It resembles a feature where the LanguageRuntime can replace/augment the unwind plan for a function, but it is doing it at offset by one level. The LanguageRuntime is looking at the local register context and/or symbol name to decide if it will override the unwind rules. For the Cortex-M exception unwinds, we need to modify THIS frame's unwind plan if the CALLER's LR had a specific value. RegisterContextUnwind has to retrieve the caller's LR value before it has completely decided on the UnwindPlan it will use for THIS stack frame.

This does mean that we will need one additional read of stack memory than we currently use when unwinding. The unwinder walks the stack lazily, as stack frames are requested, and so now if you ask for 2 stack frames, we will read enough stack to walk 2 frames, plus we will read one extra word of memory, the spilled RA value from the stack (see RegisterContextUnwind::AdoptArchitectureUnwindPlan()). In practice, with 512-byte memory cache reads, this is unlikely to be a problem, but I'm wondering if I should add an Architecture method of "does this Architecture implement GetArchitectureUnwindPlan" method -- and only do the memory read if it does. So the performance impact would be limited to armv7/Cortex-M debug sessions.

This PR includes a test with a yaml corefile description and a JSON ObjectFile, incorporating all of the necessary stack memory and symbol names from a real debug session I worked on. The architectural default unwind plans are used for all stack frames except the 0th because there's no instructions for the functions, and no unwind info. I may need to add an encoding of unwind fules to ObjectFileJSON in the future as we create more test cases like this.

This PR depends on the yaml2macho-core utility from #153911

rdar://110663219


Patch is 23.81 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/153922.diff

13 Files Affected:

  • (modified) lldb/include/lldb/Core/Architecture.h (+9)
  • (modified) lldb/include/lldb/Target/RegisterContextUnwind.h (+2)
  • (modified) lldb/source/Plugins/ABI/ARM/ABISysV_arm.cpp (+7)
  • (modified) lldb/source/Plugins/Architecture/Arm/ArchitectureArm.cpp (+120)
  • (modified) lldb/source/Plugins/Architecture/Arm/ArchitectureArm.h (+6)
  • (modified) lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.cpp (+36)
  • (modified) lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.h (+3)
  • (modified) lldb/source/Plugins/Process/mach-core/ProcessMachCore.cpp (+17)
  • (modified) lldb/source/Target/RegisterContextUnwind.cpp (+81)
  • (added) lldb/test/API/functionalities/unwind/cortex-m-exception/Makefile (+1)
  • (added) lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py (+48)
  • (added) lldb/test/API/functionalities/unwind/cortex-m-exception/armv7m-nofpu-exception.yaml (+65)
  • (added) lldb/test/API/functionalities/unwind/cortex-m-exception/binary.json (+41)
diff --git a/lldb/include/lldb/Core/Architecture.h b/lldb/include/lldb/Core/Architecture.h
index b6fc1a20e1e69..9263ea9d00d0d 100644
--- a/lldb/include/lldb/Core/Architecture.h
+++ b/lldb/include/lldb/Core/Architecture.h
@@ -129,6 +129,15 @@ class Architecture : public PluginInterface {
                                        RegisterContext &reg_context) const {
     return false;
   }
+
+  /// Return an UnwindPlan that allows architecture-defined rules for finding
+  /// saved registers in a specific context (not specific to a function's
+  /// instructions/unwind info).
+  virtual lldb::UnwindPlanSP GetArchitectureUnwindPlan(
+      lldb_private::Thread &thread, lldb::addr_t callers_return_address,
+      lldb::addr_t cfa, std::shared_ptr<const UnwindPlan> current_unwindplan) {
+    return lldb::UnwindPlanSP();
+  }
 };
 
 } // namespace lldb_private
diff --git a/lldb/include/lldb/Target/RegisterContextUnwind.h b/lldb/include/lldb/Target/RegisterContextUnwind.h
index b10a364823b83..e8d43eeba3e75 100644
--- a/lldb/include/lldb/Target/RegisterContextUnwind.h
+++ b/lldb/include/lldb/Target/RegisterContextUnwind.h
@@ -202,6 +202,8 @@ class RegisterContextUnwind : public lldb_private::RegisterContext {
 
   std::shared_ptr<const UnwindPlan> GetFullUnwindPlanForFrame();
 
+  lldb::UnwindPlanSP AdoptArchitectureUnwindPlan();
+
   void UnwindLogMsg(const char *fmt, ...) __attribute__((format(printf, 2, 3)));
 
   void UnwindLogMsgVerbose(const char *fmt, ...)
diff --git a/lldb/source/Plugins/ABI/ARM/ABISysV_arm.cpp b/lldb/source/Plugins/ABI/ARM/ABISysV_arm.cpp
index 2bcb2c0de97ac..bb0c4ba3f1b57 100644
--- a/lldb/source/Plugins/ABI/ARM/ABISysV_arm.cpp
+++ b/lldb/source/Plugins/ABI/ARM/ABISysV_arm.cpp
@@ -1921,6 +1921,13 @@ UnwindPlanSP ABISysV_arm::CreateFunctionEntryUnwindPlan() {
 
 UnwindPlanSP ABISysV_arm::CreateDefaultUnwindPlan() {
   // TODO: Handle thumb
+  // If we had a Target argument, could at least check
+  // target.GetArchitecture().GetTriple().isArmMClass()
+  // which is always thumb.
+  // To handle thumb properly, we'd need to fetch the current
+  // CPSR state at unwind time to tell if the processor is
+  // in thumb mode in this stack frame.  There's no way to
+  // express something like that in an UnwindPlan today.
   uint32_t fp_reg_num = dwarf_r11;
   uint32_t pc_reg_num = dwarf_pc;
 
diff --git a/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.cpp b/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.cpp
index 81c72122cb7e5..d8c21af976ed4 100644
--- a/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.cpp
+++ b/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.cpp
@@ -9,10 +9,15 @@
 #include "Plugins/Architecture/Arm/ArchitectureArm.h"
 #include "Plugins/Process/Utility/ARMDefines.h"
 #include "Plugins/Process/Utility/InstructionUtils.h"
+#include "Utility/ARM_DWARF_Registers.h"
 #include "lldb/Core/PluginManager.h"
+#include "lldb/Symbol/UnwindPlan.h"
+#include "lldb/Target/Process.h"
 #include "lldb/Target/RegisterContext.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/ArchSpec.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
 
 using namespace lldb_private;
 using namespace lldb;
@@ -150,3 +155,118 @@ addr_t ArchitectureArm::GetOpcodeLoadAddress(addr_t opcode_addr,
   }
   return opcode_addr & ~(1ull);
 }
+
+// The ARM M-Profile Armv7-M Architecture Reference Manual
+// "Exception return behavior" describes how the processor
+// saves registers to the stack, decrements the stack pointer,
+// puts a special value in $lr, and then calls a registered
+// exception handler routine.
+//
+// Detect that special value in $lr, and if present, add
+// unwind rules for the registers that were saved above this
+// stack frame's CFA.  Overwrite any register locations that
+// the current_unwindplan has for these registers; they are
+// not correct when we're invoked this way.
+UnwindPlanSP ArchitectureArm::GetArchitectureUnwindPlan(
+    Thread &thread, addr_t callers_return_address, addr_t cfa,
+    std::shared_ptr<const UnwindPlan> current_unwindplan) {
+
+  ProcessSP process_sp = thread.GetProcess();
+  if (!process_sp)
+    return {};
+
+  const ArchSpec arch = process_sp->GetTarget().GetArchitecture();
+  if (!arch.GetTriple().isArmMClass() || arch.GetAddressByteSize() != 4)
+    return {};
+
+  if (callers_return_address != 0xFFFFFFF1 &&
+      callers_return_address != 0xFFFFFFF9 &&
+      callers_return_address != 0xFFFFFFFD &&
+      callers_return_address != 0xFFFFFFE1 &&
+      callers_return_address != 0xFFFFFFE9 &&
+      callers_return_address != 0xFFFFFFED)
+    return {};
+
+  const RegisterKind plan_regkind = current_unwindplan->GetRegisterKind();
+  UnwindPlanSP new_plan = std::make_shared<UnwindPlan>(plan_regkind);
+  new_plan->SetSourceName("Arm Cortex-M exception return UnwindPlan");
+  new_plan->SetSourcedFromCompiler(eLazyBoolNo);
+  new_plan->SetUnwindPlanValidAtAllInstructions(eLazyBoolYes);
+  new_plan->SetUnwindPlanForSignalTrap(eLazyBoolYes);
+
+  // bit 4 will be 1 if only the general purpose registers were saved.
+  // bit 4 will be 0 if the GPRs + floating point registers were saved.
+  const bool fp_regs_saved = (callers_return_address & 0x10) == 0;
+
+  int stored_regs_size = 0x20;
+  if (fp_regs_saved)
+    stored_regs_size = 0x68;
+
+  uint32_t gpr_regs[] = {dwarf_r0,  dwarf_r1, dwarf_r2, dwarf_r3,
+                         dwarf_r12, dwarf_lr, dwarf_pc, dwarf_cpsr};
+  const int gpr_reg_count = sizeof(gpr_regs) / sizeof(uint32_t);
+  uint32_t fpr_regs[] = {dwarf_s0,  dwarf_s1,  dwarf_s2,  dwarf_s3,
+                         dwarf_s4,  dwarf_s5,  dwarf_s6,  dwarf_s7,
+                         dwarf_s8,  dwarf_s9,  dwarf_s10, dwarf_s11,
+                         dwarf_s12, dwarf_s13, dwarf_s14, dwarf_s15};
+  const int fpr_reg_count = sizeof(fpr_regs) / sizeof(uint32_t);
+
+  RegisterContextSP reg_ctx_sp = thread.GetRegisterContext();
+  std::vector<uint32_t> saved_regs;
+  for (int i = 0; i < gpr_reg_count; i++) {
+    uint32_t regno = gpr_regs[i];
+    reg_ctx_sp->ConvertBetweenRegisterKinds(eRegisterKindDWARF, gpr_regs[i],
+                                            plan_regkind, regno);
+    saved_regs.push_back(regno);
+  }
+  if (fp_regs_saved) {
+    for (int i = 0; i < fpr_reg_count; i++) {
+      uint32_t regno = fpr_regs[i];
+      reg_ctx_sp->ConvertBetweenRegisterKinds(eRegisterKindDWARF, fpr_regs[i],
+                                              plan_regkind, regno);
+      saved_regs.push_back(regno);
+    }
+  }
+
+  // PSR bit 9 indicates that the stack pointer was unaligned (to
+  // an 8-byte alignment) when the exception happened, and we must
+  // account for that when restoring the excepted stack pointer value.
+  Status error;
+  uint32_t callers_xPSR =
+      process_sp->ReadUnsignedIntegerFromMemory(cfa + 0x28, 4, 0, error);
+  const bool align_stack = callers_xPSR & (1 << 9U);
+  uint32_t callers_sp = cfa + stored_regs_size;
+  if (align_stack)
+    callers_sp |= 4;
+
+  Log *log = GetLog(LLDBLog::Unwind);
+  LLDB_LOGF(log,
+            "ArchitectureArm::GetArchitectureUnwindPlan found caller return "
+            "addr of 0x%" PRIx64 ", for frame with CFA 0x%" PRIx64
+            ", fp_regs_saved %d, stored_regs_size 0x%x, align stack %d",
+            callers_return_address, cfa, fp_regs_saved, stored_regs_size,
+            align_stack);
+
+  uint32_t sp_regnum = dwarf_sp;
+  reg_ctx_sp->ConvertBetweenRegisterKinds(eRegisterKindDWARF, dwarf_sp,
+                                          plan_regkind, sp_regnum);
+
+  const int row_count = current_unwindplan->GetRowCount();
+  for (int i = 0; i < row_count; i++) {
+    UnwindPlan::Row row = *current_unwindplan->GetRowAtIndex(i);
+    uint32_t offset = 0;
+    const size_t saved_reg_count = saved_regs.size();
+    for (size_t j = 0; j < saved_reg_count; j++) {
+      // The locations could be set with
+      // SetRegisterLocationToIsConstant(regno, cfa+offset)
+      // expressing it in terms of CFA addr+offset - this UnwindPlan
+      // is only used once, with this specific CFA.  I'm not sure
+      // which will be clearer for someone reading the unwind log.
+      row.SetRegisterLocationToAtCFAPlusOffset(saved_regs[j], offset, true);
+      offset += 4;
+    }
+    row.SetRegisterLocationToIsCFAPlusOffset(sp_regnum, callers_sp - cfa, true);
+    new_plan->AppendRow(row);
+  }
+  return new_plan;
+}
diff --git a/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.h b/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.h
index f579d6b625051..b8fc23f95b822 100644
--- a/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.h
+++ b/lldb/source/Plugins/Architecture/Arm/ArchitectureArm.h
@@ -10,6 +10,7 @@
 #define LLDB_SOURCE_PLUGINS_ARCHITECTURE_ARM_ARCHITECTUREARM_H
 
 #include "lldb/Core/Architecture.h"
+#include "lldb/Target/Thread.h"
 
 namespace lldb_private {
 
@@ -29,6 +30,11 @@ class ArchitectureArm : public Architecture {
   lldb::addr_t GetOpcodeLoadAddress(lldb::addr_t load_addr,
                                     AddressClass addr_class) const override;
 
+  lldb::UnwindPlanSP GetArchitectureUnwindPlan(
+      lldb_private::Thread &thread, lldb::addr_t callers_return_address,
+      lldb::addr_t cfa,
+      std::shared_ptr<const UnwindPlan> current_unwindplan) override;
+
 private:
   static std::unique_ptr<Architecture> Create(const ArchSpec &arch);
   ArchitectureArm() = default;
diff --git a/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.cpp b/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.cpp
index cb8ba05d461d4..0aff98078120e 100644
--- a/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.cpp
+++ b/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.cpp
@@ -12,6 +12,7 @@
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Core/Section.h"
 #include "lldb/Symbol/Symbol.h"
+#include "lldb/Target/Target.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "llvm/ADT/DenseSet.h"
@@ -233,6 +234,41 @@ void ObjectFileJSON::CreateSections(SectionList &unified_section_list) {
   }
 }
 
+bool ObjectFileJSON::SetLoadAddress(Target &target, lldb::addr_t value,
+                                    bool value_is_offset) {
+  Log *log(GetLog(LLDBLog::DynamicLoader));
+  if (!m_sections_up)
+    return true;
+
+  const bool warn_multiple = true;
+
+  addr_t slide = value;
+  if (!value_is_offset) {
+    addr_t lowest_addr = LLDB_INVALID_ADDRESS;
+    for (const SectionSP &section_sp : *m_sections_up) {
+      addr_t section_load_addr = section_sp->GetFileAddress();
+      lowest_addr = std::min(lowest_addr, section_load_addr);
+    }
+    if (lowest_addr == LLDB_INVALID_ADDRESS)
+      return false;
+    slide = value - lowest_addr;
+  }
+
+  // Apply slide to each section's file address.
+  for (const SectionSP &section_sp : *m_sections_up) {
+    addr_t section_load_addr = section_sp->GetFileAddress();
+    if (section_load_addr != LLDB_INVALID_ADDRESS) {
+      LLDB_LOGF(
+          log,
+          "ObjectFileJSON::SetLoadAddress section %s to load addr 0x%" PRIx64,
+          section_sp->GetName().AsCString(), section_load_addr + slide);
+      target.SetSectionLoadAddress(section_sp, section_load_addr + slide,
+                                   warn_multiple);
+    }
+  }
+  return true;
+}
+
 bool ObjectFileJSON::MagicBytesMatch(DataBufferSP data_sp,
                                      lldb::addr_t data_offset,
                                      lldb::addr_t data_length) {
diff --git a/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.h b/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.h
index b72565f468862..029c8ff188934 100644
--- a/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.h
+++ b/lldb/source/Plugins/ObjectFile/JSON/ObjectFileJSON.h
@@ -86,6 +86,9 @@ class ObjectFileJSON : public ObjectFile {
 
   Strata CalculateStrata() override { return eStrataUser; }
 
+  bool SetLoadAddress(Target &target, lldb::addr_t value,
+                      bool value_is_offset) override;
+
   static bool MagicBytesMatch(lldb::DataBufferSP data_sp, lldb::addr_t offset,
                               lldb::addr_t length);
 
diff --git a/lldb/source/Plugins/Process/mach-core/ProcessMachCore.cpp b/lldb/source/Plugins/Process/mach-core/ProcessMachCore.cpp
index 6037c8d2514b8..a780b3f59aded 100644
--- a/lldb/source/Plugins/Process/mach-core/ProcessMachCore.cpp
+++ b/lldb/source/Plugins/Process/mach-core/ProcessMachCore.cpp
@@ -799,6 +799,23 @@ Status ProcessMachCore::DoGetMemoryRegionInfo(addr_t load_addr,
       region_info.SetMapped(MemoryRegionInfo::eNo);
     }
     return Status();
+  } else {
+    // The corefile has no LC_SEGMENT at this virtual address,
+    // but see if there is a binary whose Section has been
+    // loaded at that address in the current Target.
+    Address addr;
+    if (GetTarget().ResolveLoadAddress(load_addr, addr)) {
+      SectionSP section_sp(addr.GetSection());
+      if (section_sp) {
+        region_info.GetRange().SetRangeBase(
+            section_sp->GetLoadBaseAddress(&GetTarget()));
+        region_info.GetRange().SetByteSize(section_sp->GetByteSize());
+        if (region_info.GetRange().Contains(load_addr)) {
+          region_info.SetLLDBPermissions(section_sp->GetPermissions());
+          return Status();
+        }
+      }
+    }
   }
 
   region_info.GetRange().SetRangeBase(load_addr);
diff --git a/lldb/source/Target/RegisterContextUnwind.cpp b/lldb/source/Target/RegisterContextUnwind.cpp
index bcf1297f2114f..1442c118e6d0b 100644
--- a/lldb/source/Target/RegisterContextUnwind.cpp
+++ b/lldb/source/Target/RegisterContextUnwind.cpp
@@ -293,6 +293,9 @@ void RegisterContextUnwind::InitializeZerothFrame() {
     return;
   }
 
+  // Give the Architecture a chance to replace the UnwindPlan.
+  AdoptArchitectureUnwindPlan();
+
   UnwindLogMsg("initialized frame current pc is 0x%" PRIx64 " cfa is 0x%" PRIx64
                " afa is 0x%" PRIx64 " using %s UnwindPlan",
                (uint64_t)m_current_pc.GetLoadAddress(exe_ctx.GetTargetPtr()),
@@ -482,6 +485,9 @@ void RegisterContextUnwind::InitializeNonZerothFrame() {
         }
       }
 
+      // Give the Architecture a chance to replace the UnwindPlan.
+      AdoptArchitectureUnwindPlan();
+
       UnwindLogMsg("initialized frame cfa is 0x%" PRIx64 " afa is 0x%" PRIx64,
                    (uint64_t)m_cfa, (uint64_t)m_afa);
       return;
@@ -686,6 +692,9 @@ void RegisterContextUnwind::InitializeNonZerothFrame() {
     }
   }
 
+  // Give the Architecture a chance to replace the UnwindPlan.
+  AdoptArchitectureUnwindPlan();
+
   UnwindLogMsg("initialized frame current pc is 0x%" PRIx64
                " cfa is 0x%" PRIx64 " afa is 0x%" PRIx64,
                (uint64_t)m_current_pc.GetLoadAddress(exe_ctx.GetTargetPtr()),
@@ -1717,6 +1726,78 @@ RegisterContextUnwind::SavedLocationForRegister(
   return UnwindLLDB::RegisterSearchResult::eRegisterNotFound;
 }
 
+UnwindPlanSP RegisterContextUnwind::AdoptArchitectureUnwindPlan() {
+  if (!m_full_unwind_plan_sp)
+    return {};
+  ProcessSP process_sp = m_thread.GetProcess();
+  if (!process_sp)
+    return {};
+
+  UnwindPlanSP arch_override_plan_sp;
+
+  RegisterNumber ra_regnum(m_thread, eRegisterKindGeneric,
+                           LLDB_REGNUM_GENERIC_RA);
+  uint32_t ra_regnum_lldb = ra_regnum.GetAsKind(eRegisterKindLLDB);
+  // The only Architecture UnwindPlan today requires the value of
+  // the return address register.
+  if (ra_regnum_lldb == LLDB_INVALID_REGNUM)
+    return {};
+
+  UnwindLLDB::ConcreteRegisterLocation regloc = {};
+  bool got_concrete_location = false;
+  if (SavedLocationForRegister(ra_regnum_lldb, regloc) ==
+      UnwindLLDB::RegisterSearchResult::eRegisterFound) {
+    got_concrete_location = true;
+  } else {
+    RegisterNumber pc_regnum(m_thread, eRegisterKindGeneric,
+                             LLDB_REGNUM_GENERIC_PC);
+    uint32_t pc_regnum_lldb = pc_regnum.GetAsKind(eRegisterKindLLDB);
+    if (SavedLocationForRegister(pc_regnum_lldb, regloc) ==
+        UnwindLLDB::RegisterSearchResult::eRegisterFound)
+      got_concrete_location = true;
+  }
+
+  if (got_concrete_location) {
+    const RegisterInfo *reg_info = GetRegisterInfoAtIndex(ra_regnum_lldb);
+    if (reg_info) {
+      RegisterValue reg_value;
+      if (ReadRegisterValueFromRegisterLocation(regloc, reg_info, reg_value)) {
+        addr_t ra;
+        if (process_sp->GetTarget().GetArchitecture().GetAddressByteSize() == 4)
+          ra = reg_value.GetAsUInt32();
+        else
+          ra = reg_value.GetAsUInt64();
+        if (Architecture *arch =
+                process_sp->GetTarget().GetArchitecturePlugin())
+          arch_override_plan_sp = arch->GetArchitectureUnwindPlan(
+              m_thread, ra, m_cfa, m_full_unwind_plan_sp);
+      }
+    }
+  }
+  if (arch_override_plan_sp) {
+    m_full_unwind_plan_sp = arch_override_plan_sp;
+    PropagateTrapHandlerFlagFromUnwindPlan(m_full_unwind_plan_sp);
+    m_registers.clear();
+    Log *log = GetLog(LLDBLog::Unwind);
+    if (log) {
+      UnwindLogMsg(
+          "Replacing Full Unwindplan with Architecture UnwindPlan, '%s'",
+          m_full_unwind_plan_sp->GetSourceName().AsCString());
+      const UnwindPlan::Row *active_row =
+          m_full_unwind_plan_sp->GetRowForFunctionOffset(m_current_offset);
+      if (active_row) {
+        StreamString active_row_strm;
+        active_row->Dump(active_row_strm, m_full_unwind_plan_sp.get(),
+                         &m_thread,
+                         m_start_pc.GetLoadAddress(&process_sp->GetTarget()));
+        UnwindLogMsg("%s", active_row_strm.GetData());
+      }
+    }
+  }
+
+  return {};
+}
+
 // TryFallbackUnwindPlan() -- this method is a little tricky.
 //
 // When this is called, the frame above -- the caller frame, the "previous"
diff --git a/lldb/test/API/functionalities/unwind/cortex-m-exception/Makefile b/lldb/test/API/functionalities/unwind/cortex-m-exception/Makefile
new file mode 100644
index 0000000000000..22f1051530f87
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/cortex-m-exception/Makefile
@@ -0,0 +1 @@
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py b/lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py
new file mode 100644
index 0000000000000..2ddc9c7d4c70d
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py
@@ -0,0 +1,48 @@
+"""
+Test that we can backtrace up an ARM Cortex-M Exception return stack
+"""
+
+import lldb
+import json
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCortexMExceptionUnwind(TestBase):
+    def test_no_fpu(self):
+        """Test that we can backtrace correctly through an ARM Cortex-M Exception return stack"""
+
+        target = self.dbg.CreateTarget("")
+        exe = "binary.json"
+        f = open(exe)
+        exe_json = json.load(f)
+        exe_uuid = exe_json["uuid"]
+        target.AddModule(exe, "", exe_uuid)
+        self.assertTrue(target.IsValid())
+
+        core = self.getBuildArtifact("core")
+        self.yaml2macho_core("armv7m-nofpu-exception.yaml", core, exe_uuid)
+
+        process = target.LoadCore(core)
+        self.assertTrue(process.IsValid())
+
+        if self.TraceOn():
+            self.runCmd("image list")
+            self.runCmd("target modules dump sections")
+            self.runCmd("target modules dump symtab")
+            self.runCmd("bt")
+
+        thread = process.GetThreadAtIndex(0)
+        self.assertTrue(thread.IsValid())
+
+        self.assertEqual(thread.GetNumFrames(), 6)
+        stackframe_names = [
+            "exception_catcher",
+            "exception_catcher",
+            "exception_thrower",
+            "main",
+        ]
+        for i in range(4):
+            name = thread.GetFrameAtIndex(i).GetSymbol().GetName()
+            self.assertEqual(stackframe_names[i], name)
diff --git a/lldb/test/API/functionalities/unwind/cortex-m-exception/armv7m-nofpu-exception.yaml b/lldb/test/API/functionalities/unwind/cortex-m-exception/armv7m-nofpu-exception.yaml
new file mode 100644
index 0000000000000..327ed22f3cb88
--- /dev/null
+++ b/lldb/test/API/functionalities/unwind/cortex-m-exception/armv7m-nofpu-exception.yaml
@@ -0,0 +1,65 @@
+cpu: armv7m
+endian: little
+threads:
...
[truncated]

accept a RegisterContextUnwind pointer, and expose the method from
that class to fetch the caller's register state.  Normally a
RegisterContext will only retrieve this stack frame's register
values, but we need the caller's LR value to determine the correct
UnwindPlan for this stack frame.

This also solves the problem of fetching the return address register
value for the caller function on all RA-having targets.  Now the
RA register value will only be fetched on armv7 Cortex-M targets.
This register fetch may result in an additional memory read packet
in gdb remote serial protocol, and incur a small performance cost.
I removed support for this from yaml2macho-core.
thread = process.GetThreadAtIndex(0)
self.assertTrue(thread.IsValid())

self.assertEqual(thread.GetNumFrames(), 6)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You check 4 frames but expect 6?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah that was the nature of the actual stack trace in the example corefile - it had two stack frames above this that were irrelevant and wouldn't indicate an unwind issue, so I didn't bother to carry those over into the binary.json symbol name/addresses.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs some comment then because it's the first thing I'll waste time wondering about if/when this test fails in the future.

good suggestions.  I need to review his comments and double
check that everything was addressed, but I think this is in a
better state now so I want to checkpoint it.
@jasonmolenda
Copy link
Collaborator Author

Thanks again for all the helpful review comments @DavidSpickett I really appreciate it. I've updated the PR to address them. The one significant choice that we haven't agreed on at this point is whether Architecture::GetArchitectureUnwindPlan should return an empty UnwindPlanSP to indicate no override, or if it should return the std::shared_ptr<const UnwindPlan> identical to its input if it indicates that no override has been done. I want RegisterContetUnwind to be able to detect if its chosen UnwindPlan has changed, so it's mainly a question of how that is most clearly indicated, I think.

@JDevlieghere
Copy link
Member

The one significant choice that we haven't agreed on at this point is whether Architecture::GetArchitectureUnwindPlan should return an empty UnwindPlanSP to indicate no override, or if it should return the std::shared_ptr<const UnwindPlan> identical to its input if it indicates that no override has been done. I want RegisterContetUnwind to be able to detect if its chosen UnwindPlan has changed, so it's mainly a question of how that is most clearly indicated, I think.

It looks like David marked the conversation as resolved which suggests that he's happy with your explanation. FWIW, personally I tend to side with the approach that retains the most fidelity. Here I'd argue that it's easier/cleaner to recognize an empty UnwindPlanSP than to do a pointer comparison.

@DavidSpickett
Copy link
Collaborator

DavidSpickett commented Sep 9, 2025

The one significant choice that we haven't agreed on at this point is whether Architecture::GetArchitectureUnwindPlan should return an empty UnwindPlanSP to indicate no override, or if it should return the std::shared_ptr identical to its input if it indicates that no override has been done. I want RegisterContetUnwind to be able to detect if its chosen UnwindPlan has changed, so it's mainly a question of how that is most clearly indicated, I think.

Yeah it's fine as it is.

At first I wondered why you didn't just do plan = dostuffmaybeto(plan) but yeah you need some flag that says "yes something was done" because you need to update other stuff if something did change.

Here I'd argue that it's easier/cleaner to recognize an empty UnwindPlanSP than to do a pointer comparison.

Yes this sounds good to me. Effectively optional<plan> isThereAnotherPlanBasedOn(current_plan).

@jasonmolenda jasonmolenda merged commit 69511ae into llvm:main Sep 9, 2025
9 checks passed
@dzhidzhoev
Copy link
Member

Please note that this patch has broken lldb-remote-linux-ubuntu.

FAIL: LLDB (/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/build/bin/clang-aarch64) :: test_no_fpu (TestCortexMExceptionUnwind.TestCortexMExceptionUnwind.test_no_fpu)
======================================================================
FAIL: test_no_fpu (TestCortexMExceptionUnwind.TestCortexMExceptionUnwind.test_no_fpu)
   Test that we can backtrace correctly through an ARM Cortex-M Exception return stack
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/llvm-project/lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py", line 42, in test_no_fpu
    self.assertEqual(thread.GetNumFrames(), 6)
AssertionError: 1 != 6
Config=aarch64-/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/build/bin/clang
----------------------------------------------------------------------

https://lab.llvm.org/buildbot/#/builders/195/builds/14363

@jasonmolenda
Copy link
Collaborator Author

Please note that this patch has broken lldb-remote-linux-ubuntu.

FAIL: LLDB (/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/build/bin/clang-aarch64) :: test_no_fpu (TestCortexMExceptionUnwind.TestCortexMExceptionUnwind.test_no_fpu)
======================================================================
FAIL: test_no_fpu (TestCortexMExceptionUnwind.TestCortexMExceptionUnwind.test_no_fpu)
   Test that we can backtrace correctly through an ARM Cortex-M Exception return stack
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/llvm-project/lldb/test/API/functionalities/unwind/cortex-m-exception/TestCortexMExceptionUnwind.py", line 42, in test_no_fpu
    self.assertEqual(thread.GetNumFrames(), 6)
AssertionError: 1 != 6
Config=aarch64-/home/buildbot/worker/as-builder-9/lldb-remote-linux-ubuntu/build/bin/clang
----------------------------------------------------------------------

https://lab.llvm.org/buildbot/#/builders/195/builds/14363

Thanks, I added a skipUnlessDarwin to the test until I have a fix. On Darwin systems, this correctly picks the ABIMacOSX_arm ABI (via the binary.json's triple being armv7m-apple), but when run on the lldb-remote-linux-ubuntu bot (but only that bot? I got no emails from any other), it is picking the ABISysV_arm ABI plugin. ABISysV_arm::CreateDefaultUnwindPlan doesn't check if the stack frame is thumb, or if the Target triple is thumb-only (as it is on Cortex-M cores), so it picks the wrong register as the frame pointer, and we fail to walk the stack.

The Darwin ABI for armv7 diverges from AAPCS in that it always uses r7 for thumb or arm code, so the inability to detect the processor mode/target triple is not a problem.

The main question is why the binary.json triple of armv7m-apple didn't force the Target triple to that, and pick the correct ABI plugin. I'll try to build this in an ubuntu vm and investigate later, not clear if this is actually only happening on a 32-bit arm ubuntu system or if it can repo on a 64-bit ubuntu system.

@dzhidzhoev
Copy link
Member

Thank you!

but only that bot? I got no emails from any other

Indeed, lldb-aarch64-ubuntu was green.

I'll try to build this in an ubuntu vm and investigate later, not clear if this is actually only happening on a 32-bit arm ubuntu system or if it can repo on a 64-bit ubuntu system.

Target architecture on the buildbot is ARMv8.2-A.

jasonmolenda added a commit to jasonmolenda/llvm-project that referenced this pull request Sep 9, 2025
)

When a processor faults/is interrupted/gets an exception, it will stop
running code and jump to an exception catcher routine. Most processors
will store the pc that was executing in a system register, and the
catcher functions have special instructions to retrieve that & possibly
other registers. It may then save those values to stack, and the author
can add .cfi directives to tell lldb's unwinder where to find those
saved values.

ARM Cortex-M (microcontroller) processors have a simpler mechanism where
a fixed set of registers are saved to the stack on an exception, and a
unique value is put in the link register to indicate to the caller that
this has taken place. No special handling needs to be written into the
exception catcher, unless it wants to inspect these preserved values.
And it is possible for a general stack walker to walk the stack with no
special knowledge about what the catch function does.

This patch adds an Architecture plugin method to allow an Architecture
to override/augment the UnwindPlan that lldb would use for a stack
frame, given the contents of the return address register. It resembles a
feature where the LanguageRuntime can replace/augment the unwind plan
for a function, but it is doing it at offset by one level. The
LanguageRuntime is looking at the local register context and/or symbol
name to decide if it will override the unwind rules. For the Cortex-M
exception unwinds, we need to modify THIS frame's unwind plan if the
CALLER's LR had a specific value. RegisterContextUnwind has to retrieve
the caller's LR value before it has completely decided on the UnwindPlan
it will use for THIS stack frame.

This does mean that we will need one additional read of stack memory
than we currently do when unwinding, on Armv7 Cortex-M targets. The
unwinder walks the stack lazily, as stack frames are requested, and so
now if you ask for 2 stack frames, we will read enough stack to walk 2
frames, plus we will read one extra word of memory, the spilled LR value
from the stack. In practice, with 512-byte memory cache reads, this is
unlikely to be a real performance hit.

This PR includes a test with a yaml corefile description and a JSON
ObjectFile, incorporating all of the necessary stack memory and symbol
names from a real debug session I worked on. The architectural default
unwind plans are used for all stack frames except the 0th because
there's no instructions for the functions, and no unwind info. I may
need to add an encoding of unwind fules to ObjectFileJSON in the future
as we create more test cases like this.

This PR depends on the yaml2macho-core utility from
llvm#153911 to run its API test.

rdar://110663219
(cherry picked from commit 69511ae)
jasonmolenda added a commit to swiftlang/llvm-project that referenced this pull request Sep 10, 2025
…ception-unwind-21.x

[lldb] Unwind through ARM Cortex-M exceptions automatically (llvm#153922)
rupprecht added a commit to rupprecht/llvm-project that referenced this pull request Sep 10, 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.

5 participants