Skip to content

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

Description

@max-charlamb

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:

  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:

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:

  1. Resolve the existing __TypeThreadStaticIndex<type> runtime symbol.
  2. Traverse NativeAOT's storage for the specific thread.
  3. Read the field offset from the synthetic storage type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions