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

via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 5 20:46:29 PDT 2026


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

>From f2e11448a60ef377f405bc3a53647d8ef86e1f0b Mon Sep 17 00:00:00 2001
From: GkvJwa <gkvjwa at gmail.com>
Date: Thu, 6 Aug 2026 11:46:17 +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 | 129 ++++++++++++++-
 llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h   |  13 ++
 .../DebugInfo/COFF/implicit-this-regrel.ll    | 154 ++++++++++++++++++
 3 files changed, 292 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..d479e7b016c09 100644
--- a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.cpp
@@ -1167,10 +1167,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");
@@ -1578,6 +1585,10 @@ void CodeViewDebug::beginFunctionImpl(const MachineFunction *MF) {
     for (const auto &MI : MBB) {
       if (!MI.isMetaInstruction() && !MI.getFlag(MachineInstr::FrameSetup) &&
           MI.getDebugLoc()) {
+        if (!CurFn->PrologueEnd) {
+          CurFn->PrologueEnd = &MI;
+          requestLabelBeforeInsn(&MI);
+        }
         PrologEndLoc = MI.getDebugLoc();
         break;
       } else if (!MI.isMetaInstruction()) {
@@ -1602,6 +1613,23 @@ void CodeViewDebug::beginFunctionImpl(const MachineFunction *MF) {
     }
   }
 
+  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 +2873,39 @@ void CodeViewDebug::emitLocalVariableList(const FunctionInfo &FI,
   }
 }
 
+bool CodeViewDebug::LocalVariable::isImplicitThis() const {
+  return DIVar->isParameter() && DIVar->isObjectPointer() &&
+         DIVar->getName() == "this";
+}
+
+bool CodeViewDebug::LocalVariable::isSimpleImplicitThis() const {
+  if (!isImplicitThis() || 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 +2930,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 (Var.isImplicitThis() && 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 +2978,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 (!Var.isImplicitThis() && !DefRange.IsSubfield &&
+                 EncFP != EncodedFramePtrReg::None &&
                  (bool(Flags & LocalSymFlags::IsParameter)
                       ? (EncFP == FI.EncodedParamFramePtrReg)
                       : (EncFP == FI.EncodedLocalFramePtrReg))) {
@@ -3092,6 +3171,48 @@ void CodeViewDebug::endFunctionImpl(const MachineFunction *MF) {
                             CurFn->Locals,
                             CurFn->Globals);
 
+  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);
+  }
+  const LocalVariable *ImplicitThis = nullptr;
+  for (const auto &ScopeVars : ScopeVariables) {
+    for (const LocalVariable &Var : ScopeVars.second) {
+      if (Var.DIVar->getScope() == GV.getSubprogram() && Var.isImplicitThis()) {
+        ImplicitThis = &Var;
+        break;
+      }
+    }
+    if (ImplicitThis)
+      break;
+  }
+  if (ImplicitThis) {
+    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;
+  }
+
   // Clear the scope and variable information from the map which will not be
   // valid after we have finished processing this routine.  This also prepares
   // the map for the subsequent routine.
diff --git a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
index e28e0ab6da36b..f9dbdec912c51 100644
--- a/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
+++ b/llvm/lib/CodeGen/AsmPrinter/CodeViewDebug.h
@@ -49,6 +49,7 @@ class GlobalVariable;
 class MCSectionCOFF;
 class MCStreamer;
 class MCSymbol;
+class MachineInstr;
 class MachineFunction;
 
 /// Collects and handles line tables information in a CodeView format.
@@ -129,6 +130,9 @@ class LLVM_LIBRARY_VISIBILITY CodeViewDebug : public DebugHandlerBase {
               SmallVector<std::pair<const MCSymbol *, const MCSymbol *>, 1>>
         DefRanges;
     std::optional<APSInt> ConstantValue;
+
+    bool isImplicitThis() const;
+    bool isSimpleImplicitThis() const;
   };
 
   struct CVGlobalVariable {
@@ -200,6 +204,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..4a0f35d1cce20
--- /dev/null
+++ b/llvm/test/DebugInfo/COFF/implicit-this-regrel.ll
@@ -0,0 +1,154 @@
+; 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:        DisplayName: foo::f
+; MULTI:      }
+; 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, unit: !0)
+!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, isDefinition: true,
+  scopeLine: 3, flags: DIFlagPublic | DIFlagPrototyped, 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 = distinct !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, unit: !0)
+!22 = distinct !DISubprogram(name: "f", linkageName: "?f at foo@@QEAAXH at Z",
+  scope: !3, file: !1, line: 5, type: !19, isLocal: false,
+  isDefinition: true, scopeLine: 5,
+  flags: DIFlagPublic | DIFlagPrototyped, 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