Skip to content

Commit 0139077

Browse files
r41k0uclaude
andcommitted
Core: Describe anonymous members in DWARF so their relocations can resolve
The previous commits emit `llvm.pt_regs:0:136$0:17:0` for `ctx.cs`, which llc happily turns into a CO-RE relocation, but the local BTF it is resolved against described member 17 as a NAMED, EMPTY composite: '_0' type_id=4 bits_offset=1088 [4] STRUCT '(anon)' size=8 vlen=0 libbpf walks a CO-RE access string by member index into the local type and then matches by field NAME in the target type, so both halves of that break it: index 0 into a vlen=0 composite has nothing to land on, and a local member called `_0` cannot be paired with the kernel's anonymous one. The object would build cleanly and then fail to load. Emit anonymous members the way a C compiler does: unnamed, with a real DW_TAG_union_type (or structure, for an anonymous struct) carrying their members in declaration order. The emitted BTF now reads '(anon)' type_id=4 bits_offset=1088 [4] UNION '(anon)' size=8 vlen=3 'cs' ... 'csx' ... 'fred_cs' ... which is the same shape clang produces for the equivalent C and the same shape `bpftool btf dump file /sys/kernel/btf/vmlinux` reports. Gated on the struct declaring `_anonymous_`, and it bails back to the old opaque member for anything it cannot describe faithfully. Across every vmlinux/xdp test program the set of emitted globals, every relocation string and every instruction are unchanged; the only diffs are the new DWARF nodes and the DWARF node renumbering they cause, in the two pt_regs programs. This is the one part of the fix that cannot be verified without loading a program, so it is kept as its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9959862 commit 0139077

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

pythonbpf/debuginfo/debug_info_generator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ def create_struct_type(
151151
is_distinct=is_distinct,
152152
)
153153

154+
def create_union_type(
155+
self, members: List[Any], size: int, is_distinct: bool
156+
) -> Any:
157+
"""Create an unnamed union type with the given members and size"""
158+
return self.module.add_debug_info(
159+
"DICompositeType",
160+
{
161+
"tag": dc.DW_TAG_union_type,
162+
"file": self.module._file_metadata,
163+
"size": size,
164+
"elements": members,
165+
},
166+
is_distinct=is_distinct,
167+
)
168+
154169
def create_struct_type_with_name(
155170
self, name: str, members: List[Any], size: int, is_distinct: bool
156171
) -> Any:

pythonbpf/vmlinux_parser/ir_gen/debug_info_gen.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,25 @@ def debug_info_generation(
5151
key=lambda item: item[1].offset,
5252
)
5353

54+
anonymous_names = set(getattr(struct.ctype_struct, "_anonymous_", None) or ())
55+
5456
for field_name, field in sorted_fields:
5557
try:
58+
if field_name in anonymous_names:
59+
# An anonymous member has to reach BTF unnamed and with its own
60+
# members, or a `$...:<n>:<m>` access string cannot be resolved
61+
# against it at load time.
62+
anonymous_member = _anonymous_member_debug_info(
63+
field, generator, generated_debug_info
64+
)
65+
if anonymous_member is not None:
66+
members.append(anonymous_member)
67+
continue
68+
logger.warning(
69+
f"Could not describe anonymous member {struct.name}.{field_name}, "
70+
"falling back to an opaque member"
71+
)
72+
5673
# Get appropriate debug type for this field
5774
field_type = _get_field_debug_type(
5875
field_name, field, generator, struct, generated_debug_info
@@ -81,6 +98,89 @@ def debug_info_generation(
8198
return struct_type
8299

83100

101+
def _lookup_generated_debug_info(
102+
type_name: str, generated_debug_info: List[Tuple[DependencyNode, Any]]
103+
):
104+
"""Find already generated debug info for a vmlinux type by name."""
105+
for existing_struct, debug_info in generated_debug_info:
106+
if existing_struct.name == type_name:
107+
return debug_info, existing_struct.__sizeof__() * 8
108+
return None
109+
110+
111+
def _anonymous_member_debug_info(
112+
field,
113+
generator: DebugInfoGenerator,
114+
generated_debug_info: List[Tuple[DependencyNode, Any]],
115+
):
116+
"""
117+
Describe an anonymous struct/union member the way a C compiler would.
118+
119+
The member itself is emitted WITHOUT a name, so it lands in BTF as `(anon)`,
120+
and its type is a real composite carrying its own members in declaration
121+
order. Both matter: libbpf walks a CO-RE access string by member index into
122+
the local type and then matches by name in the target type, so an opaque or
123+
named stand-in makes any access through the anonymous member unresolvable.
124+
125+
Returns None if the member cannot be described faithfully, in which case the
126+
caller keeps the previous behaviour.
127+
"""
128+
anonymous_type = field.type
129+
declared = getattr(anonymous_type, "_fields_", None)
130+
if not declared or not isinstance(anonymous_type, type):
131+
return None
132+
133+
inner_members = []
134+
for declared_member in declared:
135+
if len(declared_member) != 2:
136+
# Bitfield members need BTF bitfield encoding we do not emit yet.
137+
return None
138+
member_name, member_type = declared_member
139+
try:
140+
member_offset_bits = getattr(anonymous_type, member_name).offset * 8
141+
except AttributeError:
142+
return None
143+
144+
if getattr(member_type, "__module__", None) == "vmlinux":
145+
member_type_name = getattr(member_type, "__name__", None)
146+
member_debug_type = (
147+
_lookup_generated_debug_info(member_type_name, generated_debug_info)
148+
if member_type_name
149+
else None
150+
)
151+
if member_debug_type is None:
152+
# Keep the member so the indices stay right, but leave its type
153+
# opaque, exactly as nested structs are handled elsewhere.
154+
member_debug_type = (
155+
generator.create_struct_type([], 0, is_distinct=True),
156+
0,
157+
)
158+
else:
159+
member_debug_type = _get_basic_debug_type(member_type, generator)
160+
if not isinstance(member_debug_type, tuple) or len(member_debug_type) != 2:
161+
return None
162+
163+
inner_members.append(
164+
generator.create_struct_member_vmlinux(
165+
member_name, member_debug_type, member_offset_bits
166+
)
167+
)
168+
169+
size_bits = ctypes.sizeof(anonymous_type) * 8
170+
if issubclass(anonymous_type, ctypes.Union):
171+
composite = generator.create_union_type(
172+
inner_members, size_bits, is_distinct=True
173+
)
174+
else:
175+
composite = generator.create_struct_type(
176+
inner_members, size_bits, is_distinct=True
177+
)
178+
179+
return generator.create_struct_member_vmlinux(
180+
"", (composite, size_bits), field.offset * 8
181+
)
182+
183+
84184
def _get_field_debug_type(
85185
field_name: str,
86186
field,

0 commit comments

Comments
 (0)