[NativeAOT] Unix DWARF emits a process-global location for managed thread-static fields
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
## 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`:
```csharp
#: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:
```bash
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:
```text
0000000000470448 d __THREADSTATICSthreadstatic_dwarf_repro_ThreadStaticState
```
The following excerpts are abbreviated to the relevant attributes. The
thread-static field is described as:
```text
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:
1. have a location that resolves against the debugger's selected thread, or
2. 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:
```csharp
StaticOffset = fieldOffset;
StaticDataName = NodeMangler.ThreadStatics(type);
IsStaticDataInObject = 1;
```
That descriptor flows through:
```text
DwarfBuilder.GetCompleteClassTypeIndex
-> DwarfClassTypeInfo
-> DwarfStaticVariableInfo
```
`DwarfStaticVariableInfo.Dump` then emits:
```text
DW_TAG_variable
DW_AT_specification -> field declaration
DW_AT_location:
DW_OP_addr
DW_OP_deref
DW_OP_deref
DW_OP_plus_uconst
```
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:
```text
__type
t_currentThreadId DW_AT_data_member_location 0
t_currentManagedThreadId DW_AT_data_member_location 8
```
A diagnostic reader can then:
1. Resolve the existing `__TypeThreadStaticIndex` runtime symbol.
2. Traverse NativeAOT's storage for the specific thread.
3. Read the field offset from the synthetic storage type.
Contributor guide
Research direction
Start with the reproduction in threadstatic-dwarf-repro.cs, then trace UserDefinedTypeDescriptor.GetClassTypeIndex through DwarfBuilder.GetCompleteClassTypeIndex, DwarfClassTypeInfo, and DwarfStaticVariableInfo.Dump. Compare the Unix DWARF path with the Windows PDB thread-static layout, and use llvm-dwarfdump and readelf on the published binary. Done means the field location is thread-aware or the DWARF exposes enough layout information to locate it per thread.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, linux
- Domain
- compilers, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100