Skip to content

[clang][DebugInfo] Add virtuality call-site target information in DWARF.#167666

Merged
CarlosAlbertoEnciso merged 4 commits intollvm:mainfrom
CarlosAlbertoEnciso:callsite-indirect-calls
Feb 19, 2026
Merged

[clang][DebugInfo] Add virtuality call-site target information in DWARF.#167666
CarlosAlbertoEnciso merged 4 commits intollvm:mainfrom
CarlosAlbertoEnciso:callsite-indirect-calls

Conversation

@CarlosAlbertoEnciso
Copy link
Member

@CarlosAlbertoEnciso CarlosAlbertoEnciso commented Nov 12, 2025

Given the test case:

struct CBase {
  virtual void foo();
};

void bar(CBase *Base) {
  Base->foo();
}

and using '-emit-call-site-info' with 'llc', currently the following DWARF is produced for the indirect call 'Base->foo()':

0x10: DW_TAG_structure_type
        DW_AT_name	("CBase")

0x20:   DW_TAG_subprogram
          DW_AT_linkage_name	("_ZN5CBase3fooEv")
          DW_AT_name	("foo")
          ...

0x30: DW_TAG_subprogram
        DW_AT_linkage_name	("_Z3barP5CBase")
        DW_AT_name	("bar")
        ...

0x40:   DW_TAG_call_site
          DW_AT_call_target_clobbered	(DW_OP_breg0 RAX+0)
          DW_AT_call_return_pc	(0x000000000000000a)

Initial context from the SCE debugger point of view:

We can detect when a function has been affected by Identical Code Folding (ICF) from DWARF call-site information. For example,

  1. $RIP is currently in 'bar()'
  2. The call-site information in the parent frame indicates we should have called 'foo()'

If we see a situation like the above, we can infer that 'foo()' and 'bar()' have been code folded. This technique breaks when dealing with virtual functions because the call-site information only provides a way to find the target function at runtime.

However, if the call-site information includes information on which virtual function is being called, we can compare this against the $RIP location to see if we are in an implementation of the virtual function. If we are not, then we can assume we have been code folded.

For this to work we just to need to record which virtual function we are calling. We do not need to know the type of the 'this' pointer at the call-site.

This patch (available for all debuggers) helps in the identification of the intended target of a virtual call in the SCE debugger.

By adding the DW_AT_LLVM_virtual_call_origin for indirect calls, a debugger can identify the intended target of a call. These are the specific actions taking by the SCE debugger:

  • The debugger can detect functions that have been folding by comparing whether the DW_AT_call_origin matches the call frame function. If it does not, the debugger can assume the "true" target and the actual target have been code folded and add a frame annotation to the call stack to indicate this. That, or there is a tail call from foo to bar, but the debugger can disambiguate these cases by looking at the DW_AT_call_origin referenced subroutine DIE which has a tombstone DW_AT_low_pc in the ICF case.

  • For virtual calls such as the given test case, the existence of the DW_AT_LLVM_virtual_call_origin attribute tells the debugger that this is an indirect jump, and the DW_AT_LLVM_virtual_call_origin attribute, pointing to the base class method DIE, will tell which method is being called.

  • The debugger can confirm from the method's DIE that it is a virtual function call by looking at the attributes (DW_AT_virtuality, and DW_AT_vtable_elem_location) and can look at the parent DIE to work out the type.

This is the added DW_AT_LLVM_virtual_call_origin to identify the target call CBase::foo.

0x40:   DW_TAG_call_site
          DW_AT_call_target_clobbered	(DW_OP_breg0 RAX+0)
          DW_AT_call_return_pc	(0x000000000000000a)
          -----------------------------------------------
          DW_AT_LLVM_virtual_call_origin	(0x20 "_ZN5CBase3fooEv")
          -----------------------------------------------

The extra call site information is available by default for all debuggers.

@CarlosAlbertoEnciso CarlosAlbertoEnciso self-assigned this Nov 12, 2025
@CarlosAlbertoEnciso CarlosAlbertoEnciso added clang Clang issues not falling into any other category lldb clang:codegen IR generation bugs: mangling, exceptions, etc. debuginfo labels Nov 12, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 12, 2025

@llvm/pr-subscribers-llvm-selectiondag
@llvm/pr-subscribers-llvm-binary-utilities
@llvm/pr-subscribers-backend-aarch64
@llvm/pr-subscribers-backend-risc-v
@llvm/pr-subscribers-backend-arm
@llvm/pr-subscribers-backend-x86
@llvm/pr-subscribers-llvm-ir
@llvm/pr-subscribers-clang-codegen

@llvm/pr-subscribers-debuginfo

Author: Carlos Alberto Enciso (CarlosAlbertoEnciso)

Changes

Given the test case (CBase is a structure or class):

int function(CBase *Input) {
  int Output = Input->foo();
  return Output;
}

and using '-emit-call-site-info' with llc, the following DWARF debug information is produced for the indirect call 'Input->foo()':

0x51: DW_TAG_call_site
        DW_AT_call_target    (DW_OP_reg0 RAX)
        DW_AT_call_return_pc (0x0e)

This patch generates an extra 'DW_AT_call_origin' to identify the target call 'CBase::foo'.

0x51: DW_TAG_call_site
        DW_AT_call_target    (DW_OP_reg0 RAX)
        DW_AT_call_return_pc (0x0e)
        -----------------------------------------------
        DW_AT_call_origin    (0x71 "_ZN5CBase3fooEb")
        -----------------------------------------------

0x61: DW_TAG_class_type
        DW_AT_name            ("CBase")
              ...
0x71:   DW_TAG_subprogram
           DW_AT_linkage_name ("_ZN5CBase3fooEb")
           DW_AT_name         ("foo")

The extra call site information is generated only for the SCE debugger: '-Xclang -debugger-tuning=sce'


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

14 Files Affected:

  • (modified) clang/lib/CodeGen/CGCall.cpp (+3)
  • (modified) clang/lib/CodeGen/CGDebugInfo.cpp (+112-2)
  • (modified) clang/lib/CodeGen/CGDebugInfo.h (+28)
  • (modified) clang/lib/CodeGen/CodeGenFunction.cpp (+2)
  • (added) clang/test/DebugInfo/CXX/callsite-base.cpp (+42)
  • (added) clang/test/DebugInfo/CXX/callsite-derived.cpp (+85)
  • (added) clang/test/DebugInfo/CXX/callsite-edges.cpp (+125)
  • (added) cross-project-tests/debuginfo-tests/clang_llvm_roundtrip/callsite-dwarf.cpp (+71)
  • (modified) llvm/include/llvm/CodeGen/MachineFunction.h (+6)
  • (modified) llvm/include/llvm/IR/FixedMetadataKinds.def (+1)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp (+26-1)
  • (modified) llvm/lib/CodeGen/MIRPrinter.cpp (+1-1)
  • (modified) llvm/lib/CodeGen/MachineFunction.cpp (+3)
  • (modified) llvm/lib/Target/X86/X86ISelLoweringCall.cpp (+9-2)
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index efacb3cc04c01..5b5ed52e1d554 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -5958,6 +5958,9 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
     }
   }
 
+  if (CGDebugInfo *DI = CGM.getModuleDebugInfo())
+    DI->addCallTarget(CI->getCalledFunction(), CalleeDecl, CI);
+
   // If this is within a function that has the guard(nocf) attribute and is an
   // indirect call, add the "guard_nocf" attribute to this call to indicate that
   // Control Flow Guard checks should not be added, even if the call is inlined.
diff --git a/clang/lib/CodeGen/CGDebugInfo.cpp b/clang/lib/CodeGen/CGDebugInfo.cpp
index bda7b7487f59b..44d2bf5527c7f 100644
--- a/clang/lib/CodeGen/CGDebugInfo.cpp
+++ b/clang/lib/CodeGen/CGDebugInfo.cpp
@@ -2430,6 +2430,9 @@ llvm::DISubprogram *CGDebugInfo::CreateCXXMemberFunction(
 
   SPCache[Method->getCanonicalDecl()].reset(SP);
 
+  // Add the method declaration as a call target.
+  addCallTarget(MethodLinkageName, SP, /*CI=*/nullptr);
+
   return SP;
 }
 
@@ -4955,6 +4958,99 @@ void CGDebugInfo::EmitFunctionDecl(GlobalDecl GD, SourceLocation Loc,
     Fn->setSubprogram(SP);
 }
 
+bool CGDebugInfo::generateCallSiteForPS() const {
+  // The added call target will be available only for SCE targets.
+  if (CGM.getCodeGenOpts().getDebuggerTuning() != llvm::DebuggerKind::SCE)
+    return false;
+
+  // Check general conditions for call site generation.
+  return (getCallSiteRelatedAttrs() != llvm::DINode::FlagZero);
+}
+
+// Set the 'call_target' metadata in the call instruction.
+void CGDebugInfo::addCallTargetMetadata(llvm::MDNode *MD, llvm::CallBase *CI) {
+  if (!MD || !CI)
+    return;
+  CI->setMetadata(llvm::LLVMContext::MD_call_target, MD);
+}
+
+// Finalize call_target generation.
+void CGDebugInfo::finalizeCallTarget() {
+  if (!generateCallSiteForPS())
+    return;
+
+  for (auto &E : CallTargetCache) {
+    for (const auto &WH : E.second.second) {
+      llvm::CallBase *CI = dyn_cast_or_null<llvm::CallBase>(WH);
+      addCallTargetMetadata(E.second.first, CI);
+    }
+  }
+}
+
+void CGDebugInfo::addCallTarget(StringRef Name, llvm::MDNode *MD,
+                                llvm::CallBase *CI) {
+  if (!generateCallSiteForPS())
+    return;
+
+  // Record only indirect calls.
+  if (CI && !CI->isIndirectCall())
+    return;
+
+  // Nothing to do.
+  if (Name.empty())
+    return;
+
+  auto It = CallTargetCache.find(Name);
+  if (It == CallTargetCache.end()) {
+    // First time we see 'Name'. Insert record for later finalize.
+    InstrList List;
+    if (CI)
+      List.push_back(CI);
+    CallTargetCache.try_emplace(Name, MD, std::move(List));
+  } else {
+    if (MD)
+      It->second.first.reset(MD);
+    if (CI) {
+      InstrList &List = It->second.second;
+      List.push_back(CI);
+    }
+  }
+}
+
+void CGDebugInfo::addCallTarget(llvm::Function *F, const FunctionDecl *FD,
+                                llvm::CallBase *CI) {
+  if (!generateCallSiteForPS())
+    return;
+
+  if (!F && !FD)
+    return;
+
+  // Ignore method types that never can be indirect calls.
+  if (!F && (isa<CXXConstructorDecl>(FD) || isa<CXXDestructorDecl>(FD) ||
+             FD->hasAttr<CUDAGlobalAttr>()))
+    return;
+
+  StringRef Name = (F && F->hasName()) ? F->getName() : CGM.getMangledName(FD);
+  addCallTarget(Name, /*MD=*/nullptr, CI);
+}
+
+void CGDebugInfo::removeCallTarget(StringRef Name) {
+  if (!generateCallSiteForPS())
+    return;
+
+  auto It = CallTargetCache.find(Name);
+  if (It != CallTargetCache.end())
+    CallTargetCache.erase(It);
+}
+
+void CGDebugInfo::removeCallTarget(llvm::Function *F) {
+  if (!generateCallSiteForPS())
+    return;
+
+  if (F && F->hasName())
+    removeCallTarget(F->getName());
+}
+
 void CGDebugInfo::EmitFuncDeclForCallSite(llvm::CallBase *CallOrInvoke,
                                           QualType CalleeType,
                                           GlobalDecl CalleeGlobalDecl) {
@@ -4978,9 +5074,15 @@ void CGDebugInfo::EmitFuncDeclForCallSite(llvm::CallBase *CallOrInvoke,
   // If there is no DISubprogram attached to the function being called,
   // create the one describing the function in order to have complete
   // call site debug info.
-  if (!CalleeDecl->isStatic() && !CalleeDecl->isInlined())
+  if (!CalleeDecl->isStatic() && !CalleeDecl->isInlined()) {
     EmitFunctionDecl(CalleeGlobalDecl, CalleeDecl->getLocation(), CalleeType,
                      Func);
+    if (Func->getSubprogram()) {
+      // For each call instruction emitted, add the call site target metadata.
+      llvm::DISubprogram *SP = Func->getSubprogram();
+      addCallTarget(SP->getLinkageName(), SP, /*CI=*/nullptr);
+    }
+  }
 }
 
 void CGDebugInfo::EmitInlineFunctionStart(CGBuilderTy &Builder, GlobalDecl GD) {
@@ -5082,8 +5184,13 @@ void CGDebugInfo::EmitFunctionEnd(CGBuilderTy &Builder, llvm::Function *Fn) {
   }
   FnBeginRegionCount.pop_back();
 
-  if (Fn && Fn->getSubprogram())
+  if (Fn && Fn->getSubprogram()) {
+    // For each call instruction emitted, add the call site target metadata.
+    llvm::DISubprogram *SP = Fn->getSubprogram();
+    addCallTarget(SP->getLinkageName(), SP, /*CI=*/nullptr);
+
     DBuilder.finalizeSubprogram(Fn->getSubprogram());
+  }
 }
 
 CGDebugInfo::BlockByRefType
@@ -6498,6 +6605,9 @@ void CGDebugInfo::finalize() {
     if (auto MD = TypeCache[RT])
       DBuilder.retainType(cast<llvm::DIType>(MD));
 
+  // Generate call_target information.
+  finalizeCallTarget();
+
   DBuilder.finalize();
 }
 
diff --git a/clang/lib/CodeGen/CGDebugInfo.h b/clang/lib/CodeGen/CGDebugInfo.h
index 2378bdd780b3b..fd71d958bc1f5 100644
--- a/clang/lib/CodeGen/CGDebugInfo.h
+++ b/clang/lib/CodeGen/CGDebugInfo.h
@@ -682,6 +682,15 @@ class CGDebugInfo {
   /// that it is supported and enabled.
   llvm::DINode::DIFlags getCallSiteRelatedAttrs() const;
 
+  /// Add call target information.
+  void addCallTarget(StringRef Name, llvm::MDNode *MD, llvm::CallBase *CI);
+  void addCallTarget(llvm::Function *F, const FunctionDecl *FD,
+                     llvm::CallBase *CI);
+
+  /// Remove a call target entry for the given name or function.
+  void removeCallTarget(StringRef Name);
+  void removeCallTarget(llvm::Function *F);
+
 private:
   /// Amend \p I's DebugLoc with \p Group (its source atom group) and \p
   /// Rank (lower nonzero rank is higher precedence). Does nothing if \p I
@@ -903,6 +912,25 @@ class CGDebugInfo {
   /// If one exists, returns the linkage name of the specified \
   /// (non-null) \c Method. Returns empty string otherwise.
   llvm::StringRef GetMethodLinkageName(const CXXMethodDecl *Method) const;
+
+  /// For each 'DISuprogram' we store a list of call instructions 'CallBase'
+  /// that indirectly call  such 'DISuprogram'. We use its linkage name to
+  /// update such list.
+  /// The 'CallTargetCache' is updated in the following scenarios:
+  /// - Both 'CallBase' and 'MDNode' are ready available.
+  /// - If only the 'CallBase' or 'MDNode' are are available, the partial
+  ///   information is added and later is completed when the missing item
+  ///   ('CallBase' or 'MDNode') is available.
+  using InstrList = llvm::SmallVector<llvm::WeakVH, 2>;
+  using CallTargetEntry = std::pair<llvm::TrackingMDNodeRef, InstrList>;
+  llvm::SmallDenseMap<StringRef, CallTargetEntry> CallTargetCache;
+
+  /// Generate call target information only for SIE debugger.
+  bool generateCallSiteForPS() const;
+
+  /// Add 'call_target' metadata to the 'call' instruction.
+  void addCallTargetMetadata(llvm::MDNode *MD, llvm::CallBase *CI);
+  void finalizeCallTarget();
 };
 
 /// A scoped helper to set the current debug location to the specified
diff --git a/clang/lib/CodeGen/CodeGenFunction.cpp b/clang/lib/CodeGen/CodeGenFunction.cpp
index 88628530cf66b..6d83b5fdd0e1b 100644
--- a/clang/lib/CodeGen/CodeGenFunction.cpp
+++ b/clang/lib/CodeGen/CodeGenFunction.cpp
@@ -1510,6 +1510,8 @@ void CodeGenFunction::GenerateCode(GlobalDecl GD, llvm::Function *Fn,
     // Clear non-distinct debug info that was possibly attached to the function
     // due to an earlier declaration without the nodebug attribute
     Fn->setSubprogram(nullptr);
+    if (CGDebugInfo *DI = getDebugInfo())
+      DI->removeCallTarget(Fn);
     // Disable debug info indefinitely for this function
     DebugInfo = nullptr;
   }
diff --git a/clang/test/DebugInfo/CXX/callsite-base.cpp b/clang/test/DebugInfo/CXX/callsite-base.cpp
new file mode 100644
index 0000000000000..ed7c455ced9d7
--- /dev/null
+++ b/clang/test/DebugInfo/CXX/callsite-base.cpp
@@ -0,0 +1,42 @@
+// Simple class with only virtual methods: inlined and not-inlined
+// We check for a generated 'call_target' for:
+// - 'one', 'two' and 'three'.
+
+class CBase {
+public:
+  virtual void one();
+  virtual void two();
+  virtual void three() {}
+};
+void CBase::one() {}
+
+void bar(CBase *Base) {
+  Base->one();
+  Base->two();
+  Base->three();
+
+  CBase B;
+  B.one();
+}
+
+// RUN: %clang_cc1 -debugger-tuning=sce -triple=x86_64-linux -disable-llvm-passes -emit-llvm -debug-info-kind=constructor -dwarf-version=5 -O1 %s -o - | FileCheck %s -check-prefix CHECK-BASE
+
+// CHECK-BASE: define {{.*}} @_Z3barP5CBase{{.*}} {
+// CHECK-BASE-DAG:   call void %1{{.*}} !dbg {{![0-9]+}}, !call_target [[BASE_ONE:![0-9]+]]
+// CHECK-BASE-DAG:   call void %3{{.*}} !dbg {{![0-9]+}}, !call_target [[BASE_TWO:![0-9]+]]
+// CHECK-BASE-DAG:   call void %5{{.*}} !dbg {{![0-9]+}}, !call_target [[BASE_THREE:![0-9]+]]
+// CHECK-BASE-DAG:   call void @_ZN5CBaseC2Ev{{.*}} !dbg {{![0-9]+}}
+// CHECK-BASE-DAG:   call void @_ZN5CBase3oneEv{{.*}} !dbg {{![0-9]+}}
+// CHECK-BASE: }
+
+// CHECK-BASE-DAG: [[BASE_ONE]] = {{.*}}!DISubprogram(name: "one", linkageName: "_ZN5CBase3oneEv"
+// CHECK-BASE-DAG: [[BASE_TWO]] = {{.*}}!DISubprogram(name: "two", linkageName: "_ZN5CBase3twoEv"
+// CHECK-BASE-DAG: [[BASE_THREE]] = {{.*}}!DISubprogram(name: "three", linkageName: "_ZN5CBase5threeEv"
+
+// RUN: %clang_cc1 -triple=x86_64-linux -disable-llvm-passes -emit-llvm -debug-info-kind=constructor -dwarf-version=5 -O1 %s -o - | FileCheck %s -check-prefix CHECK-BASE-NON
+
+// CHECK-BASE-NON: define {{.*}} @_Z3barP5CBase{{.*}} {
+// CHECK-BASE-NON-DAG:   call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-BASE-NON-DAG:   call void %3{{.*}} !dbg {{![0-9]+}}
+// CHECK-BASE-NON-DAG:   call void %5{{.*}} !dbg {{![0-9]+}}
+// CHECK-BASE-NON: }
diff --git a/clang/test/DebugInfo/CXX/callsite-derived.cpp b/clang/test/DebugInfo/CXX/callsite-derived.cpp
new file mode 100644
index 0000000000000..b1019c4b252a4
--- /dev/null
+++ b/clang/test/DebugInfo/CXX/callsite-derived.cpp
@@ -0,0 +1,85 @@
+// Simple base and derived class with virtual and static methods:
+// We check for a generated 'call_target' for:
+// - 'one', 'two' and 'three'.
+
+class CBase {
+public:
+  virtual void one(bool Flag) {}
+  virtual void two(int P1, char P2) {}
+  static void three();
+};
+
+void CBase::three() {
+}
+void bar(CBase *Base);
+
+void foo(CBase *Base) {
+  CBase::three();
+}
+
+class CDerived : public CBase {
+public:
+  void one(bool Flag) {}
+  void two(int P1, char P2) {}
+};
+void foo(CDerived *Derived);
+
+int main() {
+  CBase B;
+  bar(&B);
+
+  CDerived D;
+  foo(&D);
+
+  return 0;
+}
+
+void bar(CBase *Base) {
+  Base->two(77, 'a');
+}
+
+void foo(CDerived *Derived) {
+  Derived->one(true);
+}
+
+// RUN: %clang_cc1 -debugger-tuning=sce -triple=x86_64-linux -disable-llvm-passes -emit-llvm -debug-info-kind=constructor -dwarf-version=5 -O1 %s -o - | FileCheck %s -check-prefix CHECK-DERIVED
+
+// CHECK-DERIVED: define {{.*}} @_Z3fooP5CBase{{.*}} {
+// CHECK-DERIVED-DAG: call void @_ZN5CBase5threeEv{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED: define {{.*}} @main{{.*}} {
+// CHECK-DERIVED-DAG:  call void @_ZN5CBaseC1Ev{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED-DAG:  call void @_Z3barP5CBase{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED-DAG:  call void @_ZN8CDerivedC1Ev{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED-DAG:  call void @_Z3fooP8CDerived{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED: define {{.*}} @_ZN5CBaseC1Ev{{.*}} {
+// CHECK-DERIVED-DAG:  call void @_ZN5CBaseC2Ev{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED: define {{.*}} @_Z3barP5CBase{{.*}} {
+// CHECK-DERIVED-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}, !call_target [[BASE_TWO:![0-9]+]]
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED: define {{.*}} @_ZN8CDerivedC1Ev{{.*}} {
+// CHECK-DERIVED-DAG:  call void @_ZN8CDerivedC2Ev{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED: define {{.*}} @_Z3fooP8CDerived{{.*}} {
+// CHECK-DERIVED-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}, !call_target [[DERIVED_ONE:![0-9]+]]
+// CHECK-DERIVED: }
+
+// CHECK-DERIVED-DAG: [[BASE_TWO]] = {{.*}}!DISubprogram(name: "two", linkageName: "_ZN5CBase3twoEic"
+// CHECK-DERIVED-DAG: [[DERIVED_ONE]] = {{.*}}!DISubprogram(name: "one", linkageName: "_ZN8CDerived3oneEb"
+
+// RUN: %clang_cc1 -triple=x86_64-linux -disable-llvm-passes -emit-llvm -debug-info-kind=constructor -dwarf-version=5 -O1 %s -o - | FileCheck %s -check-prefix CHECK-DERIVED-NON
+
+// CHECK-DERIVED-NON: define {{.*}} @_Z3barP5CBase{{.*}} {
+// CHECK-DERIVED-NON-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED-NON: }
+
+// CHECK-DERIVED-NON: define {{.*}} @_Z3fooP8CDerived{{.*}} {
+// CHECK-DERIVED-NON-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-DERIVED-NON: }
diff --git a/clang/test/DebugInfo/CXX/callsite-edges.cpp b/clang/test/DebugInfo/CXX/callsite-edges.cpp
new file mode 100644
index 0000000000000..1d4ef29f4b357
--- /dev/null
+++ b/clang/test/DebugInfo/CXX/callsite-edges.cpp
@@ -0,0 +1,125 @@
+// Check edge cases:
+
+//---------------------------------------------------------------------
+// Method is declared but not defined in current CU - Fail.
+// No debug information entry is generated for 'one'.
+// Generate 'call_target' metadata only for 'two'.
+//---------------------------------------------------------------------
+class CEmpty {
+public:
+  virtual void one(bool Flag);
+  virtual void two(int P1, char P2);
+};
+
+void CEmpty::two(int P1, char P2) {
+}
+
+void edge_a(CEmpty *Empty) {
+  Empty->one(false);
+  Empty->two(77, 'a');
+}
+
+//---------------------------------------------------------------------
+// Pure virtual method but not defined in current CU - Pass.
+// Generate 'call_target' metadata for 'one' and 'two'.
+//---------------------------------------------------------------------
+class CBase {
+public:
+  virtual void one(bool Flag) = 0;
+  virtual void two(int P1, char P2);
+};
+
+void CBase::two(int P1, char P2) {
+}
+
+void edge_b(CBase *Base) {
+  Base->one(false);
+  Base->two(77, 'a');
+}
+
+//---------------------------------------------------------------------
+// Virtual method defined very deeply - Pass.
+// Generate 'call_target' metadata for 'd0', 'd1', 'd2' and 'd3'.
+//---------------------------------------------------------------------
+struct CDeep {
+  struct CD1 {
+    struct CD2 {
+      struct CD3 {
+        virtual void d3(int P3);
+      };
+
+      CD3 D3;
+      virtual void d2(int P2);
+    };
+
+    CD2 D2;
+    virtual void d1(int P1);
+  };
+
+  CD1 D1;
+  virtual void d0(int P);
+};
+
+void CDeep::d0(int P) {}
+void CDeep::CD1::d1(int P1) {}
+void CDeep::CD1::CD2::d2(int P2) {}
+void CDeep::CD1::CD2::CD3::d3(int P3) {}
+
+void edge_c(CDeep *Deep) {
+  Deep->d0(0);
+
+  CDeep::CD1 *D1 = &Deep->D1;
+  D1->d1(1);
+
+  CDeep::CD1::CD2 *D2 = &D1->D2;
+  D2->d2(2);
+
+  CDeep::CD1::CD2::CD3 *D3 = &D2->D3;
+  D3->d3(3);
+}
+
+// RUN: %clang -Xclang -debugger-tuning=sce --target=x86_64-linux -Xclang -disable-llvm-passes -fno-discard-value-names -emit-llvm -S -g -O1 %s -o - | FileCheck %s -check-prefix CHECK-EDGES
+
+// CHECK-EDGES: define {{.*}} @_Z6edge_aP6CEmpty{{.*}} {
+// CHECK-EDGES-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-DAG:  call void %3{{.*}} !dbg {{![0-9]+}}, !call_target [[CEMPTY_TWO:![0-9]+]]
+// CHECK-EDGES: }
+
+// CHECK-EDGES: define {{.*}} @_Z6edge_bP5CBase{{.*}} {
+// CHECK-EDGES-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}, !call_target [[CBASE_ONE:![0-9]+]]
+// CHECK-EDGES-DAG:  call void %3{{.*}} !dbg {{![0-9]+}}, !call_target [[CBASE_TWO:![0-9]+]]
+// CHECK-EDGES: }
+
+// CHECK-EDGES: define {{.*}} @_Z6edge_cP5CDeep{{.*}} {
+// CHECK-EDGES-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}, !call_target [[CDEEP_D0:![0-9]+]]
+// CHECK-EDGES-DAG:  call void %4{{.*}} !dbg {{![0-9]+}}, !call_target [[CDEEP_D1:![0-9]+]]
+// CHECK-EDGES-DAG:  call void %7{{.*}} !dbg {{![0-9]+}}, !call_target [[CDEEP_D2:![0-9]+]]
+// CHECK-EDGES-DAG:  call void %10{{.*}} !dbg {{![0-9]+}}, !call_target [[CDEEP_D3:![0-9]+]]
+// CHECK-EDGES: }
+
+// CHECK-EDGES-DAG:  [[CEMPTY_TWO]] = {{.*}}!DISubprogram(name: "two", linkageName: "_ZN6CEmpty3twoEic"
+// CHECK-EDGES-DAG:  [[CBASE_ONE]] = {{.*}}!DISubprogram(name: "one", linkageName: "_ZN5CBase3oneEb"
+// CHECK-EDGES-DAG:  [[CBASE_TWO]] = {{.*}}!DISubprogram(name: "two", linkageName: "_ZN5CBase3twoEic"
+
+// CHECK-EDGES-DAG:  [[CDEEP_D0]] = {{.*}}!DISubprogram(name: "d0", linkageName: "_ZN5CDeep2d0Ei"
+// CHECK-EDGES-DAG:  [[CDEEP_D1]] = {{.*}}!DISubprogram(name: "d1", linkageName: "_ZN5CDeep3CD12d1Ei"
+// CHECK-EDGES-DAG:  [[CDEEP_D2]] = {{.*}}!DISubprogram(name: "d2", linkageName: "_ZN5CDeep3CD13CD22d2Ei"
+// CHECK-EDGES-DAG:  [[CDEEP_D3]] = {{.*}}!DISubprogram(name: "d3", linkageName: "_ZN5CDeep3CD13CD23CD32d3Ei"
+
+// RUN: %clang --target=x86_64-linux -Xclang -disable-llvm-passes -fno-discard-value-names -emit-llvm -S -g -O1 %s -o - | FileCheck %s -check-prefix CHECK-EDGES-NON
+
+// CHECK-EDGES-NON: define {{.*}} @_Z6edge_aP6CEmpty{{.*}} {
+// CHECK-EDGES-NON-DAG:  call void %3{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON: }
+
+// CHECK-EDGES-NON: define {{.*}} @_Z6edge_bP5CBase{{.*}} {
+// CHECK-EDGES-NON-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON-DAG:  call void %3{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON: }
+
+// CHECK-EDGES-NON: define {{.*}} @_Z6edge_cP5CDeep{{.*}} {
+// CHECK-EDGES-NON-DAG:  call void %1{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON-DAG:  call void %4{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON-DAG:  call void %7{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON-DAG:  call void %10{{.*}} !dbg {{![0-9]+}}
+// CHECK-EDGES-NON: }
diff --git a/cross-project-tests/debuginfo-tests/clang_llvm_roundtrip/callsite-dwarf.cpp b/cross-project-tests/debuginfo-tests/clang_llvm_roundtrip/callsite-dwarf.cpp
new file mode 100644
index 0000000000000..7374a355da549
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/clang_llvm_roundtrip/callsite-dwarf.cpp
@@ -0,0 +1,71 @@
+// Simple base and derived class with virtual:
+// We check for a generated 'DW_AT_call_origin' for 'foo', that corresponds
+// to the 'call_target' metadata added to the indirect call instruction.
+
+class CBaseOne {
+  virtual void foo(int &);
+};
+
+struct CDerivedOne : CBaseOne {
+  void foo(int &);
+};
+
+void CDerivedOne::foo(int &) {
+}
+
+struct CBaseTwo {
+  CDerivedOne *DerivedOne;
+};
+
+struct CDerivedTwo : CBaseTwo {
+  void bar(int &);
+};
+
+void CDerivedTwo::bar(int &j) {
+  DerivedOne->foo(j);
+}
+
+// The IR generated looks like:
+//
+// define dso_local void @_ZN11CDerivedTwo3barERi(...) !dbg !40 {
+// entry:
+//   ..
+//   %vtable = load ptr, ptr %0, align 8
+//   %vfn = getelementptr inbounds ptr, ptr %vtable, i64 0
+//   %2 = load ptr, ptr %vfn, align 8
+//   call void %2(...), !dbg !65, !call_target !25
+//   ret void
+// }
+//
+// !25 = !DISubprogram(name: "foo", linkageName: "_ZN11CDerivedOne3fooERi", ...)
+// !40 = !DISubprogram(name: "bar", linkageName: "_ZN11CDerivedTwo3barERi", ...)
+// !65 = !DILocation(line: 25, column: 15, scope: !40)
+
+// RUN: %clang --target=x86_64-unknown-linux -c -g -O1    \
+// RUN:        -Xclang -debugger-tuning=sce %s -o -     | \
+// RUN: llvm-dwarfdump --debug-info - | FileCheck %s --check-prefix=CHECK
+
+// CHECK: DW_TAG_compile_unit
+// CHECK:   DW_TAG_structure_type
+// CHECK:     DW_AT_name	("CDerivedOne")
+// CHECK: [[FOO_DCL:0x[a-f0-9]+]]:    DW_TAG_subprogram
+// CHECK:       DW_AT_name	("foo")
+// CHECK:   DW_TAG_class_type
+// CHECK:     DW_AT_name	("CBaseOne")
+// CHECK: [[FOO_DEF:0x[a-f0-9]+]]:  DW_TAG_subprogram
+// CHECK:     DW_AT_call_all_calls	(true)
+// CHECK:     DW_AT_specification	([[FOO_DCL]] "foo")
+// CHECK:  ...
[truncated]

@github-actions
Copy link

github-actions bot commented Nov 12, 2025

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

@CarlosAlbertoEnciso
Copy link
Member Author

CarlosAlbertoEnciso commented Nov 12, 2025

@tromey For some reasons, your name does not appear in the reviewers list. I would appreciate if you can add yourself as reviewer. Thanks

@CarlosAlbertoEnciso CarlosAlbertoEnciso changed the title [clang][DebugInfo] Add call site target information in DWARF. [clang][DebugInfo] Add virtual call-site target information in DWARF. Nov 12, 2025
@jryans
Copy link
Member

jryans commented Nov 12, 2025

The extra call site information is generated only for the SCE debugger: '-Xclang -debugger-tuning=sce'

Do you have any data on how much this adds to debug info size for some known codebase (like Clang itself)? (See for example this comparison using bloaty on before and after Clang builds from a recent PR.)

I am curious if it would be possible to emit this data by default, but I assume people would want to see debug info size data before considering this.

@jryans jryans self-requested a review November 12, 2025 13:01
@CarlosAlbertoEnciso
Copy link
Member Author

@jryans This is the internal data that we collected:

The callsite changes are guarded by the option -Xclang -debugger-tuning=sce

[..]/bloaty ./callsite-dbg/bin/clang++ -- ./reference-dbg/bin/clang++

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +9.14Ki  [ = ]       0    .debug_info
  +0.0% +2.94Ki  [ = ]       0    .debug_abbrev
  +5.9%    +512  [ = ]       0    [Unmapped]
  +0.0%     +37  [ = ]       0    .debug_str_offsets
  +0.0%      +5  [ = ]       0    .debug_line
  +1.3%      +2  [ = ]       0    .comment
  -0.0%      -6  [ = ]       0    .debug_line_str
  -0.0%    -512  -0.0%    -512    .rodata
  -0.0% -1.73Ki  [ = ]       0    .debug_str
  +0.0% +10.4Ki  -0.0%    -512    TOTAL

[...] 1512681192 ./reference-dbg/bin/clang++
[...] 1512691832 ./callsite-dbg/bin/clang++

[..]/bloaty ./callsite-dbg/bin/clang -- ./reference-dbg/bin/clang

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +9.14Ki  [ = ]       0    .debug_info
  +0.0% +2.94Ki  [ = ]       0    .debug_abbrev
  +5.9%    +512  [ = ]       0    [Unmapped]
  +0.0%     +37  [ = ]       0    .debug_str_offsets
  +0.0%      +5  [ = ]       0    .debug_line
  +1.3%      +2  [ = ]       0    .comment
  -0.0%      -6  [ = ]       0    .debug_line_str
  -0.0%    -512  -0.0%    -512    .rodata
  -0.0% -1.73Ki  [ = ]       0    .debug_str
  +0.0% +10.4Ki  -0.0%    -512    TOTAL

[...] 1512681192 ./reference-dbg/bin/clang
[...] 1512691832 ./callsite-dbg/bin/clang

[..]/bloaty ./callsite-dbg/bin/llc -- ./reference-dbg/bin/llc

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +3.05Ki  [ = ]       0    .debug_info
  +0.0% +2.12Ki  [ = ]       0    .debug_abbrev
  +6.6%    +384  [ = ]       0    [Unmapped]
  +0.0%      +9  [ = ]       0    .debug_str_offsets
  +1.3%      +2  [ = ]       0    .comment
  -0.0%      -3  [ = ]       0    .debug_line_str
  -0.0%    -384  -0.0%    -384    .rodata
  -0.0% -1.63Ki  [ = ]       0    .debug_str
  +0.0% +3.55Ki  -0.0%    -384    TOTAL

[...] 986245752 ./reference-dbg/bin/llc
[...] 986249392 ./callsite-dbg/bin/llc
{code}

[..]/bloaty ./callsite-dbg/bin/opt -- ./reference-dbg/bin/opt
{code:none}
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +3.05Ki  [ = ]       0    .debug_info
  +0.0% +2.12Ki  [ = ]       0    .debug_abbrev
   +17%    +320  [ = ]       0    [Unmapped]
  +0.0%     +17  [ = ]       0    .debug_str_offsets
  +1.3%      +2  [ = ]       0    .comment
  -0.0%      -3  [ = ]       0    .debug_line_str
  -0.0%    -320  -0.0%    -320    .rodata
  -0.0% -1.63Ki  [ = ]       0    .debug_str
  +0.0% +3.56Ki  -0.0%    -320    TOTAL

[...] 985916024 ./reference-dbg/bin/opt
[...] 985919672 ./callsite-dbg/bin/opt

[..]/bloaty ./callsite-dbg/bin/llvm-as -- ./reference-dbg/bin/llvm-as

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +1.28Ki  [ = ]       0    .debug_info
  +0.0%    +284  [ = ]       0    .debug_abbrev
  +0.0%     +65  [ = ]       0    .debug_str
  +0.0%     +10  [ = ]       0    .debug_str_offsets
  +1.3%      +2  [ = ]       0    .comment
  -0.0%      -3  [ = ]       0    .debug_line_str
  +0.0% +1.62Ki  [ = ]       0    TOTAL

[...] 142183856 ./reference-dbg/bin/llvm-as
[...] 142185520 ./callsite-dbg/bin/llvm-as

@jryans
Copy link
Member

jryans commented Nov 12, 2025

@jryans This is the internal data that we collected:

The callsite changes are guarded by the option -Xclang -debugger-tuning=sce

Thanks for sharing that data. It looks e.g. clang++ shows total growth of only 10 KiB, which is quite small indeed compared to the total binary size.

Given this data, I would personally recommend we make this additional data available by default. Of course, we should also see what @dwblaikie and other debug-info-size-sensitive downstream users think as well.

@CarlosAlbertoEnciso
Copy link
Member Author

CarlosAlbertoEnciso commented Nov 13, 2025

@jryans @dwblaikie I updated the patch to make this additional data available by default.

Copy link
Member

@jryans jryans left a comment

Choose a reason for hiding this comment

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

Overall, looking good to me with a few small things to fix mentioned inline. Thanks for working on this!

Since this touches on a few areas I am not as confident in, I think would be for one more person to also take a look.

}
}

if (CGDebugInfo *DI = CGM.getModuleDebugInfo())
Copy link
Member

Choose a reason for hiding this comment

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

As of #166202, there's now also a block a bit further down (just before the end, near line 6283) in this EmitCall function that tests for debug info. Maybe move your addition into that block?

The block I am referring to also happens to call the slightly different getDebugInfo() function, which checks whether this specific function has debug info enabled, which seems more correct. (It looks like function-level control of debug info is only changed for lambdas at the moment, so perhaps not a large difference, but good to be consistent I think.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Moving the change into that block it make sense, as they are related.

llvm::StringRef GetMethodLinkageName(const CXXMethodDecl *Method) const;

/// For each 'DISuprogram' we store a list of call instructions 'CallBase'
/// that indirectly call such 'DISuprogram'. We use its linkage name to
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// that indirectly call such 'DISuprogram'. We use its linkage name to
/// that indirectly call such 'DISuprogram'. We use its linkage name to

@tromey
Copy link
Contributor

tromey commented Nov 13, 2025

@tromey For some reasons, your name does not appear in the reviewers list. I would appreciate if you can add yourself as reviewer. Thanks

I don't have any permissions in llvm and so I think I can't be added as a reviewer.

Anyway, I didn't want your request to go unanswered, but I don't actually know that much about the code that this patch touches. My main question was why it was conditional on a particular debugger but that's been addressed.

@CarlosAlbertoEnciso
Copy link
Member Author

The updated patch includes:

  • lazily producing the function declarations (suggested by @dwblaikie)
  • the DW_AT_LLVM_virtual_call_origin always pointing to the method declaration

Copy link
Collaborator

@dwblaikie dwblaikie left a comment

Choose a reason for hiding this comment

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

Haven't looked super close, but sounds roughly right to me

Comment on lines 4988 to 4992
// Ignore method types that never can be indirect calls.
if (isa<CXXConstructorDecl>(FD) || isa<CXXDestructorDecl>(FD) ||
FD->hasAttr<CUDAGlobalAttr>())
return;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do these not fall out from the !indirect check below?

Copy link
Member Author

Choose a reason for hiding this comment

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

You are correct. These checks are not required. Keeping just the isIndirectCall check.

Comment on lines 9 to 24
struct CBase {
virtual void f1();
virtual void f2();
virtual void f3() {}
};
void CBase::f1() {}

void bar(CBase *Base) {
Base->f1();
Base->f2();
Base->f3();

CBase B;
B.f1();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd include a few more comments - about the fact that this is testing three scenarios (out-of-line defined virtual member function, declared-but-not-defined virtual member function, and inline defined virtual member function) but also about whether it's intended that CBase is defined or declared - in this case because it instantiate's the ctor, (at CBase B) it should cause the CBase type to be defined. I guess you could turn on standalone debug as a maybe more direct way to say "the type should be defined in this test" (or an explicit out of line ctor would be OK too - with a comment explaining why it's load bearing - or even just a comment on the CBase B line to explain what it'll do)

Copy link
Member Author

Choose a reason for hiding this comment

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

Very good points.

  • turning on standalone debug.
  • check that CBase should be defined.
  • adding extra comments to clarify the checks.

Comment on lines 48 to 51
struct CDeep {
struct CD1 {
struct CD2 {
struct CD3 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this depth relevant? seems a bit arbitrary - I'd expect maybe 2 deep? or some explanation for what sort of case that makes this interesting.

Copy link
Member Author

Choose a reason for hiding this comment

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

The depth was relevant for the previous implementation (using a map).
Reduced the test case, as a nesting level of 2 is just fine.

@@ -0,0 +1,68 @@
// RUN: %clang --target=x86_64-unknown-linux -c -g -O1 %s -o - | \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if this feature has sufficient cross-project complexity to justify a cross-project-test for it... but shrug don't feel /super/ strongly about it.

Copy link
Member Author

Choose a reason for hiding this comment

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

@felipepiovezan Any thoughts on this? Thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I'd be more interested in seeing a test case later inside LLDB to make use of this feature.

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 will keep the test case, but added the following note:

// Note: We should add a test case inside LLDB that make use of the
//       virtuality call-site target information in DWARF.


/// 'call_target' metadata for the DISubprogram. It is the declaration
/// or definition of the target function and might be indirect.
MDNode *MD = nullptr;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could have a more specific name for this variable?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to CallTarget.

@CarlosAlbertoEnciso
Copy link
Member Author

@dwblaikie Thanks for your review and approval.
I will address your feedback and wait for @felipepiovezan review.

@CarlosAlbertoEnciso
Copy link
Member Author

Updated patch to address @dwblaikie feedback.

@github-actions
Copy link

github-actions bot commented Feb 16, 2026

🐧 Linux x64 Test Results

  • 197174 tests passed
  • 6489 tests skipped

✅ The build succeeded and all tests passed.

These changes are intended to address the concern about
the 'CallTargetCache' map.

The map is removed by:
- injecting a function declaration if one is not available
  at the point where the function is invoked.
- always pointing the 'call_target' to the function declaraion.
Address reviewer feedback:
- Rename 'MD' variable to a better descriptive name.
- Remove redundant code for indirect call check.
- Reduce nesting level in 'callsite-edges' test case.
- Improve comments and add check for CBase type definition.
@felipepiovezan
Copy link
Contributor

Sorry, I just landed here without much context. I guess my main feedback is that we're lacking documentation for the problem and the current solution. As someone who doesn't have context trying to understand the problem and the proposed solution, the only path is reading the entire PR (which is fine for reviewing, but probably not for posteriority). Trying to reconcile the PR description -- which is what will be preserved in the git history -- with the current implementation, it seems like they don't match, right? It would be great if we could either update the PR description, or document the design of this somewhere, especially because we're creating a new attribute.

@CarlosAlbertoEnciso CarlosAlbertoEnciso changed the title [clang][DebugInfo] Add virtual call-site target information in DWARF. [clang][DebugInfo] Add virtuality call-site target information in DWARF. Feb 18, 2026
Given the test case:

  struct CBase {
    virtual void foo();
  };

  void bar(CBase *Base) {
    Base->foo();
  }

and using '-emit-call-site-info' with llc, the following DWARF
is produced for the indirect call 'Base->foo()':

1$: DW_TAG_structure_type "CBase"
      ...
2$:   DW_TAG_subprogram "foo"
        ...

3$: DW_TAG_subprogram "bar"
      ...
4$:   DW_TAG_call_site
        ...

We add DW_AT_LLVM_virtual_call_origin to existing call-site
information, linking indirect calls to the function-declaration
they correspond to.

4$:   DW_TAG_call_site
        ...
        DW_AT_LLVM_virtual_call_origin (2$ "_ZN5CBase3fooEv")

The new attribute DW_AT_LLVM_virtual_call_origin helps to
address the ambiguity to any consumer due to the usage of
DW_AT_call_origin.

The functionality is available to all supported debuggers.
@CarlosAlbertoEnciso
Copy link
Member Author

@felipepiovezan Thanks for your initial feedback.
I have updated the PR description to match the implementation and expanded the commit message as well.

Copy link
Contributor

@felipepiovezan felipepiovezan left a comment

Choose a reason for hiding this comment

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

LGTM, left some comments about style that should be straightforward to address


// Always get method definition.
if (llvm::DISubprogram *MD = getFunctionDeclaration(FD))
// Attach the target metadata
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: are these two comments adding anything to the code? The first one seems more confusing than clarifying (it says "definition" but then the line below is "get declaration"). The second comment is just restating what the CI->setMetadata words already state.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed first comment to // Always get the method declaration.
Removed second comment.

if (!FD)
return;

// Record only indirect calls.
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: I don't think this comment is adding anything of value, it is a restatement word-by-word of CI->isIndirectCall()

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the comment.

llvm::DINode::DIFlags getCallSiteRelatedAttrs() const;

/// Add call target information.
void addCallTarget(const FunctionDecl *FD, llvm::CallBase *CI);
Copy link
Contributor

Choose a reason for hiding this comment

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

since this only adds virtual call targets, should it be called addCallTargetIfVirtual?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. Changed to addCallTargetIfVirtual.

llvm::StringRef GetMethodLinkageName(const CXXMethodDecl *Method) const;

/// Generate call target information.
bool generateVirtualCallSite() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this actually generating something? or is more of a shouldGenerateVirtualCallSite?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to shouldGenerateVirtualCallSite.

@@ -0,0 +1,68 @@
// RUN: %clang --target=x86_64-unknown-linux -c -g -O1 %s -o - | \
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I'd be more interested in seeing a test case later inside LLDB to make use of this feature.

}

void DwarfCompileUnit::addLinkageNamesToDeclarations(
const DwarfDebug *DD, const DISubprogram *CalleeSP, DIE *CalleeDIE) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're going to unconditionally dereference pointers, we should make them references instead to express our intent that this function does not know how to handle nullptrs

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. Changed to use references.

YmlCS.CallLocation = CallLocation;

auto [ArgRegPairs, CalleeTypeIds] = CallSiteInfo;
auto [ArgRegPairs, CalleeTypeIds, MD] = CallSiteInfo;
Copy link
Contributor

Choose a reason for hiding this comment

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

should we use _ instead of MD to denote that this is not used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to _.


// Call site info for function parameters tracking and call base type info.
MachineFunction::CallSiteInfo CSInfo;

Copy link
Contributor

Choose a reason for hiding this comment

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

this diff can be removed

Address additional reviewer feedback:
- Rename a couple of functions to add clarity.
- Remove some comments that don't add anything.
- Change function signature to use references.
@CarlosAlbertoEnciso
Copy link
Member Author

@dwblaikie, @felipepiovezan Thanks very much for your feedback and approval.

@CarlosAlbertoEnciso CarlosAlbertoEnciso merged commit 418ba6e into llvm:main Feb 19, 2026
10 checks passed
@CarlosAlbertoEnciso CarlosAlbertoEnciso deleted the callsite-indirect-calls branch February 19, 2026 16:46
@ilovepi
Copy link
Contributor

ilovepi commented Feb 19, 2026

I've confirmed locally that reverting this patch does prevent the crash reported in #182337.

ilovepi added a commit to ilovepi/llvm-project that referenced this pull request Feb 19, 2026
…n in DWARF. (llvm#167666)"

This reverts commit 418ba6e.

The commit caused an ICE due to hitting unreachable in
llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp:1307

Fixes llvm#182337
ilovepi added a commit that referenced this pull request Feb 19, 2026
#182343)

…n in DWARF. (#167666)"

This reverts commit 418ba6e.

The commit caused an ICE due to hitting unreachable in
llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp:1307

Fixes #182337
@CarlosAlbertoEnciso
Copy link
Member Author

CarlosAlbertoEnciso commented Feb 19, 2026

@ilovepi I am sorry for the crash. Thanks for taking the time to upload the reproducer (unreduced).
Looking at the function where the assertion is raised:

dwarf::Attribute
DwarfCompileUnit::getDwarf5OrGNUAttr(dwarf::Attribute Attr) const {
  if (!useGNUAnalogForDwarf5Feature())
    return Attr;
  switch (Attr) {
  case dwarf::DW_AT_call_all_calls:
    return dwarf::DW_AT_GNU_all_call_sites;
  case dwarf::DW_AT_call_target:
    return dwarf::DW_AT_GNU_call_site_target;
  case dwarf::DW_AT_call_target_clobbered:
    return dwarf::DW_AT_GNU_call_site_target_clobbered;
  case dwarf::DW_AT_call_origin:
    return dwarf::DW_AT_abstract_origin;
  case dwarf::DW_AT_call_return_pc:
    return dwarf::DW_AT_low_pc;
  case dwarf::DW_AT_call_value:
    return dwarf::DW_AT_GNU_call_site_value;
  case dwarf::DW_AT_call_tail_call:
    return dwarf::DW_AT_GNU_tail_call;
  default:
    llvm_unreachable("DWARF5 attribute with no GNU analog");
  }
}

The function getDwarf5OrGNUAttr is called by new code (addCallSiteTargetForIndirectCalls) in the patch:

  // Create call_target connections for indirect calls.
  auto addCallSiteTargetForIndirectCalls = [&](const MachineInstr *MI,
                                               DIE &CallSiteDIE) {
    ...
    CU.addDIEEntry(CallSiteDIE,
                   CU.getDwarf5OrGNUAttr(dwarf::DW_AT_LLVM_virtual_call_origin),
                   *CalleeDIE);
    ...
  };

The patch added a new LLVM attribute LLVM_virtual_call_origin which is not included in the switch statement, in getDwarf5OrGNUAttr causing the assertion.

A temporal fix for your specific build, could be

    ...
    CU.addDIEEntry(CallSiteDIE,
                   dwarf::DW_AT_LLVM_virtual_call_origin,   <-- Don't invoke getDwarf5OrGNUAttr(...)
                   *CalleeDIE);
    ...

Do you have a reduced test? Thanks.

@CarlosAlbertoEnciso CarlosAlbertoEnciso restored the callsite-indirect-calls branch February 19, 2026 21:19
@ilovepi
Copy link
Contributor

ilovepi commented Feb 19, 2026

@ilovepi I am sorry for the crash. Thanks for taking the time to upload the reproducer (unreduced). Looking at the function where the assertion is raised:

It's no problem. Anything non-trivial is highly likely to break something somewhere. :)

dwarf::Attribute
DwarfCompileUnit::getDwarf5OrGNUAttr(dwarf::Attribute Attr) const {
  if (!useGNUAnalogForDwarf5Feature())
    return Attr;
  switch (Attr) {
  case dwarf::DW_AT_call_all_calls:
    return dwarf::DW_AT_GNU_all_call_sites;
  case dwarf::DW_AT_call_target:
    return dwarf::DW_AT_GNU_call_site_target;
  case dwarf::DW_AT_call_target_clobbered:
    return dwarf::DW_AT_GNU_call_site_target_clobbered;
  case dwarf::DW_AT_call_origin:
    return dwarf::DW_AT_abstract_origin;
  case dwarf::DW_AT_call_return_pc:
    return dwarf::DW_AT_low_pc;
  case dwarf::DW_AT_call_value:
    return dwarf::DW_AT_GNU_call_site_value;
  case dwarf::DW_AT_call_tail_call:
    return dwarf::DW_AT_GNU_tail_call;
  default:
    llvm_unreachable("DWARF5 attribute with no GNU analog");
  }
}

The function getDwarf5OrGNUAttr is called by new code (addCallSiteTargetForIndirectCalls) in the patch:

  // Create call_target connections for indirect calls.
  auto addCallSiteTargetForIndirectCalls = [&](const MachineInstr *MI,
                                               DIE &CallSiteDIE) {
    ...
    CU.addDIEEntry(CallSiteDIE,
                   CU.getDwarf5OrGNUAttr(dwarf::DW_AT_LLVM_virtual_call_origin),
                   *CalleeDIE);
    ...
  };

The patch added a new LLVM attribute LLVM_virtual_call_origin which is not included in the switch statement, in getDwarf5OrGNUAttr causing the assertion.

A temporal fix for your specific build, could be

    ...
    CU.addDIEEntry(CallSiteDIE,
                   dwarf::DW_AT_LLVM_virtual_call_origin,   <-- Don't invoke getDwarf5OrGNUAttr(...)
                   *CalleeDIE);
    ...

Do you have a reduced test? Thanks.

creduce is still running. I'm probably calling it a day soon, but I'll try to check later and get you something smaller if its done.

you can maybe get something faster w/ llvm-reduce though if you dump the IR (assuming it isn't something that requied the clang side changes too). I'm just running the in tree reduction script from the reproducer, so that's creduce/cvise for now.

@ilovepi
Copy link
Contributor

ilovepi commented Feb 19, 2026

// RUN: clang -cc1 -triple aarch64-unknown-fuchsia -O2 -emit-obj -target-feature -aes -target-feature -crypto -target-feature -fp-armv8 -target-feature -neon -target-feature -sha2 -dwarf-version=4 -debugger-tuning=gdb -fno-rtti -debug-info-kind=constructor %s

struct BaseClass {
  virtual void Value();
} *test_virtual_dispatch_abstract_b;
void test_virtual_dispatch() { test_virtual_dispatch_abstract_b->Value(); }

@CarlosAlbertoEnciso
Copy link
Member Author

Thanks very much for your reduced test case.

Able to reproduce for any target when emitting DWARF-4 and at least -O1:

clang++ --target=x86_64-linux -c -g -O1 crash.cpp -o crash.o -gdwarf-4
or
clang++ --target=aarch64-unknown-fuchsia -c -g -O1 crash.cpp -o crash.o -gdwarf-4

@CarlosAlbertoEnciso
Copy link
Member Author

Open #182510 to address the crash.

Harrish92 pushed a commit to Harrish92/llvm-project that referenced this pull request Feb 22, 2026
llvm#182343)

…n in DWARF. (llvm#167666)"

This reverts commit 418ba6e.

The commit caused an ICE due to hitting unreachable in
llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp:1307

Fixes llvm#182337
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants