Description
NativeAOT's Unix DWARF describes a managed thread-static field with a
process-global DW_AT_location. The expression contains the field's offset
within its managed thread-static storage, but does not contain a thread-relative
operation and cannot identify a different storage instance for each managed
thread.
Reproduction
Reproduction
Save the following as threadstatic-dwarf-repro.cs:
#:property PublishAot=true
#:property DebugSymbols=true
#:property StripSymbols=false
#:property IlcGenerateCompleteTypeMetadata=true
using System;
using System.Threading;
Thread first = new Thread(() => ThreadStaticState.SetAndPrint(111));
Thread second = new Thread(() => ThreadStaticState.SetAndPrint(222));
first.Start();
second.Start();
first.Join();
second.Join();
public static class ThreadStaticState
{
public static object s_gc = new object();
[ThreadStatic]
public static int s_value;
public static void SetAndPrint(int value)
{
s_value = value;
Console.WriteLine($"Thread {Environment.CurrentManagedThreadId}: {s_value}, {s_gc}");
Thread.Sleep(100);
}
}
Publish and inspect the binary on Linux:
dotnet publish threadstatic-dwarf-repro.cs \
-r linux-x64 -c Debug -o threadstatic-dwarf-out
llvm-dwarfdump --name=s_value \
threadstatic-dwarf-out/threadstatic-dwarf-repro
readelf --symbols --wide threadstatic-dwarf-out/threadstatic-dwarf-repro |
grep __THREADSTATICSthreadstatic_dwarf_repro_ThreadStaticState
readelf --sections --wide threadstatic-dwarf-out/threadstatic-dwarf-repro
Tested with .NET SDK 10.0.110 and LLVM 21.1.8 on Linux x64.
Actual behavior
The backing symbol is:
0000000000470448 d __THREADSTATICSthreadstatic_dwarf_repro_ThreadStaticState
The following excerpts are abbreviated to the relevant attributes. The
thread-static field is described as:
DW_TAG_member
DW_AT_name ("s_value")
DW_AT_declaration (true)
DW_TAG_variable
DW_AT_specification (... "s_value")
DW_AT_location (
DW_OP_addr 0x470448,
DW_OP_deref,
DW_OP_deref,
DW_OP_plus_uconst 0x8)
readelf reports __THREADSTATICS... as a local symbol in the ordinary
.data PROGBITS section. It is not in .tdata or .tbss, and no TLS
relocation applies to it. The expression also has no DW_OP_form_tls_address or
other thread-relative operation. Every operation therefore reads process-global
memory and produces the same address regardless of the selected thread, even
though the program stores different s_value values on each thread.
Expected behavior
A managed thread-static field should either:
- have a location that resolves against the debugger's selected thread, or
- omit the value location and expose enough layout information for a debugger to
locate the field through NativeAOT's managed thread-static storage.
In particular, the field offset currently encoded by
DW_OP_plus_uconst 0x8 needs to be available independently from a single
process-global value location.
Cause
The incorrect representation is produced by the normal Unix static-field DWARF
pipeline.
UserDefinedTypeDescriptor.GetClassTypeIndex creates a
StaticDataFieldDescriptor for the managed thread-static field:
StaticOffset = fieldOffset;
StaticDataName = NodeMangler.ThreadStatics(type);
IsStaticDataInObject = 1;
That descriptor flows through:
DwarfBuilder.GetCompleteClassTypeIndex
-> DwarfClassTypeInfo
-> DwarfStaticVariableInfo
DwarfStaticVariableInfo.Dump then emits:
DW_TAG_variable
DW_AT_specification -> field declaration
DW_AT_location:
DW_OP_addr <StaticDataName>
DW_OP_deref
DW_OP_deref
DW_OP_plus_uconst <StaticOffset>
This is suitable for a process-global static region accessed through an
indirection. It is not suitable for a NativeAOT managed thread-static field:
StaticDataName identifies process-global metadata for the type's thread-static
storage, not a particular thread's storage object.
NativeAOT thread statics are instead reached through managed per-thread storage.
The process-global symbol does not identify a particular thread's storage, so
the resulting expression cannot represent the field's runtime location.
Possible fix
Emit a named synthetic DWARF type for the thread-static storage layout, analogous
to the type already emitted into PDBs on Windows:
__type<UnixNodeMangler.ThreadStatics(type)>
t_currentThreadId DW_AT_data_member_location 0
t_currentManagedThreadId DW_AT_data_member_location 8
A diagnostic reader can then:
- Resolve the existing
__TypeThreadStaticIndex<type> runtime symbol.
- Traverse NativeAOT's storage for the specific thread.
- Read the field offset from the synthetic storage type.
Description
NativeAOT's Unix DWARF describes a managed thread-static field with a
process-global
DW_AT_location. The expression contains the field's offsetwithin its managed thread-static storage, but does not contain a thread-relative
operation and cannot identify a different storage instance for each managed
thread.
Reproduction
Reproduction
Save the following as
threadstatic-dwarf-repro.cs:Publish and inspect the binary on Linux:
dotnet publish threadstatic-dwarf-repro.cs \ -r linux-x64 -c Debug -o threadstatic-dwarf-out llvm-dwarfdump --name=s_value \ threadstatic-dwarf-out/threadstatic-dwarf-repro readelf --symbols --wide threadstatic-dwarf-out/threadstatic-dwarf-repro | grep __THREADSTATICSthreadstatic_dwarf_repro_ThreadStaticState readelf --sections --wide threadstatic-dwarf-out/threadstatic-dwarf-reproTested with .NET SDK 10.0.110 and LLVM 21.1.8 on Linux x64.
Actual behavior
The backing symbol is:
The following excerpts are abbreviated to the relevant attributes. The
thread-static field is described as:
readelfreports__THREADSTATICS...as a local symbol in the ordinary.dataPROGBITSsection. It is not in.tdataor.tbss, and no TLSrelocation applies to it. The expression also has no
DW_OP_form_tls_addressorother thread-relative operation. Every operation therefore reads process-global
memory and produces the same address regardless of the selected thread, even
though the program stores different
s_valuevalues on each thread.Expected behavior
A managed thread-static field should either:
locate the field through NativeAOT's managed thread-static storage.
In particular, the field offset currently encoded by
DW_OP_plus_uconst 0x8needs to be available independently from a singleprocess-global value location.
Cause
The incorrect representation is produced by the normal Unix static-field DWARF
pipeline.
UserDefinedTypeDescriptor.GetClassTypeIndexcreates aStaticDataFieldDescriptorfor the managed thread-static field:That descriptor flows through:
DwarfStaticVariableInfo.Dumpthen emits:This is suitable for a process-global static region accessed through an
indirection. It is not suitable for a NativeAOT managed thread-static field:
StaticDataNameidentifies process-global metadata for the type's thread-staticstorage, not a particular thread's storage object.
NativeAOT thread statics are instead reached through managed per-thread storage.
The process-global symbol does not identify a particular thread's storage, so
the resulting expression cannot represent the field's runtime location.
Possible fix
Emit a named synthetic DWARF type for the thread-static storage layout, analogous
to the type already emitted into PDBs on Windows:
A diagnostic reader can then:
__TypeThreadStaticIndex<type>runtime symbol.