Skip to content

Conversation

yonghong-song
Copy link
Contributor

@yonghong-song yonghong-song commented Sep 7, 2025

Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The following is an example:
Source:

  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }

Compiled and dump dwarf with:

  clang -O2 -c -g test.c
  llvm-dwarfdump test.o

and related dwarf output

0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL

There are some restrictions in the current implementation:

  • Only C language is supported
  • BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  • Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.
  • Missed flag to turn off this feature and missed some dbg info (e.g.
    argument cannot be easily retrieved from dbg_value etc.).
  • Currently, some functions (e.g. foo.llvm.) do not change
    signatures but the current implementation still marks them
    as artificial. We might be able to avoid DW_TAG_inlined_subroutine
    for these routines.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:

  65341 original number of functions
  1085  new functions with this patch

For thin-lto case:

  65595 original number of functions
  2492  new functions with this patch

[1] #127855

@llvmbot
Copy link
Member

llvmbot commented Sep 7, 2025

@llvm/pr-subscribers-pgo
@llvm/pr-subscribers-llvm-transforms

@llvm/pr-subscribers-debuginfo

Author: None (yonghong-song)

Changes

Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The ultimate goal is to add new information to dwarf like below:

  DW_TAG_compile_unit
    ...
    // New functions with suffix
    DW_TAG_inlined_subroutine
      DW_AT_name      ("foo.1")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("b")
        DW_AT_type    (0x0000000000000091 "int")

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "long")

    ...
    // Functions with changed signatures
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar")
      DW_AT_type      (0x0000000000000091 "int")
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

      DW_TAG_formal_parameter
        DW_AT_name    ("c")
        DW_AT_type    (0x0000000000000095 "unsigned int")

    ...
    // Functions not obtained function changed signatures yet
    // The DW_CC_nocall presence indicates such cases.
    DW_TAG_inlined_subroutine
      DW_AT_name      ("bar" or "bar.1")
      DW_AT_calling_convention        (DW_CC_nocall)
      DW_AT_artificial (true)
      DW_AT_specificiation (original DW_TAG_subprogram)

The parent tag of above DW_TAG_inlined_subroutine is
DW_TAG_compile_unit. This is a new feature for dwarf
so it won't cause issues with existing dwarf related tools.
Total three patterns are introduced as the above.
. New functions with suffix, e.g., 'foo.1' or 'foo.llvm.<hash>'.
. Functions with changed signature due to ArgumentPromotion
or DeadArgumentElimination.
. Functions the current implementation cannot get proper
signature. For this case, DW_CC_nocall is set to indicate
signature is lost. More details in the below.

A special CompileUnit with file name "<artificial>" is created
to hold special DISubprograms for the above three kinds of functions.
During actual dwarf generation, these special DISubprograms
will turn to above to proper DW_TAG_inlined_subroutine tags.

The below are some discussions with not handled cases and
some other alternative things:
(1) Currently, there are three not handled signature changes.
. During to ArgumentPromotion, we may have
foo(..., struct foo *p, ...) =&gt; foo(..., int p.0.val, int p.4.val, ...)
. Struct argument which expands to two actual arguments,
foo(..., struct foo v, ...) =&gt; foo(..., v.coerce0, v.coerce1, ...)
. Struct argument changed to struct pointer,
foo(..., struct foo v, ...) =&gt; foo(..., struct foo *p, ...)
I think by utilizing dbg_value/dbg_declare and instructions, we
might be able to resolve the above and get proper signature.
But any suggestions are welcome.
(2) Currently, I am using a special CompileUnit "<artificial>" to hold
newly created DISubprograms. But there is an alternative.
For example, "llvm.dbg.cu" metadata is used to hold all CompileUnits.
We could introduce "llvm.dbg.sp.extra" to hold all new
DISubprograms instead of a new CompileUnit.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:

  65288 original number of functions
  910   new functions with this patch (including DW_CC_nocall case)
  7     new functions without signatures (with DW_CC_nocall)

For thin-lto case:

  65541 original number of functions
  2324  new functions with this patch (including DW_CC_nocall case)
  14    new functions without signatures (with DW_CC_nocall)

The following are some examples with thinlto with generated dwarf:

  ...
  0x0001707f:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("msr_build_context")
                  DW_AT_type      (0x00004163 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x0000440b "msr_build_context")

  0x0001708b:     DW_TAG_formal_parameter
                    DW_AT_name    ("msr_id")
                    DW_AT_type    (0x0000e55c "const u32 *")

  0x00017093:     NULL
  ...
  0x004225e5:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("__die_body.llvm.14794269134614576759")
                  DW_AT_type      (0x00418a14 "int")
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x00422348 "__die_body")

  0x004225f1:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x004181f3 "const char *")

  0x004225f9:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x00419118 "pt_regs *")

  0x00422601:     DW_TAG_formal_parameter
                    DW_AT_name    ("")
                    DW_AT_type    (0x0041af2f "long")

  0x00422609:     NULL
  ...
  0x013f5dac:   DW_TAG_inlined_subroutine
                  DW_AT_name      ("devkmsg_emit")
                  DW_AT_calling_convention        (DW_CC_nocall)
                  DW_AT_artificial        (true)
                  DW_AT_specification     (0x013ef75b "devkmsg_emit")

[1] #127855


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

14 Files Affected:

  • (added) llvm/include/llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h (+33)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp (+72)
  • (modified) llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h (+2)
  • (modified) llvm/lib/Passes/PassBuilder.cpp (+1)
  • (modified) llvm/lib/Passes/PassBuilderPipelines.cpp (+7-3)
  • (modified) llvm/lib/Passes/PassRegistry.def (+1)
  • (modified) llvm/lib/Transforms/IPO/ArgumentPromotion.cpp (+9)
  • (modified) llvm/lib/Transforms/Utils/CMakeLists.txt (+1)
  • (added) llvm/lib/Transforms/Utils/EmitChangedFuncDebugInfo.cpp (+337)
  • (modified) llvm/test/Other/new-pm-defaults.ll (+2)
  • (modified) llvm/test/Other/new-pm-thinlto-postlink-defaults.ll (+1)
  • (modified) llvm/test/Other/new-pm-thinlto-postlink-pgo-defaults.ll (+1)
  • (modified) llvm/test/Other/new-pm-thinlto-postlink-samplepgo-defaults.ll (+1)
  • (modified) llvm/test/Transforms/ArgumentPromotion/dbg.ll (+5-1)
diff --git a/llvm/include/llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h b/llvm/include/llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h
new file mode 100644
index 0000000000000..8d569cd95d7f7
--- /dev/null
+++ b/llvm/include/llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h
@@ -0,0 +1,33 @@
+//===- EmitChangedFuncDebugInfo.h - Emit Additional Debug Info -*- C++ --*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+/// \file
+/// Emit debug info for changed or new funcs.
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_TRANSFORMS_UTILS_EMITCHANGEDFUNCDEBUGINFO_H
+#define LLVM_TRANSFORMS_UTILS_EMITCHANGEDFUNCDEBUGINFO_H
+
+#include "llvm/IR/PassManager.h"
+
+namespace llvm {
+
+class Module;
+
+// Pass that emits late dwarf.
+class EmitChangedFuncDebugInfoPass
+    : public PassInfoMixin<EmitChangedFuncDebugInfoPass> {
+public:
+  EmitChangedFuncDebugInfoPass() = default;
+
+  PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+};
+
+} // end namespace llvm
+
+#endif // LLVM_TRANSFORMS_UTILS_EMITCHANGEDFUNCDEBUGINFO_H
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
index c27f100775625..3245d486feb77 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp
@@ -1266,11 +1266,83 @@ void DwarfDebug::finishSubprogramDefinitions() {
   }
 }
 
+void DwarfDebug::addChangedSubprograms() {
+  // Generate additional dwarf for functions with signature changed.
+  NamedMDNode *NMD = MMI->getModule()->getNamedMetadata("llvm.dbg.cu");
+  DICompileUnit *ExtraCU = nullptr;
+  for (MDNode *N : NMD->operands()) {
+    auto *CU = cast<DICompileUnit>(N);
+    if (CU->getFile()->getFilename() == "<artificial>") {
+      ExtraCU = CU;
+      break;
+    }
+  }
+  if (!ExtraCU)
+    return;
+
+  llvm::DebugInfoFinder DIF;
+  DIF.processModule(*MMI->getModule());
+  for (auto *ExtraSP : DIF.subprograms()) {
+    if (ExtraSP->getUnit() != ExtraCU)
+      continue;
+
+    DISubprogram *SP = cast<DISubprogram>(ExtraSP->getScope());
+    DwarfCompileUnit &Cu = getOrCreateDwarfCompileUnit(SP->getUnit());
+    DIE *ScopeDIE =
+        DIE::get(DIEValueAllocator, dwarf::DW_TAG_inlined_subroutine);
+    Cu.getUnitDie().addChild(ScopeDIE);
+
+    Cu.addString(*ScopeDIE, dwarf::DW_AT_name, ExtraSP->getName());
+
+    DITypeRefArray Args = ExtraSP->getType()->getTypeArray();
+
+    if (Args[0])
+        Cu.addType(*ScopeDIE, Args[0]);
+
+    if (ExtraSP->getType()->getCC() == llvm::dwarf::DW_CC_nocall) {
+      Cu.addUInt(*ScopeDIE, dwarf::DW_AT_calling_convention,
+                 dwarf::DW_FORM_data1, llvm::dwarf::DW_CC_nocall);
+    }
+
+    Cu.addFlag(*ScopeDIE, dwarf::DW_AT_artificial);
+
+    // dereference the DIE* for DIEEntry
+    DIE *OriginDIE = Cu.getOrCreateSubprogramDIE(SP);
+    Cu.addDIEEntry(*ScopeDIE, dwarf::DW_AT_specification, DIEEntry(*OriginDIE));
+
+    SmallVector<const DILocalVariable *> ArgVars(Args.size());
+    for (const DINode *DN : ExtraSP->getRetainedNodes()) {
+      if (const auto *DV = dyn_cast<DILocalVariable>(DN)) {
+        uint32_t Arg = DV->getArg();
+        if (Arg)
+          ArgVars[Arg - 1] = DV;
+      }
+    }
+
+    for (unsigned i = 1, N = Args.size(); i < N; ++i) {
+      const DIType *Ty = Args[i];
+      if (!Ty) {
+        assert(i == N-1 && "Unspecified parameter must be the last argument");
+        Cu.createAndAddDIE(dwarf::DW_TAG_unspecified_parameters, *ScopeDIE);
+      } else {
+        DIE &Arg =
+            Cu.createAndAddDIE(dwarf::DW_TAG_formal_parameter, *ScopeDIE);
+        const DILocalVariable *DV = ArgVars[i - 1];
+        if (DV)
+          Cu.addString(Arg, dwarf::DW_AT_name, DV->getName());
+        Cu.addType(Arg, Ty);
+      }
+    }
+  }
+}
+
 void DwarfDebug::finalizeModuleInfo() {
   const TargetLoweringObjectFile &TLOF = Asm->getObjFileLowering();
 
   finishSubprogramDefinitions();
 
+  addChangedSubprograms();
+
   finishEntityDefinitions();
 
   bool HasEmittedSplitCU = false;
diff --git a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h
index 89813dcf0fdab..417ffb19633c3 100644
--- a/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h
+++ b/llvm/lib/CodeGen/AsmPrinter/DwarfDebug.h
@@ -565,6 +565,8 @@ class DwarfDebug : public DebugHandlerBase {
 
   void finishSubprogramDefinitions();
 
+  void addChangedSubprograms();
+
   /// Finish off debug information after all functions have been
   /// processed.
   void finalizeModuleInfo();
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 587f0ece0859b..fa937a9a317be 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -344,6 +344,7 @@
 #include "llvm/Transforms/Utils/CanonicalizeAliases.h"
 #include "llvm/Transforms/Utils/CanonicalizeFreezeInLoops.h"
 #include "llvm/Transforms/Utils/CountVisits.h"
+#include "llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h"
 #include "llvm/Transforms/Utils/DXILUpgrade.h"
 #include "llvm/Transforms/Utils/Debugify.h"
 #include "llvm/Transforms/Utils/DeclareRuntimeLibcalls.h"
diff --git a/llvm/lib/Passes/PassBuilderPipelines.cpp b/llvm/lib/Passes/PassBuilderPipelines.cpp
index 98821bb1408a7..123041ea8cad8 100644
--- a/llvm/lib/Passes/PassBuilderPipelines.cpp
+++ b/llvm/lib/Passes/PassBuilderPipelines.cpp
@@ -133,6 +133,7 @@
 #include "llvm/Transforms/Utils/AssumeBundleBuilder.h"
 #include "llvm/Transforms/Utils/CanonicalizeAliases.h"
 #include "llvm/Transforms/Utils/CountVisits.h"
+#include "llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h"
 #include "llvm/Transforms/Utils/EntryExitInstrumenter.h"
 #include "llvm/Transforms/Utils/ExtraPassManager.h"
 #include "llvm/Transforms/Utils/InjectTLIMappings.h"
@@ -1625,9 +1626,12 @@ PassBuilder::buildModuleOptimizationPipeline(OptimizationLevel Level,
   if (PTO.CallGraphProfile && !LTOPreLink)
     MPM.addPass(CGProfilePass(isLTOPostLink(LTOPhase)));
 
-  // RelLookupTableConverterPass runs later in LTO post-link pipeline.
-  if (!LTOPreLink)
+  // RelLookupTableConverterPass and EmitChangedFuncDebugInfoPass run later in
+  // LTO post-link pipeline.
+  if (!LTOPreLink) {
     MPM.addPass(RelLookupTableConverterPass());
+    MPM.addPass(EmitChangedFuncDebugInfoPass());
+  }
 
   return MPM;
 }
@@ -2355,4 +2359,4 @@ AAManager PassBuilder::buildDefaultAAPipeline() {
 bool PassBuilder::isInstrumentedPGOUse() const {
   return (PGOOpt && PGOOpt->Action == PGOOptions::IRUse) ||
          !UseCtxProfile.empty();
-}
\ No newline at end of file
+}
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 299aaa801439b..78ee4ca6f96a1 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -73,6 +73,7 @@ MODULE_PASS("debugify", NewPMDebugifyPass())
 MODULE_PASS("declare-runtime-libcalls", DeclareRuntimeLibcallsPass())
 MODULE_PASS("dfsan", DataFlowSanitizerPass())
 MODULE_PASS("dot-callgraph", CallGraphDOTPrinterPass())
+MODULE_PASS("dwarf-emit-late", EmitChangedFuncDebugInfoPass())
 MODULE_PASS("dxil-upgrade", DXILUpgradePass())
 MODULE_PASS("elim-avail-extern", EliminateAvailableExternallyPass())
 MODULE_PASS("extract-blocks", BlockExtractorPass({}, false))
diff --git a/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp b/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
index 262c902d40d2d..609e4f8e4d23a 100644
--- a/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
+++ b/llvm/lib/Transforms/IPO/ArgumentPromotion.cpp
@@ -50,6 +50,7 @@
 #include "llvm/IR/BasicBlock.h"
 #include "llvm/IR/CFG.h"
 #include "llvm/IR/Constants.h"
+#include "llvm/IR/DIBuilder.h"
 #include "llvm/IR/DataLayout.h"
 #include "llvm/IR/DerivedTypes.h"
 #include "llvm/IR/Dominators.h"
@@ -432,6 +433,14 @@ doPromotion(Function *F, FunctionAnalysisManager &FAM,
     PromoteMemToReg(Allocas, DT, &AC);
   }
 
+  // DW_CC_nocall to DISubroutineType to inform debugger that it may not be safe
+  // to call this function.
+  DISubprogram *SP = NF->getSubprogram();
+  if (SP) {
+    auto Temp = SP->getType()->cloneWithCC(llvm::dwarf::DW_CC_nocall);
+    SP->replaceType(MDNode::replaceWithPermanent(std::move(Temp)));
+  }
+
   return NF;
 }
 
diff --git a/llvm/lib/Transforms/Utils/CMakeLists.txt b/llvm/lib/Transforms/Utils/CMakeLists.txt
index e411d68570096..0b36693ce7975 100644
--- a/llvm/lib/Transforms/Utils/CMakeLists.txt
+++ b/llvm/lib/Transforms/Utils/CMakeLists.txt
@@ -22,6 +22,7 @@ add_llvm_component_library(LLVMTransformUtils
   Debugify.cpp
   DeclareRuntimeLibcalls.cpp
   DemoteRegToStack.cpp
+  EmitChangedFuncDebugInfo.cpp
   DXILUpgrade.cpp
   EntryExitInstrumenter.cpp
   EscapeEnumerator.cpp
diff --git a/llvm/lib/Transforms/Utils/EmitChangedFuncDebugInfo.cpp b/llvm/lib/Transforms/Utils/EmitChangedFuncDebugInfo.cpp
new file mode 100644
index 0000000000000..82acae3f0efeb
--- /dev/null
+++ b/llvm/lib/Transforms/Utils/EmitChangedFuncDebugInfo.cpp
@@ -0,0 +1,337 @@
+//==- EmitChangedFuncDebugInfoPass - Emit Additional Debug Info -*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements emitting debug info for functions with changed
+// signatures or new functions.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Transforms/Utils/EmitChangedFuncDebugInfo.h"
+#include "llvm/IR/DIBuilder.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/IntrinsicInst.h"
+#include "llvm/IR/Module.h"
+
+using namespace llvm;
+
+static bool getArg(BasicBlock &FirstBB, unsigned Idx, DIBuilder &DIB,
+                   DIFile *NewFile, Function *F, DISubprogram *OldSP,
+                   SmallVector<Metadata *, 5> &TypeList,
+                   SmallVector<Metadata *, 5> &ArgList) {
+  for (Instruction &I : FirstBB) {
+    for (const DbgRecord &DR : I.getDbgRecordRange()) {
+      auto *DVR = dyn_cast<DbgVariableRecord>(&DR);
+      if (!DVR)
+        continue;
+      // All of DbgVariableRecord::LocationType::{Value,Assign,Declare}
+      // are covered.
+      Metadata *Loc = DVR->getRawLocation();
+      auto *ValueMDN = dyn_cast<ValueAsMetadata>(Loc);
+      if (!ValueMDN)
+        continue;
+
+      // A poison value may correspond to a unused argument.
+      if (isa<PoisonValue>(ValueMDN->getValue())) {
+        Type *Ty = ValueMDN->getType();
+        auto *Var = cast<DILocalVariable>(DVR->getRawVariable());
+        if (!Var || Var->getArg() != (Idx + 1))
+          continue;
+
+        // Check for cases like below due to ArgumentPromotion
+        //   define internal ... i32 @add42_byref(i32 %p.0.val) ... {
+        //     #dbg_value(ptr poison, !17, !DIExpression(), !18)
+        //     ...
+        //   }
+        // TODO: one pointer expands to more than one argument is not
+        // supported yet. For example,
+        //   define internal ... i32 @add42_byref(i32 %p.0.val, i32 %p.4.val)
+        //   ...
+        if (Ty->isPointerTy() && F->getArg(Idx)->getType()->isIntegerTy()) {
+          // For such cases, a new argument is created.
+          auto *IntTy = cast<IntegerType>(F->getArg(Idx)->getType());
+          unsigned IntBitWidth = IntTy->getBitWidth();
+
+          DIType *IntDIType =
+              DIB.createBasicType("int" + std::to_string(IntBitWidth),
+                                  IntBitWidth, dwarf::DW_ATE_signed);
+          Var = DIB.createParameterVariable(OldSP, F->getArg(Idx)->getName(),
+                                            Idx + 1, NewFile, OldSP->getLine(),
+                                            IntDIType);
+        }
+
+        TypeList.push_back(Var->getType());
+        ArgList.push_back(Var);
+        return true;
+      }
+
+      // Handle the following pattern:
+      //   ... @vgacon_do_font_op(..., i32 noundef, i1 noundef zeroext %ch512)
+      //   ... {
+      //     ...
+      //       #dbg_value(i32 %set, !8568, !DIExpression(), !8589)
+      //     %storedv = zext i1 %ch512 to i8
+      //       #dbg_value(i8 %storedv, !8569, !DIExpression(), !8589)
+      //     ...
+      //   }
+      if (ValueMDN->getValue() != F->getArg(Idx)) {
+        Instruction *PrevI = I.getPrevNode();
+        if (!PrevI)
+          continue;
+        if (ValueMDN->getValue() != PrevI)
+          continue;
+        auto *ZExt = dyn_cast<ZExtInst>(PrevI);
+        if (!ZExt)
+          continue;
+        if (ZExt->getOperand(0) != F->getArg(Idx))
+          continue;
+      }
+
+      auto *Var = cast<DILocalVariable>(DVR->getRawVariable());
+
+      // Even we get dbg_*(...) for arguments, we still need to ensure
+      // compatible types between IR func argument types and debugInfo argument
+      // types.
+      Type *Ty = ValueMDN->getType();
+      DIType *DITy = Var->getType();
+      while (auto *DTy = dyn_cast<DIDerivedType>(DITy)) {
+        if (DTy->getTag() == dwarf::DW_TAG_pointer_type) {
+          DITy = DTy;
+          break;
+        }
+        DITy = DTy->getBaseType();
+      }
+
+      if (Ty->isIntegerTy()) {
+        if (auto *DTy = dyn_cast<DICompositeType>(DITy)) {
+          if (!Ty->isIntegerTy(DTy->getSizeInBits())) {
+            // TODO: A struct param breaks into two actual arguments like
+            //    static int count(struct user_arg_ptr argv, int max)
+            // and the actual func signature:
+            //    i32 @count(i8 range(i8 0, 2) %argv.coerce0, ptr %argv.coerce1)
+            //    {
+            //      #dbg_value(i8 %argv.coerce0, !14759,
+            //      !DIExpression(DW_OP_LLVM_fragment, 0, 8), !14768)
+            //      #dbg_value(ptr %argv.coerce1, !14759,
+            //      !DIExpression(DW_OP_LLVM_fragment, 64, 64), !14768)
+            //      ...
+            //    }
+            return false;
+          }
+        }
+      } else if (Ty->isPointerTy()) {
+        // TODO: A struct turned into a pointer to struct.
+        //   @rhashtable_lookup_fast(ptr noundef %key,
+        //      ptr noundef readonly byval(%struct.rhashtable_params)
+        //        align 8 captures(none) %params) {
+        //      ...
+        //      %MyAlloca = alloca [160 x i8], align 32
+        //      %0 = ptrtoint ptr %MyAlloca to i64
+        //      %1 = add i64 %0, 32
+        //      %2 = inttoptr i64 %1 to ptr
+        //      ...
+        //      call void @llvm.memcpy.p0.p0.i64(ptr align 8 %2, ptr align 8
+        //      %params, i64 40, i1 false)
+        //        #dbg_value(ptr @offdevs, !15308, !DIExpression(), !15312)
+        //        #dbg_value(ptr %key, !15309, !DIExpression(), !15312)
+        //        #dbg_declare(ptr %MyAlloca, !15310,
+        //        !DIExpression(DW_OP_plus_uconst, 32), !15313)
+        //      tail call void @__rcu_read_lock() #14, !dbg !15314
+        //   }
+        if (dyn_cast<DICompositeType>(DITy))
+          return false;
+
+        auto *DTy = dyn_cast<DIDerivedType>(DITy);
+        if (!DTy)
+          continue;
+        if (DTy->getTag() != dwarf::DW_TAG_pointer_type)
+          continue;
+      }
+
+      TypeList.push_back(Var->getType());
+      if (Var->getArg() != (Idx + 1) ||
+          Var->getName() != F->getArg(Idx)->getName()) {
+        Var = DIB.createParameterVariable(OldSP, F->getArg(Idx)->getName(),
+                                          Idx + 1, OldSP->getUnit()->getFile(),
+                                          OldSP->getLine(), Var->getType());
+      }
+      ArgList.push_back(Var);
+      return true;
+    }
+  }
+
+  return false;
+}
+
+static bool getTypeArgList(DIBuilder &DIB, DIFile *NewFile, Function *F,
+                           FunctionType *FTy, DISubprogram *OldSP,
+                           SmallVector<Metadata *, 5> &TypeList,
+                           SmallVector<Metadata *, 5> &ArgList) {
+  Type *RetTy = FTy->getReturnType();
+  if (RetTy->isVoidTy()) {
+    // Void return type may be due to optimization.
+    TypeList.push_back(nullptr);
+  } else {
+    // Optimization does not change return type from one
+    // non-void type to another non-void type.
+    DITypeRefArray TyArray = OldSP->getType()->getTypeArray();
+    TypeList.push_back(TyArray[0]);
+  }
+
+  unsigned NumArgs = FTy->getNumParams();
+  BasicBlock &FirstBB = F->getEntryBlock();
+  for (unsigned i = 0; i < NumArgs; ++i) {
+    if (!getArg(FirstBB, i, DIB, NewFile, F, OldSP, TypeList, ArgList))
+      return false;
+  }
+
+  return true;
+}
+
+static void generateDebugInfo(Module &M, Function *F) {
+  // For this CU, we want generate the following three dwarf units:
+  // DW_TAG_compile_unit
+  //   ...
+  //   // New functions with suffix
+  //   DW_TAG_inlined_subroutine
+  //     DW_AT_name      ("foo.1")
+  //     DW_AT_type      (0x0000000000000091 "int")
+  //     DW_AT_artificial (true)
+  //     DW_AT_specificiation (original DW_TAG_subprogram)
+  //
+  //     DW_TAG_formal_parameter
+  //       DW_AT_name    ("b")
+  //       DW_AT_type    (0x0000000000000091 "int")
+  //
+  //     DW_TAG_formal_parameter
+  //       DW_AT_name    ("c")
+  //       DW_AT_type    (0x0000000000000095 "long")
+  //   ...
+  //   // Functions with changed signatures
+  //   DW_TAG_inlined_subroutine
+  //     DW_AT_name      ("bar")
+  //     DW_AT_type      (0x0000000000000091 "int")
+  //     DW_AT_artificial (true)
+  //     DW_AT_specificiation (original DW_TAG_subprogram)
+  //
+  //     DW_TAG_formal_parameter
+  //       DW_AT_name    ("c")
+  //       DW_AT_type    (0x0000000000000095 "unsigned int")
+  //   ...
+  //   // Functions not obtained function changed signatures yet
+  //   // The DW_CC_nocall presence indicates such cases.
+  //   DW_TAG_inlined_subroutine
+  //     DW_AT_name      ("bar" or "bar.1")
+  //     DW_AT_calling_convention        (DW_CC_nocall)
+  //     DW_AT_artificial (true)
+  //     DW_AT_specificiation (original DW_TAG_subprogram)
+  //   ...
+
+  // A new ComputeUnit is created with file name "<artificial>"
+  // to host newly-created DISubprogram's.
+  DICompileUnit *NewCU = nullptr;
+  NamedMDNode *CUs = M.getNamedMetadata("llvm.dbg.cu");
+  // Check whether the expected CU already there or not.
+  for (MDNode *Node : CUs->operands()) {
+    auto *CU = cast<DICompileUnit>(Node);
+    if (CU->getFile()->getFilename() == "<artificial>") {
+      NewCU = CU;
+      break;
+    }
+  }
+
+  DISubprogram *OldSP = F->getSubprogram();
+  DIBuilder DIB(M, /*AllowUnresolved=*/false, NewCU);
+  DIFile *NewFile;
+
+  if (NewCU) {
+    NewFile = NewCU->getFile();
+  } else {
+    DICompileUnit *OldCU = OldSP->getUnit();
+    DIFile *OldFile = OldCU->getFile();
+    NewFile = DIB.createFile("<artificial>", OldFile->getDirectory());
+    NewCU = DIB.createCompileUnit(
+        OldCU->getSourceLanguage(), NewFile, OldCU->getProducer(),
+        OldCU->isOptimized(), OldCU->getFlags(), OldCU->getRuntimeVersion());
+  }
+
+  SmallVector<Metadata *, 5> TypeList;
+  SmallVector<Metadata *, 5> ArgList;
+
+  FunctionType *FTy = F->getFunctionType();
+  bool Success = getTypeArgList(DIB, NewFile, F, FTy, OldSP, TypeList, ArgList);
+  if (!Success) {
+    TypeList.clear();
+    TypeList.push_back(nullptr);
+    ArgList.clear();
+  }
+
+  DITypeRefArray DITypeArray = DIB.getOrCreateTypeArray(TypeList);
+  auto *SubroutineType = DIB.createSubroutineType(DITypeArray);
+  DINodeArray ArgArray = DIB.getOrCreateArray(ArgList);
+
+  Function *DummyF =
+      Function::Create(FTy, GlobalValue::AvailableExternallyLinkage,
+                       F->getName() + ".newsig", &M);
+
+  DISubprogram *NewSP =
+      DIB.createFunction(OldSP,                   // Scope
+                         F->getName(),            // Name
+                         OldSP->getLinkageName(), // Linkage name
+                         NewFile,                 // File
+                         OldSP->getLine(),        // Line
+                         SubroutineType,          // DISubroutineType
+                         OldSP->getScopeLine(),   // ScopeLine
+      ...
[truncated]

Copy link

github-actions bot commented Sep 7, 2025

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

@yonghong-song
Copy link
Contributor Author

cc @jemarch

@yonghong-song yonghong-song force-pushed the signature-change branch 3 times, most recently from a864cf6 to aea80d2 Compare September 7, 2025 18:35
@yonghong-song yonghong-song force-pushed the signature-change branch 14 times, most recently from c9f140c to c995ada Compare September 8, 2025 01:41
@@ -1266,11 +1266,83 @@ void DwarfDebug::finishSubprogramDefinitions() {
}
}

void DwarfDebug::addChangedSubprograms() {
// Generate additional dwarf for functions with signature changed.
NamedMDNode *NMD = MMI->getModule()->getNamedMetadata("llvm.dbg.cu");
Copy link
Contributor

Choose a reason for hiding this comment

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

Module::debug_compile_units

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Module::debug_compile_units

Thanks! Will do as you suggested.

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Sep 24, 2025

It could be a case. In order to avoid that, I'd change EmitChangedFuncDebugInfo in the following way:

  1. When a mismatch between function arguments/return type and original DISubprogram's arguments/return type is detected, a new "trampoline" DISubprogram should be created, which will have updated arguments/return type.
  2. A new "trampoline" DISubprogram should be attached to the function instead of the original one (but the trampoline subprogram preserves the name/linkage name of the original subprogram in targetFuncName field).
  3. All DILocations of the function should be updated in the way as if the original DISubprogram is "inlined" into the "trampoline" DISubprogram, simliarly to what is done in llvm/lib/Transforms/Utils/InlineFunction.cpp:{fixupLineNumbers, fixupAssignments} (probably we can just reuse functions of InlineFunction.cpp).
  4. As the result, we should have the following DIEs:
  • An abstract DIE for the original DISubprogram with original arguments/return type.
  • Concrete DIE for the "trampoline" DISubprogram, having DW_AT_linkage_name, DW_AT_low_pc, DW_AT_high_pc corresponding to the function with modified arguments/return type, and with a children DW_TAG_inlined_subproutine, which will refer to abstract DIE of the original DISubprogram.

If a function, its DISubprogram and the related metadata is updated according to (1)-(3), no changes/few changes will be needed in DwarfDebug to achieve (4), IMO.

Let's illustrate that with an example.
If we have a function like

define dso_local noundef i32 @_Z3mulii(i32 noundef %0, i32 noundef %1) local_unnamed_addr #0 !dbg !0 {
    #dbg_value(i32 %0, !5, !DIExpression(), !7)
    #dbg_value(i32 %1, !6, !DIExpression(), !7)
  %3 = mul nsw i32 %1, %0, !dbg !8
  ret i32 %3, !dbg !9
}

!0 = distinct !DISubprogram(name: "mul", linkageName: "_Z3mulii", line: 2, type: !1, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !4)
!1 = !DISubroutineType(types: !2)
!2 = !{!3, !3, !3}
!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!4 = !{!5, !6}
!5 = !DILocalVariable(name: "num1", arg: 1, scope: !0, line: 2, type: !3)
!6 = !DILocalVariable(name: "num2", arg: 2, scope: !0, line: 2, type: !3)
!7 = !DILocation(line: 0, scope: !10)
!8 = !DILocation(line: 3, column: 17, scope: !0, atomGroup: 1, atomRank: 2)
!9 = !DILocation(line: 3, column: 5, scope: !0, atomGroup: 1, atomRank: 1)

And some LLVM passes transform it into something like:

define dso_local noundef i32 @_Z3mulii(i32 noundef %0) local_unnamed_addr #0 !dbg !0 {
    #dbg_value(i32 %0, !5, !DIExpression(), !7)
    #dbg_value(i32 %0, !6, !DIExpression(), !7)
  %3 = mul nsw i32 %0, %0, !dbg !8
  ret i32 %3, !dbg !9
}

; Function signature is not synced with metadata.
!0 = distinct !DISubprogram(name: "mul", linkageName: "_Z3mulii", line: 2, type: !1, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !4)
!1 = !DISubroutineType(types: !2)
!2 = !{!3, !3, !3}
!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!4 = !{!5, !6}
!5 = !DILocalVariable(name: "num1", arg: 1, scope: !0, line: 2, type: !3)
!6 = !DILocalVariable(name: "num2", arg: 2, scope: !0, line: 2, type: !3)
!7 = !DILocation(line: 0, scope: !10)
!8 = !DILocation(line: 3, column: 17, scope: !0, atomGroup: 1, atomRank: 2)
!9 = !DILocation(line: 3, column: 5, scope: !0, atomGroup: 1, atomRank: 1)

Then, EmitChangedFuncDebugInfo should transform debug info metadata into something like:

define dso_local noundef i32 @_Z3mulii(i32 noundef %0) local_unnamed_addr #0 !dbg !10 {
    #dbg_value(i32 %0, !14, !DIExpression(), !15)
    #dbg_value(i32 %0, !5, !DIExpression(), !16)
    #dbg_value(i32 %0, !6, !DIExpression(), !16)
  %3 = mul nsw i32 %0, %0, !dbg !18
  ret i32 %3, !dbg !19
}

; Old metadata for the function (but linkageName is removed from the old DISubprogram).
!0 = distinct !DISubprogram(name: "mul", line: 2, type: !1, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !4)
!1 = !DISubroutineType(types: !2)
!2 = !{!3, !3, !3}
!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!4 = !{!5, !6}
!5 = !DILocalVariable(name: "num1", arg: 1, scope: !0, line: 2, type: !3)
!6 = !DILocalVariable(name: "num2", arg: 2, scope: !0, line: 2, type: !3)
!8 = !DILocation(line: 3, column: 17, scope: !0, atomGroup: 1, atomRank: 2)
!9 = !DILocation(line: 3, column: 5, scope: !0, atomGroup: 1, atomRank: 1)

; New metadata for the function.
!10 = distinct !DISubprogram(name: "mul", linkageName: "_Z3mulii" line: 6, type: !11, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !13)
!11 = !DISubroutineType(types: !12)
!12 = !{!3, !3}
!13 = !{!14}
!14 = !DILocalVariable(name: "num1", arg: 1, scope: !10, line: 6, type: !3)
!15 = !DILocation(line: 0, scope: !10)
!16 = !DILocation(line: 0, scope: !0, inlinedAt: !17)
!17 = distinct !DILocation(line: 0, scope: !10)
!18 = !DILocation(line: 3, column: 17, scope: !0, inlinedAt: !17, atomGroup: 1, atomRank: 2)
!19 = !DILocation(line: 3, column: 5, scope: !0, inlinedAt: !17, atomGroup: 1, atomRank: 1)

Which should cause the following DWARF to be produced:

LOC_A: DW_TAG_subprogram
                DW_AT_name	("mul")
                DW_AT_decl_file	("/app/example.cpp")
                DW_AT_decl_line	(...)
                DW_AT_type	(0x0000003d "int")
                DW_AT_inline	(DW_INL_inlined)

      LOC_B: DW_TAG_formal_parameter
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
      LOC_C: DW_TAG_formal_parameter
                  DW_AT_name	("num2")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
     NULL

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_linkage_name	("_Z3mulii")
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

              DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(LOC_A "mul")
                  DW_AT_low_pc	(...)
                  DW_AT_high_pc	(...)
                  DW_AT_call_file	("/app/example.cpp")
                  DW_AT_call_line	(...)
                  DW_AT_call_column	(...)

                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_B "num1")
                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_C "num2")
             NULL
NULL

Would that work to solve the original issue?

@yonghong-song
Copy link
Contributor Author

yonghong-song commented Sep 24, 2025

Thanks! So the idea is to the actual function itself has tag for the original subprogram, but the the code in the actual function has tags for the modified subprogram. This may work. Will give a try.

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Sep 24, 2025

Thanks! So the idea is to the actual function itself has tag for the original subprogram, but the the code in the actual function has tags for the modified subprogram. This may work. Will give a try.

To avoid misunderstanding, in my example,LOC_A: DW_TAG_subprogram corresponds to !0 (it's an abstract DIE, so there may be no separate assembly funciton referring to it), second DW_TAG_subprogram corresponds to !10, and !10 is attached to @_Z3mulii after EmitChangedFuncDebugInfo.

@yonghong-song
Copy link
Contributor Author

@dzhidzhoev For the following dwarf:

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_linkage_name	("_Z3mulii")
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

              DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(LOC_A "mul")
                  DW_AT_low_pc	(...)
                  DW_AT_high_pc	(...)
                  DW_AT_call_file	("/app/example.cpp")
                  DW_AT_call_line	(...)
                  DW_AT_call_column	(...)

                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_B "num1")
                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_C "num2")
             NULL
NULL

If the tool tries to find the original signature based on

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_linkage_name	("_Z3mulii")
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

the tool may get incorrect result.
The current dwarf without this patch allows getting correct original function signature based on

DW_TAG_subprogram
  ...
  DW_TAG_formal_parameter
  ...
  DW_TAG_formal_parameter
  ...

WDYT? Could your proposed dwarf solution break some tools? Or this is something we can tolerate?

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Sep 26, 2025

Thank you for checking that!

Yes, having the exapmle, I understand better, what we're trying to achieve.
What if we try to keep linkage name of the original subprogram in the original DISugbprogram (!0) in the example, to get the DWARF like this:

LOC_A: DW_TAG_subprogram
                DW_AT_name	("mul")
                DW_AT_linkage_name	("_Z3mulii")
                DW_AT_decl_file	("/app/example.cpp")
                DW_AT_decl_line	(...)
                DW_AT_type	(0x0000003d "int")
                DW_AT_inline	(DW_INL_inlined)

      LOC_B: DW_TAG_formal_parameter
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
      LOC_C: DW_TAG_formal_parameter
                  DW_AT_name	("num2")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
     NULL

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

              DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(LOC_A "mul")
                  DW_AT_low_pc	(...)
                  DW_AT_high_pc	(...)
                  DW_AT_call_file	("/app/example.cpp")
                  DW_AT_call_line	(...)
                  DW_AT_call_column	(...)

                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_B "num1")
                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_C "num2")
             NULL
NULL

With LLVM IR produced by the EmitChangedFuncDebugInfo pass like this:

define dso_local noundef i32 @_Z3mulii(i32 noundef %0) local_unnamed_addr #0 !dbg !0 {
    #dbg_value(i32 %0, !14, !DIExpression(), !15)
    #dbg_value(i32 %0, !5, !DIExpression(), !16)
    #dbg_value(i32 %0, !6, !DIExpression(), !16)
  %3 = mul nsw i32 %0, %0, !dbg !18
  ret i32 %3, !dbg !19
}

; Old metadata for the function.
!0 = distinct !DISubprogram(name: "mul", linkageName: "_Z3mulii", line: 2, type: !1, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !4)
!1 = !DISubroutineType(types: !2)
!2 = !{!3, !3, !3}
!3 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!4 = !{!5, !6}
!5 = !DILocalVariable(name: "num1", arg: 1, scope: !0, line: 2, type: !3)
!6 = !DILocalVariable(name: "num2", arg: 2, scope: !0, line: 2, type: !3)
!8 = !DILocation(line: 3, column: 17, scope: !0, atomGroup: 1, atomRank: 2)
!9 = !DILocation(line: 3, column: 5, scope: !0, atomGroup: 1, atomRank: 1)

; New metadata for the function (and with removed linkageName, or with linkageName=="F.getName()", if "F.getName() != !0.getLinkageName()").
!10 = distinct !DISubprogram(name: "mul", line: 6, type: !11, spFlags: DISPFlagDefinition | DISPFlagOptimized, retainedNodes: !13)
!11 = !DISubroutineType(types: !12)
!12 = !{!3, !3}
!13 = !{!14}
!14 = !DILocalVariable(name: "num1", arg: 1, scope: !10, line: 6, type: !3)
!15 = !DILocation(line: 0, scope: !10)
!16 = !DILocation(line: 0, scope: !0, inlinedAt: !17)
!17 = distinct !DILocation(line: 0, scope: !10)
!18 = !DILocation(line: 3, column: 17, scope: !0, inlinedAt: !17, atomGroup: 1, atomRank: 2)
!19 = !DILocation(line: 3, column: 5, scope: !0, inlinedAt: !17, atomGroup: 1, atomRank: 1)

Will the result meet the conditions:
(1) the tool (pahole?) will correctly identify the signature of _Z3mulii,
(2) the tool will understand that mul() is not callable with "original" set of arguments (since LOC_A is an abstract subprogram DIE)?

Btw, I believe this is what the colleagues proposed in the course of the earlier discussion on #127855:

The idea, which we were briefly discussing today in the #gcc IRC channel, would be to have the signature change of some given function represented in DWARF using a DW_TAG_artificial "container" with an inlined subroutine reflecting the original signature. Something like:

foo.clone (int a, int b) { foo (1, { a, b }); } 

The signature change would be described in the formal parameters of the DW_TAG_inlined_subroutine.

It's an interesting idea... I don't totally object to it. Handles the case of "you can't call this" (because it's inlined) and I suppose with the WIP DWARFv6 feature you could even describe the return value even if it's ABI-optimized away (while still being present/computable within foo.clone).

We do not introduce any new LLVM IR functions (e.g. mul.clone or "dummy" functions), but from the DWARF's prospective, the original function is "inlined" into the function with modified signature.

@yonghong-song
Copy link
Contributor Author

yonghong-song commented Sep 26, 2025

For

(1) the tool (pahole?) will correctly identify the signature of _Z3mulii,

For C language, the linkage_name will likely be the same function name.

foo.clone (int a, int b) { foo (1, { a, b }); }

This will introduce new functions in symbol table. We will have to remove these functions at the end before final emitting the code. Otherwise users will be really confusing about why these new functions. Also, foo(1, {a, b}) may be inlined.

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Sep 26, 2025

foo.clone (int a, int b) { foo (1, { a, b }); }

This will introduce new functions in symbol table. We will have to remove these functions at the end before final emitting the code. Otherwise users will be really confusing about why these new functions. Also, foo(1, {a, b}) may be inlined.

What do you mean? From my understanding, this is not the code that should be produced in LLVM IR/MIR/assembly. This is just an illustration of how DWARF should look like.

We do not introduce any new LLVM IR functions (e.g. mul.clone or "dummy" functions), but from the DWARF's prospective, the original function is "inlined" into the function with modified signature.

@yonghong-song
Copy link
Contributor Author

foo.clone (int a, int b) { foo (1, { a, b }); }

This will introduce new functions in symbol table. We will have to remove these functions at the end before final emitting the code. Otherwise users will be really confusing about why these new functions. Also, foo(1, {a, b}) may be inlined.

What do you mean? From my understanding, this is not the code that should be produced in LLVM IR/MIR/assembly. This is just an illustration of how DWARF should look like.

We do not introduce any new LLVM IR functions (e.g. mul.clone or "dummy" functions), but from the DWARF's prospective, the original function is "inlined" into the function with modified signature.

Okay, indeed. mul.clone is not actually in IR. Just a conceptual idea from implementation point of view.

I will try to generate the following dwarf:

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

              DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(LOC_A "mul")
                  DW_AT_low_pc	(...)
                  DW_AT_high_pc	(...)
                  DW_AT_call_file	("/app/example.cpp")
                  DW_AT_call_line	(...)
                  DW_AT_call_column	(...)

                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_B "num1")
                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_C "num2")
             NULL
NULL

Here, the top DW_TAG_subprogram will have final function types instead of the original one. The original one will be encoded in DW_TAG_inlined_subroutine.

@yonghong-song
Copy link
Contributor Author

yonghong-song commented Oct 2, 2025

@dzhidzhoev I roughly finished a version which can roughly generate dwarf like

LOC_A: DW_TAG_subprogram
                DW_AT_name	("mul")
                DW_AT_linkage_name	("_Z3mulii")
                DW_AT_decl_file	("/app/example.cpp")
                DW_AT_decl_line	(...)
                DW_AT_type	(0x0000003d "int")
                DW_AT_inline	(DW_INL_inlined)

      LOC_B: DW_TAG_formal_parameter
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
      LOC_C: DW_TAG_formal_parameter
                  DW_AT_name	("num2")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")
     NULL

DW_TAG_subprogram
                DW_AT_low_pc	(0x0000000000000000)
                DW_AT_high_pc	(0x0000000000000006)
                DW_AT_frame_base	(DW_OP_reg7 RSP)
                DW_AT_call_all_calls	(true)
                DW_AT_name	("mul")
                ...

              DW_TAG_formal_parameter
                  DW_AT_location	(DW_OP_reg5 RDI)
                  DW_AT_name	("num1")
                  DW_AT_decl_file	("/app/example.cpp")
                  DW_AT_decl_line	(...)
                  DW_AT_type	(0x0000003d "int")

              DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(LOC_A "mul")
                  DW_AT_low_pc	(...)
                  DW_AT_high_pc	(...)
                  DW_AT_call_file	("/app/example.cpp")
                  DW_AT_call_line	(...)
                  DW_AT_call_column	(...)

                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_B "num1")
                    DW_TAG_formal_parameter
                            DW_AT_location	(DW_OP_reg5 RDI)
                            DW_AT_abstract_origin	(LOC_C "num2")
             NULL
NULL

But building linux kernel caused failure with 'pahole' where 'pahole' intends to generate BTF types. The error message from pahole:

bpf_lsm_socket_getpeersec_stream (bpf_lsm_socket_getpeersec_stream): skipping BTF encoding of function due to param count mismatch; 4 params != 6 params

From pahole perspective, it founds two functions which has identical name 'bpf_lsm_socket_getpeersec_stream' with different number of parameters. Note that in the above, there are two DW_TAG_subprogram with the same name and at the same level.
Since two functions have different signatures, function 'bpf_lsm_socket_getpeersec_stream' will be skipped in vmlinux BTF and this will cause error for later vmlinux build. Totally there are more than 1000 functions having this problems.

So in my opinion, having two DW_TAG_subprogram's at the same level with identical func name probably will break some tools. WDYT?

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Oct 2, 2025

So in my opinion, having two DW_TAG_subprogram's at the same level with identical func name probably will break some tools. WDYT?

I don't think this is a problem, especially when functions have different signatures. For example, DWARF in [0] has two DW_TAG_subprograms at the same level with the same DW_AT_names, and I think the majority of tools will easily process it.

From pahole perspective, it founds two functions which has identical name 'bpf_lsm_socket_getpeersec_stream' with different number of parameters. Note that in the above, there are two DW_TAG_subprogram with the same name and at the same level.

Are you sure that the tool matches functions based on the name in DWARF? I'm not familiar with pahole, but briefly looking at it, I assume that the error is caused by having two ELF symbols with the same name replesenting functions with different signatures (in [1] [2] functions are matched based on elf->name field). I doubt that it's caused by some sort of "incorrect DWARF". Correct me if I'm wrong.

[0] https://godbolt.org/z/9cW6xsr4z
[1] https://github.com/acmel/dwarves/blob/042d73962d35fdd1466e056f1ea14590b1cdbb9b/btf_encoder.c#L1463
[2] https://github.com/acmel/dwarves/blob/042d73962d35fdd1466e056f1ea14590b1cdbb9b/btf_encoder.c#L1371

@yonghong-song
Copy link
Contributor Author

Okay, I think I found the pahole issue which prevents linux kernel build.
The pahole fix likes below:

diff --git a/btf_encoder.c b/btf_encoder.c
index 0bc2334..18f0162 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -2652,6 +2652,8 @@ int btf_encoder__encode_cu(struct btf_encoder *encoder, struct cu *cu, struct co
                 */
                if (fn->declaration)
                        continue;
+               if (function__inlined(fn))
+                       continue;
                if (!ftype__has_arg_names(&fn->proto))
                        continue;
                if (funcs->cnt) {

Basically if the 'fn' is inlined, we should not add this 'fn' into the elf function list.

For this particular func, my unpublished RFC code generates the following dwarf (from vmlinux.o):

0x01c2660a:   DW_TAG_subprogram
                DW_AT_name      ("bpf_lsm_socket_getpeersec_stream")
                DW_AT_decl_file ("/home/yhs/work/bpf-next/include/linux/lsm_hook_defs.h")
                DW_AT_decl_line (344)
                DW_AT_prototyped        (true)    
                DW_AT_type      (0x01c0a7db "int")
                DW_AT_external  (true)
                DW_AT_inline    (DW_INL_inlined)

0x01c26614:     DW_TAG_formal_parameter
                  DW_AT_name    ("sock")  
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/lsm_hook_defs.h")
                  DW_AT_decl_line       (344)     
                  DW_AT_type    (0x01c1b4e0 "socket *")

0x01c2661e:     DW_TAG_formal_parameter
                  DW_AT_name    ("optval")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/lsm_hook_defs.h")
                  DW_AT_decl_line       (344)     
                  DW_AT_type    (0x01c1429d "sockptr_t")

0x01c26628:     DW_TAG_formal_parameter
                  DW_AT_name    ("optlen")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/lsm_hook_defs.h")
                  DW_AT_decl_line       (344)     
                  DW_AT_type    (0x01c1429d "sockptr_t")

0x01c26632:     DW_TAG_formal_parameter
                  DW_AT_name    ("len")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/lsm_hook_defs.h")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c0aa01 "unsigned int")

0x01c2663b:     NULL

0x01c2663c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x000000000077bfb0)
                DW_AT_high_pc   (0x000000000077bfc4)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("bpf_lsm_socket_getpeersec_stream")
                DW_AT_name      ("bpf_lsm_socket_getpeersec_stream")
                DW_AT_decl_file ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                DW_AT_decl_line (344)
                DW_AT_type      (0x01c0a7db "int")
                DW_AT_external  (true)

0x01c26650:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_name    ("sock")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c1b4e0 "socket *")

0x01c2665c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_name    ("optval.coerce0")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c2790e "structure  *")

0x01c26668:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg1 RDX)
                  DW_AT_name    ("optval.coerce1")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c27913 "int8")

0x01c26674:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg2 RCX)
                  DW_AT_name    ("optlen.coerce0")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c2790e "structure  *")

0x01c26680:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg8 R8)
                  DW_AT_name    ("optlen.coerce1")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c27913 "int8")

0x01c2668c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg9 R9)
                  DW_AT_name    ("len")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_decl_line       (344)
                  DW_AT_type    (0x01c0aa01 "unsigned int")

0x01c26697:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x01c2660a "bpf_lsm_socket_getpeersec_stream")
                  DW_AT_low_pc  (0x000000000077bfb9)
                  DW_AT_high_pc (0x000000000077bfc4)
                  DW_AT_call_file       ("/home/yhs/work/bpf-next/kernel/bpf/bpf_lsm.c")
                  DW_AT_call_line       (0)

0x01c266a4:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x01c26614 "sock")

0x01c266ab:       DW_TAG_formal_parameter
                    DW_AT_location      (indexed (0x0) loclist = 0x003f0aad:
                       [0x000000000077bfb9, 0x000000000077bfc4): DW_OP_reg4 RSI, DW_OP_piece 0x8, DW_OP_reg1 RDX, DW_OP_piece 0x1)
                    DW_AT_abstract_origin       (0x01c2661e "optval")

0x01c266b1:       DW_TAG_formal_parameter
                    DW_AT_location      (indexed (0x1) loclist = 0x003f0aba:
                       [0x000000000077bfb9, 0x000000000077bfc4): DW_OP_reg2 RCX, DW_OP_piece 0x8, DW_OP_reg8 R8, DW_OP_piece 0x1)
                    DW_AT_abstract_origin       (0x01c26628 "optlen")

0x01c266b7:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg9 R9)
                    DW_AT_abstract_origin       (0x01c26632 "len")

0x01c266be:       NULL

0x01c266bf:     NULL

===========

0x01c1429d:   DW_TAG_typedef
                DW_AT_type      (0x01c142a6 "structure ")
                DW_AT_name      ("sockptr_t")
                DW_AT_decl_file ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                DW_AT_decl_line (20)

0x01c142a6:   DW_TAG_structure_type
                DW_AT_byte_size (0x10)
                DW_AT_decl_file ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                DW_AT_decl_line (14)

0x01c142aa:     DW_TAG_member
                  DW_AT_type    (0x01c142b2 "structure ::union ")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                  DW_AT_decl_line       (15)
                  DW_AT_data_member_location    (0x00)
              
0x01c142b2:     DW_TAG_union_type       
                  DW_AT_byte_size       (0x08)
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                  DW_AT_decl_line       (15)
                
0x01c142b6:       DW_TAG_member
                    DW_AT_name  ("kernel")
                    DW_AT_type  (0x01c0b2da "void *")
                    DW_AT_decl_file     ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                    DW_AT_decl_line     (16)
                    DW_AT_data_member_location  (0x00)

0x01c142c0:       DW_TAG_member
                    DW_AT_name  ("user")
                    DW_AT_type  (0x01c142d7 "void *")
                    DW_AT_decl_file     ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                    DW_AT_decl_line     (17)
                    DW_AT_data_member_location  (0x00)

0x01c142ca:       NULL

0x01c142cb:     DW_TAG_member
                  DW_AT_name    ("is_kernel")
                  DW_AT_type    (0x01c0b09a "bool")
                  DW_AT_decl_file       ("/home/yhs/work/bpf-next/include/linux/sockptr.h")
                  DW_AT_decl_line       (19)
                  DW_AT_bit_size        (0x01)
                  DW_AT_data_bit_offset (0x40)

0x01c142d6:     NULL

Could you help check whether the above format is expected or not?

Running bpf selftests, I found additional failures. Mostly likely additional pahole change is needed. I am debugging those now.

@dzhidzhoev
Copy link
Member

Could you help check whether the above format is expected or not?

Yes, it looks kind of what I expected. Hope it solves the original problem and doesn't create the new ones.

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Oct 2, 2025

So in my opinion, having two DW_TAG_subprogram's at the same level with identical func name probably will break some tools. WDYT?

I don't think this is a problem, especially when functions have different signatures. For example, DWARF in [0] has two DW_TAG_subprograms at the same level with the same DW_AT_names, and I think the majority of tools will easily process it.

A quick note on that. I remembered the message from the discussion in #127855:

One possible alternate solution is to have an attribute on the DW_TAG_subprogram DIE that points to a function type DIE from the original source.

Seems like it'd be a substantial diversion from existing practice - now the primary subprogram wouldn't reflect the source anymore & debuggers unaware of the extra attribute/content would render something confusing to the user (describing the function to the user in a way that doesn't match the source) - overload resolution and other features in the debugger might behave in unexpected ways.

I think, having two functions with the same DW_AT_name but different formal parameters may indeed interfere with debugger name resolution: debugger users will be able to call the function with the signature not matching the signature from the source code.
Probably, DW_TAG_subprogram with the final list of parameters (0x01c2663c: DW_TAG_subprogram in your example) should have DW_AT_artifical to prevent that (DW_AT_artifical can be emitted by raising DIFlagArtificial in the corresponding DISubprogram).

@llvmbot llvmbot added the PGO Profile Guided Optimizations label Oct 3, 2025
@yonghong-song
Copy link
Contributor Author

Just uploaded a new revision which used DW_TAG_inlined_subroutine to represent the function with original signature. The DW_TAG_inlined_subroutine will be the children of the DW_TAG_subprogram.

With pahole change

diff --git a/btf_encoder.c b/btf_encoder.c
index 0bc2334..18f0162 100644
--- a/btf_encoder.c
+++ b/btf_encoder.c
@@ -2652,6 +2652,8 @@ int btf_encoder__encode_cu(struct btf_encoder *encoder, struct cu *cu, struct co
                 */
                if (fn->declaration)
                        continue;
+               if (function__inlined(fn))
+                       continue;
                if (!ftype__has_arg_names(&fn->proto))
                        continue;
                if (funcs->cnt) {

Linux kernel can be built successfully and I am running bpf selftest. There are only a couple of selftest failures due to the kernel/module func signature change which can be resolved by modifying the code with proper actual signature.

@dzhidzhoev
Copy link
Member

dzhidzhoev commented Oct 3, 2025

Could you please add more tests for that?

  1. A test to check what EmitChangedFuncDebugInfo produces.
  2. A test to check how ArgumentPromotion pass works together with EmitChangedFuncDebugInfo (this may also be relevant for other passes that modify function signatures).
  3. Would be nice to have a cross-project test to see if lldb can handle the resulting DWARFs properly. It can check whether this problem does not occur.

I think, having two functions with the same DW_AT_name but different formal parameters may indeed interfere with debugger name resolution: debugger users will be able to call the function with the signature not matching the signature from the source code.
Probably, DW_TAG_subprogram with the final list of parameters (0x01c2663c: DW_TAG_subprogram in your example) should have DW_AT_artifical to prevent that (DW_AT_artifical can be emitted by raising DIFlagArtificial in the corresponding DISubprogram).

I think it will be easier to review this if we have examples of LLVM IR input + expected LLVM IR or DWARF output in form of tests.

@yonghong-song
Copy link
Contributor Author

Could you please add more tests for that?

1. A test to check what EmitChangedFuncDebugInfo produces.

2. A test to check how ArgumentPromotion pass works together with EmitChangedFuncDebugInfo (this may also be relevant for other passes that modify function signatures).

3. Would be nice to have a cross-project test to see if lldb can handle the resulting DWARFs properly. It can check whether this problem does not occur.

I think, having two functions with the same DW_AT_name but different formal parameters may indeed interfere with debugger name resolution: debugger users will be able to call the function with the signature not matching the signature from the source code.
Probably, DW_TAG_subprogram with the final list of parameters (0x01c2663c: DW_TAG_subprogram in your example) should have DW_AT_artifical to prevent that (DW_AT_artifical can be emitted by raising DIFlagArtificial in the corresponding DISubprogram).

I think it will be easier to review this if we have examples of LLVM IR input + expected LLVM IR or DWARF output in form of tests.

Thanks for the suggestions. Among other things I mentioned in the description, I will add test cases as well in the next revision.

@OCHyams
Copy link
Contributor

OCHyams commented Oct 3, 2025

Hi, I've read over this thread but haven't managed to build a full picture of what's being introduced here. I see "RFC" in the PR title - has this change been discussed on discourse? It might be worth doing that if you haven't, to raise awareness, if this is going to affect many debug info consumers.

As far as I understand it from the thread, this change intends to add an additional DWARF inline scope to functions with changed signatures, where the inline scope has the original signature. Is that right? If so, isn't that going to cause debuggers to spuriously show an extra "inline" frame in the call stack?

  • Do you intend to have this feature (pass?) enabled by default?
  • What assumptions in DWARF consumers is this likely to break?

@yonghong-song
Copy link
Contributor Author

Hi, I've read over this thread but haven't managed to build a full picture of what's being introduced here. I see "RFC" in the PR title - has this change been discussed on discourse? It might be worth doing that if you haven't, to raise awareness, if this is going to affect many debug info consumers.

I have an old discourse thread. I just made some update on it. See
https://discourse.llvm.org/t/rfc-identify-func-signature-change-in-llvm-compiled-kernel-image/82609/6

Initial format at (#127855) will not change debugger. But it seems not elegant from dwarf perspective. So the new mechanism is to directly encode actual signature in debuginfo/dwarf and use inlinedAt to refer to the original signature.

As far as I understand it from the thread, this change intends to add an additional DWARF inline scope to functions with changed signatures, where the inline scope has the original signature. Is that right? If so, isn't that going to cause debuggers to spuriously show an extra "inline" frame in the call stack?

Yes. I am not familiar with lldb in this regard. Do we have an example to show how lldb will work in such cases?

* Do you intend to have this feature (pass?) enabled by default?

Yes. I do. I suggest to support C language first which probably will have least impact compared to C++ or other languages. If there are non-resolvable issues (I hope not), we may not be able to enable by default. We can have a flag to enable it but when building linux kernel, we can enable this feature.

* What assumptions in DWARF consumers is this likely to break?

Good question. I don't have concrete cases for this. I guess the main issue probably with artificial inlining.

Copy link
Contributor

@eddyz87 eddyz87 left a comment

Choose a reason for hiding this comment

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

Tbh, I'm not sure why upstream insists on this being a separate pass. From ad-hoc patterns in e.g. getArg() it appears that original idea to handle signature changes in each individual optimization is simpler and more reliable way handle this.

OldSP->getLine(), // Line
nullptr, // DISubroutineType
OldSP->getScopeLine(), // ScopeLine
DINode::FlagZero, DISubprogram::SPFlagDefinition);
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 think call DIB.createArtificialSubprogram(NewSP) can be replaced with DINode::FlagArtificial flag specified here.
Alternatively, will it make sense to just call DIB.createArtificialSubprogram(NewSP) instead of DIB.createFunction(...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding DINode::FlagArtificial sounds a better choice.


static bool getTypeArgList(Module &M, DIBuilder &DIB, Function *F,
DISubprogram *OldSP, DISubprogram *NewSP,
SmallVector<Metadata *, 5> &TypeList,
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 think canonical way to pass small vector param is SmallVectorImpl<Metadata> &.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack. Will do.


// Go through the function itself to replace DILocations.
LLVMContext &Ctx = M.getContext();
DILocation *DL2 = DILocation::get(Ctx, 0, 0, NewSP, nullptr, 0, 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: only first three parameters are required, the rest defaults to zero.

DLlist.push_back(OldDL);
}
DILocation *PrevLoc = DL2;
for (int i = DLlist.size() - 1; i >= 0; i--) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this computation is copy-pasted below for I.getDebugLoc(), maybe extract it as a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. A little bit repetition. I didn't polish the code a lot since this is a RFC and things could change again. But will make the change in next revision.

for (int i = DLlist.size() - 1; i >= 0; i--) {
OldDL = DLlist[i];
PrevLoc = DILocation::get(
Ctx, OldDL->getLine(), OldDL->getColumn(), OldDL->getScope(),
Copy link
Contributor

Choose a reason for hiding this comment

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

OldDL->getScope() do scopes need cloning alongside the containing subprogram?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The scope will be preserved as the original.


// Strip modifiers (const, volatile, etc.)
DIType *DITy = Var->getType();
while (auto *DTy = dyn_cast<DIDerivedType>(DITy)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this derived type processing is copy-pasted multiple times, maybe extract a utility function?

// !DIExpression(DW_OP_LLVM_fragment, 64, 64), !14768)
// ...
// }
static DIType *getTypeFromExpr(DIBuilder &DIB, DIExpression *Expr,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is used only for integer types, right?
Maybe rename it to getIntTypeFromExpr()?

@yonghong-song
Copy link
Contributor Author

Tbh, I'm not sure why upstream insists on this being a separate pass. From ad-hoc patterns in e.g. getArg() it appears that original idea to handle signature changes in each individual optimization is simpler and more reliable way handle this.

Thanks for suggestion. Since now we have an overall implementation, let us see whether changes in each individual optimization make sense or not vs. the current implementaiton.

@yonghong-song
Copy link
Contributor Author

@OCHyams Since you are worried about debugger with this pull request, could you show me some examples how the change could affect debugger? If the debugger is not happy, we either need to tweak the current implementation or we might need to choose other alternative solution (e.g., yonghong-song@be0cdea). Thanks!

alan-maguire pushed a commit to alan-maguire/dwarves that referenced this pull request Oct 9, 2025
In llvm pull request [1], the dwarf is changed to accommodate functions
whose signatures are different from source level although they have
the same name. Other non-source functions are also included in dwarf.

The following is an example:

The source:
====
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }
====

Part of generated dwarf:
====
0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL
====

In the above, there are two subprograms with the same name 'foo'.
Currently btf encoder will consider both functions as ELF functions.
Since two subprograms have different signature, the funciton will
be ignored.

But actually, one of function 'foo' is marked as DW_INL_inlined which means
we should not treat it as an elf funciton. The patch fixed this issue
by filtering subprograms if the corresponding function__inlined() is true.

This will fix the issue for [1]. But it should work fine without [1] too.

  [1] llvm/llvm-project#157349

Signed-off-by: Yonghong Song <[email protected]>
Cc: Arnaldo Carvalho de Melo <[email protected]>
Cc: Andrii Nakryiko <[email protected]>
Cc: Alexei Starovoitov <[email protected]>
Cc: Daniel Borkmann <[email protected]>
Link: https://lore.kernel.org/dwarves/[email protected]/
Signed-off-by: Alan Maguire <[email protected]>
Yonghong Song added 3 commits October 11, 2025 22:19
ArgumentPromotion pass may change function signatures. If this happens
and debuginfo is enabled, let us add DW_CC_nocall to debuginfo so it is
clear that the function signature has changed.
DeadArgumentElimination ([1]) has similar implementation.

Also fix an ArgumentPromotion test due to adding DW_CC_nocall to
debuginfo.

  [1] llvm@340b0ca
During development of emitting dwarf data for signature-changed or new
functions, I found two test failures
  llvm/test/Transforms/SampleProfile/ctxsplit.ll
  llvm/test/Transforms/SampleProfile/flattened.ll
due to incorrect DISubroutineType(s). This patch fixed the issue with
proper types.
Add a new pass EmitChangedFuncDebugInfo which will add dwarf for
additional functions including functions with signature change
and new functions.

The previous approach in [1] tries to add debuginfo for those
optimization passes which cause signature changes. Based on
discussion in [1], it is preferred to have a specific pass to
add debuginfo and later on dwarf generation can include those
new debuginfo.

The following is an example:
Source:
  $ cat test.c
  struct t { int a; };
  char *tar(struct t *a, struct t *d);
  __attribute__((noinline)) static char * foo(struct t *a, struct t *d, int b)
  {
    return tar(a, d);
  }
  char *bar(struct t *a, struct t *d)
  {
    return foo(a, d, 1);
  }
Compiled and dump dwarf with:
  clang -O2 -c -g test.c
  llvm-dwarfdump test.o

0x0000005c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000010)
                DW_AT_high_pc   (0x0000000000000015)
                DW_AT_frame_base        (DW_OP_reg7 RSP)
                DW_AT_linkage_name      ("foo")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_artificial        (true)
                DW_AT_external  (true)

0x0000006c:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg5 RDI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x00000075:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_reg4 RSI)
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x0000007e:     DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin (0x0000009a "foo")
                  DW_AT_low_pc  (0x0000000000000010)
                  DW_AT_high_pc (0x0000000000000015)
                  DW_AT_call_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_call_line       (0)

0x0000008a:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg5 RDI)
                    DW_AT_abstract_origin       (0x000000a2 "a")

0x00000091:       DW_TAG_formal_parameter
                    DW_AT_location      (DW_OP_reg4 RSI)
                    DW_AT_abstract_origin       (0x000000aa "d")

0x00000098:       NULL

0x00000099:     NULL

0x0000009a:   DW_TAG_subprogram
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/home/yhs/tests/sig-change/deadarg/test.c")
                DW_AT_decl_line (3)
                DW_AT_prototyped        (true)
                DW_AT_type      (0x000000bb "char *")
                DW_AT_inline    (DW_INL_inlined)

0x000000a2:     DW_TAG_formal_parameter
                  DW_AT_name    ("a")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000aa:     DW_TAG_formal_parameter
                  DW_AT_name    ("d")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000c4 "t *")

0x000000b2:     DW_TAG_formal_parameter
                  DW_AT_name    ("b")
                  DW_AT_decl_file       ("/home/yhs/tests/sig-change/deadarg/test.c")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x000000d8 "int")

0x000000ba:     NULL

There are some restrictions in the current implementation:
  - Only C language is supported
  - BPF target is excluded as one of main goals for this pull request
    is to generate proper vmlinux BTF for arch's like x86_64/arm64 etc.
  - Function must not be a intrinsic, decl only, return value size more
    than arch register size and func with variable arguments.

I have tested this patch set by building latest bpf-next linux kernel.
For no-lto case:
  65341 original number of functions
  1085  new functions with this patch
For thin-lto case:
  65595 original number of functions
  2492  new functions with this patch

  [1] llvm#127855
@yonghong-song
Copy link
Contributor Author

@OCHyams I just uploaded another version which fixed some issues mentioned by @eddyz87. But the potential issue related to debugger is not investigated yet and I need your guidance how to test.

As discussed in earlier this thread, the new implementation is very similar to code like foo.clone (int a, int b) { foo (1, { a, b }); } where foo.clone represents the true signatures and the original function is inlined with proper debuginfo representation. Of course, 'foo.clone' name is just an illustration and its actual name is from the Function itself.

I think there probably already some examples in debugger to track inlined subroutines, it would be great if you can give some example.

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.

6 participants