dotnet / dotnet/runtime

[NativeAOT] Unix DWARF emits a process-global location for managed thread-static fields

Open
#132,237 1 comment 0 reactions 0 assignees View on GitHub
area-NativeAOT-coreclr untriaged
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.