[llvm] [BPF] Treat class records as structs in BTF and CO-RE (PR #226723)
Maxim Skokov via llvm-commits
llvm-commits at lists.llvm.org
Sat Sep 26 11:03:11 PDT 2026
https://github.com/maxskokov created https://github.com/llvm/llvm-project/pull/226723
A C++ record declared with the `class` keyword has `DW_TAG_class_type` in the debug info, and the BPF backend only handles `DW_TAG_structure_type`:
```cpp
class C { public: int x; int y; };
C c;
unsigned long f(C *p) { return (unsigned long)__builtin_preserve_access_index(&p->y); }
```
- **BTF:** since #155783 (LLVM 22.1), `visitCompositeType()` hits `llvm_unreachable()` on a class, so compiling C++ for BPF with `-g` crashes as soon as a class is used. Before, the class was emitted as type 0 (clang 19: `Holder.c` gets `type_id=0`).
- **CO-RE:** the field offset is only computed for `DW_TAG_structure_type`, so `&p->y` above is relocated as offset 0 instead of 4 (`@"llvm.C:0:0$0:1"` with clang 19 and main).
This PR handles `DW_TAG_class_type` wherever `DW_TAG_structure_type` is handled, through one helper, so a class becomes a BTF struct (not a union) and is relocated like one. On its own it fixes classes whose elements are all data members; a class with methods, base classes or static data members also needs #226717, so neither PR makes C++ classes work without the other.
**Testing:** new tests check that a class and a declared-only class become `STRUCT` and `FWD` in BTF, that a map definition declared as a class gets the type its value points to in full rather than as `FWD`, as for a struct, and that a CO-RE access and the field info byte offset of a bit-field in a class are right; both fail on main. The kernel BPF selftests (999 C programs) compile to byte-identical code, `.BTF` and `.BTF.ext`. All BPF lit tests pass.
This is the other half of #226717: that PR handles base classes, static data members and methods in C++ records, this one records declared with `class`. They touch the same lines, so whichever lands second needs a rebase, and two checks from #226717 then need `isStructTag()` as well: `getBTFRecordElements()` in `BPFCORE.h`, and the struct/union check before the field index mapping in `computeBaseAndAccessKey()`.
This fixes a crash that is a regression since 22.1 (#155783); once this lands I'd like to request a backport to 23.x together with #226717.
Assisted-by: Claude Code (Claude Opus 5.5)
>From 21928d350518a2383b95f833408ffffe74eb5be9 Mon Sep 17 00:00:00 2001
From: Maxim Skokov <skokovmaksimevg at gmail.com>
Date: Sat, 26 Sep 2026 20:19:18 +0300
Subject: [PATCH] [BPF] Treat class records as structs in BTF and CO-RE
A C++ record declared with the class keyword has DW_TAG_class_type in
the debug info. Since #155783, visitCompositeType() hits
llvm_unreachable() on it; before, the class type was emitted as type 0.
The CO-RE pass only computes the field offset for DW_TAG_structure_type,
so an access into a class got the offset 0: for "class C { public: int
x, y; }", &c->y was relocated as offset 0 instead of 4.
Handle DW_TAG_class_type wherever DW_TAG_structure_type is handled, so
that a class becomes a BTF struct (not a union) and is relocated like
one. A class with methods, base classes or static data members also
needs #226717.
Assisted-by: Claude Code (Claude Opus 5.5)
---
.../Target/BPF/BPFAbstractMemberAccess.cpp | 12 ++-
llvm/lib/Target/BPF/BPFCORE.h | 6 ++
llvm/lib/Target/BPF/BTFDebug.cpp | 11 +--
llvm/test/CodeGen/BPF/BTF/class-type.ll | 80 +++++++++++++++++++
llvm/test/CodeGen/BPF/CORE/class-type.ll | 44 ++++++++++
5 files changed, 141 insertions(+), 12 deletions(-)
create mode 100644 llvm/test/CodeGen/BPF/BTF/class-type.ll
create mode 100644 llvm/test/CodeGen/BPF/CORE/class-type.ll
diff --git a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
index f5410820bfb494..3720c60b553a5d 100644
--- a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
+++ b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
@@ -511,13 +511,11 @@ bool BPFAbstractMemberAccess::IsValidAIChain(const MDNode *ParentType,
assert(PTy && CTy && "ParentType or ChildType is null or not composite");
uint32_t PTyTag = PTy->getTag();
- assert(PTyTag == dwarf::DW_TAG_array_type ||
- PTyTag == dwarf::DW_TAG_structure_type ||
+ assert(PTyTag == dwarf::DW_TAG_array_type || isStructTag(PTyTag) ||
PTyTag == dwarf::DW_TAG_union_type);
uint32_t CTyTag = CTy->getTag();
- assert(CTyTag == dwarf::DW_TAG_array_type ||
- CTyTag == dwarf::DW_TAG_structure_type ||
+ assert(CTyTag == dwarf::DW_TAG_array_type || isStructTag(CTyTag) ||
CTyTag == dwarf::DW_TAG_union_type);
// Multi dimensional arrays, base element should be the same
@@ -684,7 +682,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
auto *EltTy = stripQualifiers(CTy->getBaseType());
PatchImm += AccessIndex * calcArraySize(CTy, 1) *
(EltTy->getSizeInBits() >> 3);
- } else if (Tag == dwarf::DW_TAG_structure_type) {
+ } else if (isStructTag(Tag)) {
auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
if (!MemberTy->isBitField()) {
PatchImm += MemberTy->getOffsetInBits() >> 3;
@@ -919,7 +917,7 @@ Value *BPFAbstractMemberAccess::computeBaseAndAccessKey(CallInst *Call,
}
unsigned CTag = CTy->getTag();
- if (CTag == dwarf::DW_TAG_structure_type || CTag == dwarf::DW_TAG_union_type) {
+ if (isStructTag(CTag) || CTag == dwarf::DW_TAG_union_type) {
TypeName = std::string(CTy->getName());
} else {
if (HasPreserveFieldInfoCall(CallStack))
@@ -966,7 +964,7 @@ Value *BPFAbstractMemberAccess::computeBaseAndAccessKey(CallInst *Call,
auto *CTy = cast<DICompositeType>(stripQualifiers(cast<DIType>(MDN)));
uint64_t BTFIndex = AccessIndex;
- if (CTy->getTag() == dwarf::DW_TAG_structure_type) {
+ if (isStructTag(CTy->getTag())) {
DINodeArray Elements = CTy->getElements();
uint64_t Offset = getBTFRecordElementOffset(Elements[AccessIndex]);
// Find this element's position in the stable offset order without
diff --git a/llvm/lib/Target/BPF/BPFCORE.h b/llvm/lib/Target/BPF/BPFCORE.h
index 5aa7695c0ecad7..63ba3ee7eef8f3 100644
--- a/llvm/lib/Target/BPF/BPFCORE.h
+++ b/llvm/lib/Target/BPF/BPFCORE.h
@@ -21,6 +21,12 @@ class BasicBlock;
class Instruction;
class Module;
+/// Whether a debug info tag describes a record that BTF represents as a
+/// struct. A C++ record declared with the class keyword is one as well.
+inline bool isStructTag(unsigned Tag) {
+ return Tag == dwarf::DW_TAG_structure_type || Tag == dwarf::DW_TAG_class_type;
+}
+
/// Return the bit offset used to order an element of a BTF structure record.
inline uint64_t getBTFRecordElementOffset(const DINode *Element) {
switch (Element->getTag()) {
diff --git a/llvm/lib/Target/BPF/BTFDebug.cpp b/llvm/lib/Target/BPF/BTFDebug.cpp
index 7ddeedb838e870..92717b4a03af7e 100644
--- a/llvm/lib/Target/BPF/BTFDebug.cpp
+++ b/llvm/lib/Target/BPF/BTFDebug.cpp
@@ -980,7 +980,7 @@ void BTFDebug::visitStructType(const DICompositeType *CTy, bool IsStruct,
SmallVector<const DINode *, 8> Elements(DIElements.begin(), DIElements.end());
// Structure elements must have nondecreasing offsets in BTF. Preserve DI
// order for union and variant-part records.
- if (CTy->getTag() == dwarf::DW_TAG_structure_type)
+ if (isStructTag(CTy->getTag()))
llvm::stable_sort(Elements, [](const DINode *LHS, const DINode *RHS) {
return getBTFRecordElementOffset(LHS) < getBTFRecordElementOffset(RHS);
});
@@ -1127,13 +1127,14 @@ void BTFDebug::visitCompositeType(const DICompositeType *CTy,
auto Tag = CTy->getTag();
switch (Tag) {
case dwarf::DW_TAG_structure_type:
+ case dwarf::DW_TAG_class_type:
case dwarf::DW_TAG_union_type:
case dwarf::DW_TAG_variant_part:
// Handle forward declaration differently as it does not have members.
if (CTy->isForwardDecl())
visitFwdDeclType(CTy, Tag == dwarf::DW_TAG_union_type, TypeId);
else
- visitStructType(CTy, Tag == dwarf::DW_TAG_structure_type, TypeId);
+ visitStructType(CTy, isStructTag(Tag), TypeId);
break;
case dwarf::DW_TAG_array_type:
visitArrayType(CTy, TypeId);
@@ -1149,8 +1150,7 @@ void BTFDebug::visitCompositeType(const DICompositeType *CTy,
bool BTFDebug::IsForwardDeclCandidate(const DIType *Base) {
if (const auto *CTy = dyn_cast<DICompositeType>(Base)) {
auto CTag = CTy->getTag();
- if ((CTag == dwarf::DW_TAG_structure_type ||
- CTag == dwarf::DW_TAG_union_type) &&
+ if ((isStructTag(CTag) || CTag == dwarf::DW_TAG_union_type) &&
!CTy->getName().empty() && !CTy->isForwardDecl())
return true;
}
@@ -1323,7 +1323,8 @@ void BTFDebug::visitMapDefType(const DIType *Ty, uint32_t &TypeId) {
// Visit nested map array and jump to the element type
visitMapDefType(dyn_cast<DICompositeType>(Ty)->getBaseType(), TmpId);
break;
- case dwarf::DW_TAG_structure_type: {
+ case dwarf::DW_TAG_structure_type:
+ case dwarf::DW_TAG_class_type: {
// Visit all struct members to ensure their types are visited.
const auto *CTy = cast<DICompositeType>(Ty);
const DINodeArray Elements = CTy->getElements();
diff --git a/llvm/test/CodeGen/BPF/BTF/class-type.ll b/llvm/test/CodeGen/BPF/BTF/class-type.ll
new file mode 100644
index 00000000000000..c617ecfd62ced1
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/BTF/class-type.ll
@@ -0,0 +1,80 @@
+; RUN: llc -mtriple=bpfel -filetype=obj -o %t1 %s
+; RUN: llvm-objcopy --dump-section='.BTF'=%t2 %t1
+; RUN: %python %p/print_btf.py %t2 | FileCheck %s
+; RUN: %python %p/print_btf.py %t2 | FileCheck %s --check-prefix=FULL
+
+; A C++ record declared with the class keyword has DW_TAG_class_type and is
+; a BTF struct like one declared with struct, also when only declared, and a
+; map definition declared as a class is handled like a struct one: the types
+; its members point to, here Val, are emitted in full rather than as FWD.
+;
+; class C { public: int x; int y; };
+; class Fwd;
+; struct Holder { C c; Fwd *f; };
+; Holder h;
+; class Map { public: int *key; Val *value; }; // struct Val { long a; };
+; Map map __attribute__((section(".maps")));
+
+; CHECK: [1] PTR '(anon)' type_id=2
+; CHECK-NEXT: [2] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
+; CHECK-NEXT: [3] PTR '(anon)' type_id=4
+; CHECK-NEXT: [4] STRUCT 'Val' size=8 vlen=1
+; CHECK-NEXT: 'a' type_id=5 bits_offset=0
+; CHECK-NEXT: [5] INT 'long' size=8 bits_offset=0 nr_bits=64 encoding=SIGNED
+; CHECK-NEXT: [6] STRUCT 'Map' size=16 vlen=2
+; CHECK-NEXT: 'key' type_id=1 bits_offset=0
+; CHECK-NEXT: 'value' type_id=3 bits_offset=64
+; CHECK-NEXT: [7] VAR 'map' type_id=6, linkage=global
+; CHECK-NEXT: [8] STRUCT 'Holder' size=16 vlen=2
+; CHECK-NEXT: 'c' type_id=9 bits_offset=0
+; CHECK-NEXT: 'f' type_id=10 bits_offset=64
+; CHECK-NEXT: [9] STRUCT 'C' size=8 vlen=2
+; CHECK-NEXT: 'x' type_id=2 bits_offset=0
+; CHECK-NEXT: 'y' type_id=2 bits_offset=32
+; CHECK-NEXT: [10] PTR '(anon)' type_id=11
+; CHECK-NEXT: [11] FWD 'Fwd' fwd_kind=struct
+; CHECK-NEXT: [12] VAR 'h' type_id=8, linkage=global
+
+; FULL-NOT: FWD 'Val'
+; FULL: STRUCT 'Val' size=8 vlen=1
+; FULL-NOT: FWD 'Val'
+
+%struct.Holder = type { %class.C, ptr }
+%class.C = type { i32, i32 }
+
+ at h = global %struct.Holder zeroinitializer, align 8, !dbg !0
+ at map = global [2 x ptr] zeroinitializer, section ".maps", align 8, !dbg !30
+
+!llvm.dbg.cu = !{!2}
+!llvm.module.flags = !{!20, !21}
+
+!0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression())
+!1 = distinct !DIGlobalVariable(name: "h", scope: !2, file: !3, line: 4, type: !5, isLocal: false, isDefinition: true)
+!2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !3, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, globals: !4)
+!3 = !DIFile(filename: "test.cpp", directory: "/")
+!4 = !{!0, !30}
+!5 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Holder", file: !3, line: 3, size: 128, elements: !6)
+!6 = !{!7, !13}
+!7 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !5, file: !3, line: 3, baseType: !8, size: 64)
+!8 = distinct !DICompositeType(tag: DW_TAG_class_type, name: "C", file: !3, line: 1, size: 64, flags: DIFlagTypePassByValue, elements: !9)
+!9 = !{!10, !12}
+!10 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !8, file: !3, line: 1, baseType: !11, size: 32, flags: DIFlagPublic)
+!11 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !8, file: !3, line: 1, baseType: !11, size: 32, offset: 32, flags: DIFlagPublic)
+!13 = !DIDerivedType(tag: DW_TAG_member, name: "f", scope: !5, file: !3, line: 3, baseType: !14, size: 64, offset: 64)
+!14 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !15, size: 64)
+!15 = !DICompositeType(tag: DW_TAG_class_type, name: "Fwd", file: !3, line: 2, flags: DIFlagFwdDecl)
+!20 = !{i32 7, !"Dwarf Version", i32 5}
+!21 = !{i32 2, !"Debug Info Version", i32 3}
+!30 = !DIGlobalVariableExpression(var: !31, expr: !DIExpression())
+!31 = distinct !DIGlobalVariable(name: "map", scope: !2, file: !3, line: 6, type: !32, isLocal: false, isDefinition: true)
+!32 = distinct !DICompositeType(tag: DW_TAG_class_type, name: "Map", file: !3, line: 5, size: 128, elements: !33)
+!33 = !{!34, !36}
+!34 = !DIDerivedType(tag: DW_TAG_member, name: "key", scope: !32, file: !3, line: 5, baseType: !35, size: 64, flags: DIFlagPublic)
+!35 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !11, size: 64)
+!36 = !DIDerivedType(tag: DW_TAG_member, name: "value", scope: !32, file: !3, line: 5, baseType: !37, size: 64, offset: 64, flags: DIFlagPublic)
+!37 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !38, size: 64)
+!38 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Val", file: !3, line: 5, size: 64, elements: !39)
+!39 = !{!40}
+!40 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !38, file: !3, line: 5, baseType: !41, size: 64)
+!41 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
diff --git a/llvm/test/CodeGen/BPF/CORE/class-type.ll b/llvm/test/CodeGen/BPF/CORE/class-type.ll
new file mode 100644
index 00000000000000..28857e4a4d272c
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/CORE/class-type.ll
@@ -0,0 +1,44 @@
+; RUN: opt -O2 %s -S | FileCheck %s
+
+; A CO-RE access into a record declared with the class keyword
+; (DW_TAG_class_type) is relocated like one into a struct. The name of each
+; relocation global encodes <kind>:<patched value>$<access string>.
+;
+; class C { public: int x; int y; unsigned a : 3; unsigned b : 5; };
+; &c->y, and the byte offset of the storage of c->b
+
+; CHECK-DAG: @"llvm.C:0:4$0:1" =
+; CHECK-DAG: @"llvm.C:0:8$0:3" =
+
+target triple = "bpf"
+
+%class.C = type { i32, i32, i8 }
+
+define ptr @get_y(ptr %p) {
+ %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%class.C) %p, i32 1, i32 1), !llvm.preserve.access.index !3
+ ret ptr %r
+}
+
+define i32 @b_offset(ptr %p) {
+ %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%class.C) %p, i32 2, i32 3), !llvm.preserve.access.index !3
+ %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 0)
+ ret i32 %r
+}
+
+declare ptr @llvm.preserve.struct.access.index.p0.p0(ptr, i32 immarg, i32 immarg)
+declare i32 @llvm.bpf.preserve.field.info.p0(ptr, i64 immarg)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!8}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cpp", directory: "/")
+!2 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!3 = distinct !DICompositeType(tag: DW_TAG_class_type, name: "C", file: !1, size: 96, elements: !4)
+!4 = !{!5, !6, !9, !10}
+!5 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !3, file: !1, baseType: !2, size: 32, flags: DIFlagPublic)
+!6 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !3, file: !1, baseType: !2, size: 32, offset: 32, flags: DIFlagPublic)
+!8 = !{i32 2, !"Debug Info Version", i32 3}
+!9 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !3, file: !1, baseType: !11, size: 3, offset: 64, flags: DIFlagPublic | DIFlagBitField, extraData: i64 64)
+!10 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !3, file: !1, baseType: !11, size: 5, offset: 67, flags: DIFlagPublic | DIFlagBitField, extraData: i64 64)
+!11 = !DIBasicType(name: "unsigned int", size: 32, encoding: DW_ATE_unsigned)
More information about the llvm-commits
mailing list