[llvm] [CodeView] Match MSVC implicit this records for Visual Studio (PR #214289)

via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 5 23:23:43 PDT 2026


https://github.com/GkvJwa updated https://github.com/llvm/llvm-project/pull/214289

>From fbc71a410610904458fdf2772f2426fd25186ae8 Mon Sep 17 00:00:00 2001
From: GkvJwa <gkvjwa at gmail.com>
Date: Thu, 6 Aug 2026 14:23:36 +0800
Subject: [PATCH] [CodeView] Match MSVC implicit this records for Visual Studio

The Visual Studio debugger does not resolve LLVM's S_LOCAL and S_DEFRANGE
representation for an implicit C++ `this` pointer.

MSVC emits this location as S_REGREL32 and provides DbgStart/DbgEnd in the
containing S_GPROC32_ID record. Match this representation for simple stack
locations so the Visual Studio debugger can resolve `this` while stepping
through functions such as constructors and inspect member variables through
it.

The debug range also prevents the debugger from using the location after
the function epilogue restores the stack pointer.
---
 llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp | 142 +++++++++++++++-
 llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h   |  12 ++
 .../DebugInfo/COFF/implicit-this-regrel.ll    | 155 ++++++++++++++++++
 3 files changed, 305 insertions(+), 4 deletions(-)
 create mode 100644 llvm/test/DebugInfo/COFF/implicit-this-regrel.ll

diff --git a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp
index 84ea5e349f01d..525f7b5d746f8 100644
--- a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp
@@ -111,6 +111,12 @@ class CVMCAdapter : public CodeViewRecordStreamer {
 };
 } // namespace
 
+static bool isImplicitThisVariable(const DILocalVariable *Var,
+                                   const DISubprogram *SP = nullptr) {
+  return Var && (!SP || Var->getScope() == SP) && Var->isParameter() &&
+         Var->isObjectPointer() && Var->getName() == "this";
+}
+
 static CPUType mapArchToCVCPUType(Triple::ArchType Type) {
   switch (Type) {
   case Triple::ArchType::x86:
@@ -1167,10 +1173,17 @@ void CodeViewDebug::emitDebugInfoForFunction(const Function *GV,
     // code is located and what's its size:
     OS.AddComment("Code size");
     OS.emitAbsoluteSymbolDiff(FI.End, Fn, 4);
+    const bool HasDebugRange = FI.DebugStart && FI.DebugEnd;
     OS.AddComment("Offset after prologue");
-    OS.emitInt32(0);
+    if (HasDebugRange)
+      OS.emitAbsoluteSymbolDiff(FI.DebugStart, Fn, 4);
+    else
+      OS.emitInt32(0);
     OS.AddComment("Offset before epilogue");
-    OS.emitInt32(0);
+    if (HasDebugRange)
+      OS.emitAbsoluteSymbolDiff(FI.DebugEnd, Fn, 4);
+    else
+      OS.emitInt32(0);
     OS.AddComment("Function type index");
     OS.emitInt32(getFuncIdForSubprogram(GV->getSubprogram()).getIndex());
     OS.AddComment("Function section relative address");
@@ -1568,6 +1581,15 @@ void CodeViewDebug::beginFunctionImpl(const MachineFunction *MF) {
 
   OS.emitCVFuncIdDirective(CurFn->FuncId);
 
+  const DISubprogram *SP = GV.getSubprogram();
+  const bool HasImplicitThis =
+      any_of(
+          MF->getVariableDbgInfo(),
+          [&](const auto &VI) { return isImplicitThisVariable(VI.Var, SP); }) ||
+      any_of(DbgValues, [&](const auto &I) {
+        return isImplicitThisVariable(cast<DILocalVariable>(I.first.first), SP);
+      });
+
   // Find the end of the function prolog.  First known non-DBG_VALUE and
   // non-frame setup location marks the beginning of the function body.
   // FIXME: is there a simpler a way to do this? Can we just search
@@ -1578,6 +1600,10 @@ void CodeViewDebug::beginFunctionImpl(const MachineFunction *MF) {
     for (const auto &MI : MBB) {
       if (!MI.isMetaInstruction() && !MI.getFlag(MachineInstr::FrameSetup) &&
           MI.getDebugLoc()) {
+        if (HasImplicitThis && !CurFn->PrologueEnd) {
+          CurFn->PrologueEnd = &MI;
+          requestLabelBeforeInsn(&MI);
+        }
         PrologEndLoc = MI.getDebugLoc();
         break;
       } else if (!MI.isMetaInstruction()) {
@@ -1602,6 +1628,25 @@ void CodeViewDebug::beginFunctionImpl(const MachineFunction *MF) {
     }
   }
 
+  if (HasImplicitThis) {
+    for (const auto &MBB : *MF) {
+      const MachineInstr *EpilogueBegin = nullptr;
+      for (const auto &MI : MBB) {
+        if (!MI.isMetaInstruction() && MI.getFlag(MachineInstr::FrameDestroy)) {
+          EpilogueBegin = &MI;
+          requestLabelBeforeInsn(EpilogueBegin);
+          break;
+        }
+      }
+      if (!EpilogueBegin)
+        continue;
+
+      MachineBasicBlock::const_iterator Last = MBB.getLastNonDebugInstr();
+      if (Last != MBB.end())
+        requestLabelAfterInsn(&*Last);
+    }
+  }
+
   // Mark branches that may potentially be using jump tables with labels.
   bool isThumb = MMI->getModule()->getTargetTriple().getArch() ==
                  llvm::Triple::ArchType::thumb;
@@ -2845,8 +2890,34 @@ void CodeViewDebug::emitLocalVariableList(const FunctionInfo &FI,
   }
 }
 
+bool CodeViewDebug::LocalVariable::isSimpleImplicitThis() const {
+  if (!isImplicitThisVariable(DIVar) || DefRanges.size() != 1)
+    return false;
+
+  const auto &DefRange = DefRanges.begin()->first;
+  const auto &Ranges = DefRanges.begin()->second;
+  return DefRange.InMemory && !DefRange.IsSubfield &&
+         DefRange.DerefOffset == LocalVarDef::NoDeref && Ranges.size() == 1;
+}
+
 void CodeViewDebug::emitLocalVariable(const FunctionInfo &FI,
                                       const LocalVariable &Var) {
+  if (FI.DebugStart && FI.DebugEnd && FI.EpilogueRanges.size() <= 1 &&
+      Var.isSimpleImplicitThis()) {
+    const auto &DefRange = Var.DefRanges.begin()->first;
+    MCSymbol *LocalEnd = beginSymbolRecord(SymbolKind::S_REGREL32);
+    OS.AddComment("Offset");
+    OS.emitInt32(DefRange.DataOffset);
+    OS.AddComment("TypeIndex");
+    OS.emitInt32(getCompleteTypeIndex(Var.DIVar->getType()).getIndex());
+    OS.AddComment("Register");
+    OS.emitInt16(DefRange.CVRegister);
+    OS.AddComment("Name");
+    emitNullTerminatedSymbolName(OS, Var.DIVar->getName());
+    endSymbolRecord(LocalEnd);
+    return;
+  }
+
   // LocalSym record, see SymbolRecord.h for more info.
   MCSymbol *LocalEnd = beginSymbolRecord(SymbolKind::S_LOCAL);
 
@@ -2871,7 +2942,26 @@ void CodeViewDebug::emitLocalVariable(const FunctionInfo &FI,
   SmallString<20> BytePrefix;
   for (const auto &Pair : Var.DefRanges) {
     LocalVarDef DefRange = Pair.first;
-    const auto &Ranges = Pair.second;
+    using Range = std::pair<const MCSymbol *, const MCSymbol *>;
+    ArrayRef<Range> Ranges = Pair.second;
+    SmallVector<Range, 4> RangesWithoutEpilogues;
+
+    if (isImplicitThisVariable(Var.DIVar) && FI.EpilogueRanges.size() > 1 &&
+        DefRange.InMemory && !DefRange.IsSubfield &&
+        DefRange.DerefOffset == LocalVarDef::NoDeref && Ranges.size() == 1) {
+      const MCSymbol *Begin = Ranges.front().first;
+      const MCSymbol *End = Ranges.front().second;
+      for (const auto &Epilogue : FI.EpilogueRanges) {
+        if (Begin != Epilogue.first)
+          RangesWithoutEpilogues.emplace_back(Begin, Epilogue.first);
+        Begin = Epilogue.second;
+      }
+      if (Begin != End)
+        RangesWithoutEpilogues.emplace_back(Begin, End);
+      if (!RangesWithoutEpilogues.empty())
+        Ranges = RangesWithoutEpilogues;
+    }
+
     BytePrefix.clear();
     if (DefRange.InMemory) {
       int Offset = DefRange.DataOffset;
@@ -2900,7 +2990,8 @@ void CodeViewDebug::emitLocalVariable(const FunctionInfo &FI,
         DRHdr.BasePointerOffset = Offset;
         DRHdr.OffsetInUdt = DefRange.DerefOffset;
         OS.emitCVDefRangeDirective(Ranges, DRHdr);
-      } else if (!DefRange.IsSubfield && EncFP != EncodedFramePtrReg::None &&
+      } else if (!isImplicitThisVariable(Var.DIVar) && !DefRange.IsSubfield &&
+                 EncFP != EncodedFramePtrReg::None &&
                  (bool(Flags & LocalSymFlags::IsParameter)
                       ? (EncFP == FI.EncodedParamFramePtrReg)
                       : (EncFP == FI.EncodedLocalFramePtrReg))) {
@@ -3085,6 +3176,49 @@ void CodeViewDebug::endFunctionImpl(const MachineFunction *MF) {
 
   collectVariableInfo(GV.getSubprogram());
 
+  const LocalVariable *ImplicitThis = nullptr;
+  for (const auto &ScopeVars : ScopeVariables) {
+    for (const LocalVariable &Var : ScopeVars.second) {
+      if (isImplicitThisVariable(Var.DIVar, GV.getSubprogram())) {
+        ImplicitThis = &Var;
+        break;
+      }
+    }
+    if (ImplicitThis)
+      break;
+  }
+  if (ImplicitThis) {
+    for (const auto &MBB : *MF) {
+      const MachineInstr *EpilogueBegin = nullptr;
+      for (const auto &MI : MBB) {
+        if (!MI.isMetaInstruction() && MI.getFlag(MachineInstr::FrameDestroy)) {
+          EpilogueBegin = &MI;
+          break;
+        }
+      }
+      if (!EpilogueBegin)
+        continue;
+
+      MachineBasicBlock::const_iterator Last = MBB.getLastNonDebugInstr();
+      if (Last == MBB.end())
+        continue;
+
+      MCSymbol *EpilogueEnd = getLabelAfterInsn(&*Last);
+      assert(EpilogueEnd && "missing label after epilogue");
+      CurFn->EpilogueRanges.emplace_back(getLabelBeforeInsn(EpilogueBegin),
+                                         EpilogueEnd);
+    }
+
+    CurFn->DebugStart =
+        CurFn->PrologueEnd
+            ? getLabelBeforeInsn(CurFn->PrologueEnd)
+            : (ImplicitThis->DefRanges.empty()
+                   ? nullptr
+                   : ImplicitThis->DefRanges.begin()->second.front().first);
+    if (!CurFn->EpilogueRanges.empty())
+      CurFn->DebugEnd = CurFn->EpilogueRanges.back().first;
+  }
+
   // Build the lexical block structure to emit for this routine.
   if (LexicalScope *CFS = LScopes.getCurrentFunctionScope())
     collectLexicalBlockInfo(*CFS,
diff --git a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
index e28e0ab6da36b..851c04f413efc 100644
--- a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
+++ b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
@@ -50,6 +50,7 @@ class MCSectionCOFF;
 class MCStreamer;
 class MCSymbol;
 class MachineFunction;
+class MachineInstr;
 
 /// Collects and handles line tables information in a CodeView format.
 class LLVM_LIBRARY_VISIBILITY CodeViewDebug : public DebugHandlerBase {
@@ -129,6 +130,8 @@ class LLVM_LIBRARY_VISIBILITY CodeViewDebug : public DebugHandlerBase {
               SmallVector<std::pair<const MCSymbol *, const MCSymbol *>, 1>>
         DefRanges;
     std::optional<APSInt> ConstantValue;
+
+    bool isSimpleImplicitThis() const;
   };
 
   struct CVGlobalVariable {
@@ -200,6 +203,15 @@ class LLVM_LIBRARY_VISIBILITY CodeViewDebug : public DebugHandlerBase {
 
     const MCSymbol *Begin = nullptr;
     const MCSymbol *End = nullptr;
+
+    const MCSymbol *DebugStart = nullptr;
+    const MCSymbol *DebugEnd = nullptr;
+
+    SmallVector<std::pair<const MCSymbol *, const MCSymbol *>, 1>
+        EpilogueRanges;
+
+    const MachineInstr *PrologueEnd = nullptr;
+
     unsigned FuncId = 0;
     unsigned LastFileId = 0;
 
diff --git a/llvm/test/DebugInfo/COFF/implicit-this-regrel.ll b/llvm/test/DebugInfo/COFF/implicit-this-regrel.ll
new file mode 100644
index 0000000000000..7aa377df63dab
--- /dev/null
+++ b/llvm/test/DebugInfo/COFF/implicit-this-regrel.ll
@@ -0,0 +1,155 @@
+; RUN: llc < %s -filetype=obj | llvm-readobj - --codeview | FileCheck %s --check-prefix=SIMPLE
+; RUN: llc < %s | llvm-mc -filetype=obj --triple=x86_64-windows | llvm-readobj - --codeview | FileCheck %s --check-prefix=SIMPLE
+; RUN: llc < %s -O0 -enable-tail-merge=0 -filetype=obj | llvm-readobj - --codeview | FileCheck %s --check-prefix=MULTI
+
+; Check that the compatibility S_REGREL32 record used for an implicit C++
+; `this` pointer is bounded by the containing procedure's debug range. In
+; particular, the range must end before the stack frame is restored.
+;
+; A stack-based implicit `this` cannot use S_REGREL32 when a function has
+; multiple epilogues. Each epilogue must instead be represented as a gap in
+; S_DEFRANGE_REGISTER_REL.
+
+; SIMPLE:      GlobalProcIdSym {
+; SIMPLE:        CodeSize: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; SIMPLE:        DbgStart: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; SIMPLE:        DbgEnd: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; SIMPLE:        DisplayName: foo::foo
+; SIMPLE:      }
+; SIMPLE:      RegRelativeSym {
+; SIMPLE-NEXT:   Kind: S_REGREL32 (0x1111)
+; SIMPLE:        VarName: this
+; SIMPLE:      }
+
+; MULTI:      GlobalProcIdSym {
+; MULTI:        CodeSize: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; MULTI:        DbgStart: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; MULTI:        DbgEnd: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; MULTI-LABEL: DisplayName: foo::f
+; MULTI:        LinkageName: ?f at foo@@QEAAXH at Z
+; MULTI-NOT:   RegRelativeSym {
+; MULTI:      LocalSym {
+; MULTI:        VarName: this
+; MULTI:      }
+; MULTI:      DefRangeRegisterRelSym {
+; MULTI:        Kind: S_DEFRANGE_REGISTER_REL (0x1145)
+; MULTI:        BaseRegister: RSP
+; MULTI:        LocalVariableAddrGap
+; MULTI:          GapStartOffset: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+; MULTI:        LocalVariableAddrGap
+; MULTI:          GapStartOffset: 0x{{[1-9A-Fa-f][0-9A-Fa-f]*}}
+
+target datalayout = "e-m:w-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-pc-windows-msvc19.11.25507"
+
+%class.foo = type { i32, i32 }
+
+$"\01??0foo@@QEAA at XZ" = comdat any
+$"?f at foo@@QEAAXH at Z" = comdat any
+
+; Function Attrs: noinline optnone uwtable
+define linkonce_odr void @"\01??0foo@@QEAA at XZ"(ptr %this) #0 comdat align 2 !dbg !10 {
+entry:
+  %this.addr = alloca ptr, align 8
+  store ptr %this, ptr %this.addr, align 8
+  call void @llvm.dbg.declare(metadata ptr %this.addr, metadata !11, metadata !DIExpression()), !dbg !12
+  %this1 = load ptr, ptr %this.addr, align 8
+  %a = getelementptr inbounds %class.foo, ptr %this1, i32 0, i32 0
+  store i32 1, ptr %a, align 4, !dbg !13
+  %b = getelementptr inbounds %class.foo, ptr %this1, i32 0, i32 1
+  store i32 2, ptr %b, align 4, !dbg !13
+  ret void, !dbg !13
+}
+
+declare void @exit_a(ptr)
+declare void @exit_b(ptr)
+declare void @exit_c(ptr)
+
+; Function Attrs: noinline optnone uwtable
+define linkonce_odr void @"?f at foo@@QEAAXH at Z"(ptr %this, i32 %value) #0 comdat align 2 !dbg !22 {
+entry:
+  %this.addr = alloca ptr, align 8
+  store ptr %this, ptr %this.addr, align 8
+  call void @llvm.dbg.declare(metadata ptr %this.addr, metadata !23,
+                               metadata !DIExpression()), !dbg !24
+  %negative = icmp slt i32 %value, 0, !dbg !25
+  br i1 %negative, label %exit.a, label %check.zero, !dbg !25
+
+exit.a:
+  %this.a = load ptr, ptr %this.addr, align 8, !dbg !26
+  call void @exit_a(ptr %this.a), !dbg !26
+  ret void, !dbg !26
+
+check.zero:
+  %zero = icmp eq i32 %value, 0, !dbg !27
+  br i1 %zero, label %exit.b, label %exit.c, !dbg !27
+
+exit.b:
+  %this.b = load ptr, ptr %this.addr, align 8, !dbg !28
+  call void @exit_b(ptr %this.b), !dbg !28
+  ret void, !dbg !28
+
+exit.c:
+  %this.c = load ptr, ptr %this.addr, align 8, !dbg !29
+  call void @exit_c(ptr %this.c), !dbg !29
+  ret void, !dbg !29
+}
+
+declare void @llvm.dbg.declare(metadata, metadata, metadata)
+
+attributes #0 = { noinline optnone uwtable "frame-pointer"="none" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!14, !15, !16}
+!llvm.ident = !{!17}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !1,
+  producer: "clang", isOptimized: false, runtimeVersion: 0,
+  emissionKind: FullDebug, enums: !2, retainedTypes: !2)
+!1 = !DIFile(filename: "test.cpp", directory: "C:\\src")
+!2 = !{}
+!3 = distinct !DICompositeType(tag: DW_TAG_class_type, name: "foo", file: !1,
+  line: 1, size: 64, align: 64, elements: !4, identifier: ".?AVfoo@@")
+!4 = !{!5}
+!5 = !DISubprogram(name: "foo", linkageName: "\01??0foo@@QEAA at XZ", scope: !3,
+  file: !1, line: 3, type: !6, isLocal: false, isDefinition: false,
+  scopeLine: 3, flags: DIFlagPublic | DIFlagPrototyped)
+!6 = !DISubroutineType(types: !7)
+!7 = !{null, !8}
+!8 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !3, size: 64,
+  align: 64, flags: DIFlagArtificial | DIFlagObjectPointer)
+!9 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !3, size: 64,
+  align: 64)
+!10 = distinct !DISubprogram(name: "foo", linkageName: "\01??0foo@@QEAA at XZ",
+  scope: !3, file: !1, line: 3, type: !6, isLocal: false,
+  scopeLine: 3, flags: DIFlagPublic | DIFlagPrototyped,
+  spFlags: DISPFlagDefinition, unit: !0,
+  declaration: !5, retainedNodes: !2)
+!11 = !DILocalVariable(name: "this", arg: 1, scope: !10, type: !9,
+  flags: DIFlagArtificial | DIFlagObjectPointer)
+!12 = !DILocation(line: 0, scope: !10)
+!13 = !DILocation(line: 3, column: 17, scope: !10)
+!14 = !{i32 2, !"CodeView", i32 1}
+!15 = !{i32 2, !"Debug Info Version", i32 3}
+!16 = !{i32 1, !"wchar_size", i32 2}
+!17 = !{!"clang"}
+!18 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!19 = !DISubroutineType(types: !20)
+!20 = !{null, !9, !18}
+!21 = !DISubprogram(name: "f", linkageName: "?f at foo@@QEAAXH at Z",
+  scope: !3, file: !1, line: 5, type: !19, isLocal: false,
+  isDefinition: false, scopeLine: 5,
+  flags: DIFlagPublic | DIFlagPrototyped)
+!22 = distinct !DISubprogram(name: "f", linkageName: "?f at foo@@QEAAXH at Z",
+  scope: !3, file: !1, line: 5, type: !19, isLocal: false,
+  scopeLine: 5, flags: DIFlagPublic | DIFlagPrototyped,
+  spFlags: DISPFlagDefinition, unit: !0, declaration: !21,
+  retainedNodes: !2)
+!23 = !DILocalVariable(name: "this", arg: 1, scope: !22, type: !9,
+  flags: DIFlagArtificial | DIFlagObjectPointer)
+!24 = !DILocation(line: 0, scope: !22)
+!25 = !DILocation(line: 6, column: 5, scope: !22)
+!26 = !DILocation(line: 7, column: 5, scope: !22)
+!27 = !DILocation(line: 8, column: 5, scope: !22)
+!28 = !DILocation(line: 9, column: 5, scope: !22)
+!29 = !DILocation(line: 10, column: 5, scope: !22)



More information about the llvm-commits mailing list