Generated banner for Apple Crash Symbolication
Generated logo for Apple Crash Symbolication

Apple Crash Symbolication

.NET (Microsoft)OfficialPublished by .NET (Microsoft)

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Symbolicate .NET runtime frames in Apple platform .ips crash logs (iOS, tvOS, Mac Catalyst, macOS). Extracts UUIDs and addresses from the native backtrace, locates dSYM debug symbols, and runs atos to produce function names with source file and line numbers. Automatically downloads .dwarf symbols from the Microsoft symbol server using Mach-O UUIDs. USE FOR triaging a .NET MAUI or Mono app crash from an .ips file on any Apple platform, resolving native backtrace frames in libcoreclr or libmonosgen-2.0 to .NET runtime source code, retrieving .ips crash logs from a connected iOS device or iPhone, or investigating EXC_CRASH, EXC_BAD_ACCESS, SIGABRT, or SIGSEGV originating from the .NET runtime. DO NOT USE FOR pure Swift/Objective-C crashes with no .NET components, or Android tombstone files. INVOKES Symbolicate-Crash.ps1 script, atos, dwarfdump, idevicecrashreport.
— the description by .NET (Microsoft)

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured workflow for triaging and symbolicating .NET MAUI and Mono application crashes on Apple platforms (iOS, tvOS, Mac Catalyst, macOS). It guides users through parsing .ips JSON crash logs, identifying .NET runtime libraries, locating necessary dSYM debug symbols via the Microsoft symbol server or local build artifacts, and using the atos tool to resolve native backtrace frames into readable source code references.

What it does

  • Parses .ips JSON crash logs to extract metadata and backtrace information.
  • Identifies .NET runtime libraries within native backtraces.
  • Automates the download of .dwarf symbols from the Microsoft symbol server using Mach-O UUIDs.
  • Provides instructions for using atos to map memory addresses to source files and line numbers.
  • Includes a PowerShell script to automate the entire symbolication process.
  • Offers guidance on retrieving crash logs from connected iOS devices.

When to use it

  • When investigating .NET MAUI or Mono app crashes on Apple platforms.
  • When you have an .ips crash log and need to resolve native backtrace frames to .NET source code.
  • When debugging EXC_CRASH, EXC_BAD_ACCESS, SIGABRT, or SIGSEGV errors originating from the .NET runtime.
  • When you need to verify if a crash is related to managed exception handling.

Advantages

  • Streamlines the complex process of mapping native Apple crash logs to .NET source code.
  • Automates symbol retrieval from official Microsoft servers.
  • Reduces manual effort by providing a ready-to-use automation script.
  • Helps distinguish between .NET runtime issues and native Apple framework crashes.

Usage examples

“How do I symbolicate a .NET MAUI crash log from my iPhone?”
The agent will guide you to use the Symbolicate-Crash.ps1 script, pointing you to the .ips file and explaining how to ensure the necessary dSYM files are accessible.
“I have an .ips file for a macOS app crash, how do I find the source line?”
The agent will instruct you to parse the .ips file, identify the faulting thread, and use the atos command with the correct dSYM bundle to resolve the stack trace.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/dotnet/apple-crash-symbolication/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip apple-crash-symbolication.zip -d .agents/skills

Documentation by .NET (Microsoft)

Apple Platform Crash Log .NET Symbolication

Resolves native backtrace frames from .NET MAUI and Mono app crashes on Apple platforms (iOS, tvOS, Mac Catalyst, macOS) to function names, source files, and line numbers using Mach-O UUIDs and dSYM debug symbol bundles.

Inputs: Crash log file (.ips JSON format, iOS 15+ / macOS 12+), atos (from Xcode), optionally a connected iOS device to pull crash logs from.

Do not use when: The crashing library is not a .NET component (e.g., pure Swift/UIKit), or the crash log is an Android tombstone.


Workflow

Step 1: Parse the .ips Crash Log

Format check: Before proceeding, verify the file is .ips JSON format. The first line must be valid JSON. If the file is plain text (e.g., Android tombstone with #NN pc frame lines, or legacy Apple .crash text format), stop immediately — this workflow does not apply. Report the format mismatch to the user and do not attempt any symbolication.

The .ips file is two-part JSON: line 1 is a metadata header; the remaining lines are a separate JSON crash body. Parse them separately:

lines = open('crash.ips').readlines()
metadata = json.loads(lines[0])           # app_name, bundleID, os_version, slice_uuid
crash    = json.loads(''.join(lines[1:])) # Full crash report

Key fields in the crash body:

  • usedImages[N] has name, base (load address), uuid, arch for each loaded binary

  • threads[N].frames[M] has imageOffset, imageIndex; frame address = usedImages[imageIndex].base + imageOffset

  • exception.type, exception.signal (e.g., EXC_CRASH / SIGABRT)

  • asi (Application Specific Information) often contains the managed exception message

  • lastExceptionBacktrace has frames from the exception that triggered the crash

  • faultingThread is the index into the threads array

Parsing gotcha: Some .ips files have case-conflicting duplicate keys (vmRegionInfo / vmregioninfo). Pre-process the raw JSON to rename the lowercase duplicate before parsing. The asi field may be absent.

Step 2: Identify .NET Runtime Libraries

Filter usedImages to .NET runtime libraries:

LibraryRuntime
libcoreclrCoreCLR runtime
libmonosgen-2.0Mono runtime
libSystem.Native.NET BCL native component
libSystem.Globalization.Native.NET BCL globalization
libSystem.Security.Cryptography.Native.Apple.NET BCL crypto
libSystem.IO.Compression.Native.NET BCL compression
libSystem.Net.Security.Native.NET BCL net security

On Apple platforms these ship as .framework bundles, so image names may omit .dylib. Match using substring (e.g., libcoreclr not libcoreclr.dylib). The app binary may appear twice in usedImages with different UUIDs.

Key bridge functions in the app binary: xamarin_process_managed_exception (managed exception bridged to ObjC NSException), xamarin_main, mono_jit_exec, coreclr_execute_assembly.

NativeAOT: Runtime is statically linked into the app binary. libSystem.* BCL libraries remain separate. The app binary needs its own dSYM from the build output.

Skip libsystem_kernel.dylib, UIKitCore, and other Apple system frameworks unless specifically asked.

Step 3: Interpret the Crash

Start with asi (Application Specific Information) — for .NET crashes, it often contains the managed exception type and message (e.g., XamlParseException, NullReferenceException). The root cause may already be visible here.

Then examine the faulting thread (threads[faultingThread]). Explain what frames #0 and #1 mean before examining other threads. Cross-thread context (GC state, thread pool) is useful for validation but not evidence of causation.

Also check lastExceptionBacktrace for the managed exception path through bridge functions like xamarin_process_managed_exception.

Sometimes the .NET runtime version is visible in image paths in usedImages, particularly on macOS when using shared-framework installs or NuGet-pack-style layouts (e.g., .../Microsoft.NETCore.App/10.0.4/libcoreclr.dylib). On iOS, however, image paths are typically inside the app bundle (for example, .../Frameworks/libcoreclr.framework/libcoreclr) and do not embed the runtime version, so you usually need to infer it via the Mach-O UUID by matching against SDK packs or symbol-server downloads rather than relying on the path alone.

Step 4: Locate dSYMs

For each .NET library needing symbolication, locate a UUID-matched dSYM:

  1. Microsoft symbol server (automatic): Download .dwarf via https://msdl.microsoft.com/download/symbols/_.dwarf/mach-uuid-sym-{UUID}/_.dwarf (UUID lowercase, no dashes). Convert to .dSYM bundle (use the image name from usedImages[].name, e.g., libcoreclr):

    mkdir -p libcoreclr.dSYM/Contents/Resources/DWARF
    cp _.dwarf libcoreclr.dSYM/Contents/Resources/DWARF/libcoreclr
  2. Build output: bin/Debug/net*-ios/ios-arm64/<App>.app.dSYM/

  3. SDK packs: $DOTNET_ROOT/packs/Microsoft.NETCore.App.Runtime.<rid>/<version>/runtimes/<rid>/native/

  4. NuGet cache: ~/.nuget/packages/microsoft.netcore.app.runtime.<rid>/<version>/runtimes/<rid>/native/

  5. dotnet-symbol: dotnet-symbol --symbols -o symbols-out <path-to-binary.dylib>

Always verify: dwarfdump --uuid <dsym> must match the UUID from the crash log exactly.

Step 5: Symbolicate with atos
atos -arch arm64 -o <path.dSYM/Contents/Resources/DWARF/binary_name> -l <load_address> <frame_addresses...>
  • -o points to the DWARF binary inside the .dSYM bundle (Contents/Resources/DWARF/), not the bundle itself

  • -l is the load address from usedImages[N].base

  • Use the arch from usedImages[N].arch (usually arm64, may be arm64e)

  • Pass multiple addresses per invocation for batch symbolication

# Example: symbolicate libcoreclr frames
atos -arch arm64 -o libcoreclr.dSYM/Contents/Resources/DWARF/libcoreclr -l 0x104000000 0x104522098 0x1043c0014

Strip the /__w/1/s/ CI workspace prefix from output — meaningful paths start at src/runtime/, mapping to the dotnet/dotnet VMR.

Automation Script

scripts/Symbolicate-Crash.ps1 automates the full workflow (parsing, dSYM lookup, symbol download, and symbolication). Resolve the path relative to this SKILL.md file.

# $SKILL_DIR is the directory containing this SKILL.md
pwsh "$SKILL_DIR/scripts/Symbolicate-Crash.ps1" -CrashFile MyApp-2026-02-25.ips

Start with -ParseOnly for a fast overview without requiring atos. The script automatically downloads symbols from the Microsoft symbol server when local dSYMs are missing.

Flags: -CrashingThreadOnly, -OutputFile path, -ParseOnly, -SkipVersionLookup, -SkipSymbolDownload, -SymbolCacheDir path, -DsymSearchPaths path1,path2.


Retrieving Crash Logs

Pull crash logs from a connected iOS device using idevicecrashreport (from libimobiledevice):

idevicecrashreport -e /tmp/crashlogs/
find /tmp/crashlogs/ -iname '*MyApp*' -name '*.ips'

Also available in Xcode > Window > Devices and Simulators > View Device Logs, or at ~/Library/Logs/CrashReporter/ (Mac Catalyst), ~/Library/Logs/DiagnosticReports/ (macOS).


Validation

  1. dwarfdump --uuid <dsym> matches UUID from the crash log

  2. At least one .NET frame resolves to a function name (not a raw address)

  3. Resolved paths contain recognizable .NET runtime structure (e.g., src/coreclr/, mono/metadata/, mono/mini/)

Stop Signals

  • Wrong file format: If the file is not .ips JSON (e.g., Android tombstone with #NN pc stack frames, legacy .crash text format), stop immediately — report the format mismatch to the user and do not proceed with any symbolication. Do not attempt to symbolicate using other tools or workflows.

  • No .NET frames found: Report parsed frames and stop.

  • All frames resolved: Present symbolicated backtrace with brief crash analysis (faulting thread, exception type, likely area). If the user asks for deeper investigation, proceed.

  • dSYM not available / UUID mismatch: Report unsymbolicated frames with UUIDs and addresses. Suggest locating the original build artifacts.

  • atos not available: Present the manual atos commands for the user to run. Do not install Xcode. atos ships with Xcode Command Line Tools (xcode-select --install).

References

Files

scripts/Symbolicate-Crash.ps1

<#
.SYNOPSIS
    Symbolicates .NET runtime frames in an Apple platform .ips crash log.

.DESCRIPTION
    Parses an Apple platform .ips crash log (JSON format, iOS 15+ / macOS 12+), extracts
    Mach-O UUIDs and frame addresses from the native backtrace, locates dSYM debug symbols
    from local SDK packs and NuGet cache, downloads missing symbols from the Microsoft
    symbol server, and runs atos to resolve each frame to function name, source file, and
    line number. Supports iOS, tvOS, Mac Catalyst, and macOS.

.PARAMETER CrashFile
    Path to the .ips crash log file.

.PARAMETER Atos
    Path to atos. Defaults to 'atos' (assumes Xcode Command Line Tools are installed).

.PARAMETER DsymSearchPaths
    Additional directories to search for dSYM bundles. Searched before SDK packs and
    NuGet cache.

.PARAMETER OutputFile
    Optional path to write the symbolicated backtrace. If omitted, writes to stdout.

.PARAMETER CrashingThreadOnly
    Limit symbolication to the faulting thread only.

.PARAMETER ParseOnly
    Parse the crash log and report libraries, UUIDs, and frame addresses without
    symbolicating. Useful when atos or dSYMs are not available.

.PARAMETER SkipVersionLookup
    Skip .NET runtime version identification.

.PARAMETER SymbolCacheDir
    Directory to cache downloaded symbol files. Defaults to a temp directory.

.PARAMETER SymbolServerUrl
    Base URL for the symbol server. Defaults to Microsoft's public server.

.PARAMETER SkipSymbolDownload
    Skip downloading symbols from the symbol server. Only use locally-found dSYMs.

.EXAMPLE
    pwsh Symbolicate-Crash.ps1 -CrashFile MyApp-2026-02-25.ips

.EXAMPLE
    pwsh Symbolicate-Crash.ps1 -CrashFile MyApp.ips -DsymSearchPaths ./build/dSYMs -OutputFile symbolicated.txt

.EXAMPLE
    pwsh Symbolicate-Crash.ps1 -CrashFile MyApp.ips -SymbolCacheDir ./symbol-cache
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$CrashFile,

    [Parameter()]
    [string]$Atos = 'atos',

    [Parameter()]
    [string[]]$DsymSearchPaths = @(),

    [Parameter()]
    [string]$OutputFile,

    [Parameter()]
    [switch]$CrashingThreadOnly,

    [Parameter()]
    [switch]$ParseOnly,

    [Parameter()]
    [switch]$SkipVersionLookup,

    [Parameter()]
    [string]$SymbolCacheDir,

    [Parameter()]
    [string]$SymbolServerUrl = 'https://msdl.microsoft.com/download/symbols',

    [Parameter()]
    [switch]$SkipSymbolDownload
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# .NET runtime libraries on Apple platforms (framework bundles omit .dylib extension)
$dotnetLibraries = @(
    'libcoreclr'
    'libmonosgen-2.0'
    'libSystem.Native'
    'libSystem.Globalization.Native'
    'libSystem.Security.Cryptography.Native.Apple'
    'libSystem.IO.Compression.Native'
    'libSystem.Net.Security.Native'
)

# All Apple platform RIDs to search for dSYMs and runtime packs
$appleRids = @(
    'ios-arm64'
    'iossimulator-arm64'
    'iossimulator-x64'
    'tvos-arm64'
    'tvossimulator-arm64'
    'tvossimulator-x64'
    'maccatalyst-arm64'
    'maccatalyst-x64'
    'osx-arm64'
    'osx-x64'
)

function Test-DotNetLibrary([string]$imageName) {
    foreach ($lib in $dotnetLibraries) {
        if ($imageName -like "*$lib*") { return $true }
    }
    return $false
}

# Normalize UUID for comparison: lowercase, no dashes
function Format-Uuid([string]$uuid) {
    return ($uuid -replace '-', '').ToLowerInvariant()
}

# Parse .ips crash log (two-part JSON: line 1 = metadata, lines 2+ = crash body)
function Read-IpsCrashLog([string]$path) {
    $lines = Get-Content $path -Raw
    $splitIndex = $lines.IndexOf("`n")
    if ($splitIndex -lt 0) {
        Write-Error "Invalid .ips file: expected multi-line JSON format"
        exit 1
    }

    $metadataJson = $lines.Substring(0, $splitIndex)
    $bodyJson = $lines.Substring($splitIndex + 1)

    try {
        $metadata = $metadataJson | ConvertFrom-Json
    }
    catch {
        Write-Error "Failed to parse .ips metadata (line 1): $_"
        exit 1
    }

    try {
        # Some .ips files contain case-variant duplicate keys (e.g. vmRegionInfo
        # and vmregioninfo) which ConvertFrom-Json rejects. Rename the lowercase
        # duplicate so parsing succeeds while preserving the camelCase variant.
        if ($bodyJson -match '"vmRegionInfo"\s*:' -and $bodyJson -match '"vmregioninfo"\s*:') {
            $bodyJson = $bodyJson -creplace '"vmregioninfo"\s*:', '"_vmregioninfo_dup":'
        }
        $body = $bodyJson | ConvertFrom-Json
    }
    catch {
        Write-Error "Failed to parse .ips crash body (lines 2+): $_"
        exit 1
    }

    return @{ Metadata = $metadata; Body = $body }
}

# Build a lookup table of images from usedImages[]
function Get-ImageTable($crashBody) {
    $images = @()
    $usedImages = $crashBody.usedImages
    if (-not $usedImages) { return $images }

    for ($i = 0; $i -lt $usedImages.Count; $i++) {
        $img = $usedImages[$i]
        $imgPath = if ($img.PSObject.Properties['path'] -and $img.path) { $img.path } else { $null }
        $imgName = if ($img.PSObject.Properties['name'] -and $img.name) { $img.name }
                   elseif ($imgPath) { [System.IO.Path]::GetFileName($imgPath) }
                   else { $null }
        # Skip sentinel/empty entries (e.g. null UUID, no name or path)
        if (-not $imgName) { continue }
        $images += [PSCustomObject]@{
            Index     = $i
            Name      = $imgName
            Path      = $imgPath
            Base      = [uint64]$img.base
            Uuid      = Format-Uuid $img.uuid
            Arch      = if ($img.arch) { $img.arch } else { 'arm64' }
            IsDotNet  = (Test-DotNetLibrary $imgName)
        }
    }
    return $images
}

# Extract frames from a thread, computing absolute addresses from imageOffset + base
function Get-ThreadFrames($thread, $images) {
    $frames = @()
    if (-not $thread.frames) { return $frames }

    $imageMap = @{}
    foreach ($img in $images) { $imageMap[$img.Index] = $img }

    foreach ($f in $thread.frames) {
        $imgIdx = [int]$f.imageIndex
        $offset = [uint64]$f.imageOffset
        $img = $imageMap[$imgIdx]

        if ($img) {
            $address = $img.Base + $offset
            $frames += [PSCustomObject]@{
                ImageIndex  = $imgIdx
                ImageName   = $img.Name
                ImagePath   = $img.Path
                ImageUuid   = $img.Uuid
                ImageArch   = $img.Arch
                LoadAddress = $img.Base
                Offset      = $offset
                Address     = $address
                AddressHex  = '0x{0:x}' -f $address
                IsDotNet    = $img.IsDotNet
            }
        }
    }
    return $frames
}

# Search for a dSYM matching a given UUID
function Find-Dsym([string]$uuid, [string]$libraryName, [string[]]$extraPaths) {
    # Build list of search directories
    $searchDirs = @()

    # 1. User-provided paths
    $searchDirs += $extraPaths

    # 2. SDK packs
    $dotnetRoot = if ($env:DOTNET_ROOT) { $env:DOTNET_ROOT }
                  elseif (Test-Path (Join-Path $HOME '.dotnet')) { Join-Path $HOME '.dotnet' }
                  else { $null }
    if ($dotnetRoot) {
        $packPatterns = @()
        foreach ($rid in $script:appleRids) {
            $packPatterns += "packs/Microsoft.NETCore.App.Runtime.$rid/*/runtimes/$rid/native"
            $packPatterns += "packs/Microsoft.NETCore.App.Runtime.Mono.$rid/*/runtimes/$rid/native"
        }
        foreach ($pattern in $packPatterns) {
            $searchDirs += @(Get-Item (Join-Path $dotnetRoot $pattern) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
        }
    }

    # 3. NuGet cache
    $nugetDir = if ($env:NUGET_PACKAGES) { $env:NUGET_PACKAGES }
                else { Join-Path $HOME '.nuget/packages' }
    $nugetPatterns = @()
    foreach ($rid in $script:appleRids) {
        $nugetPatterns += "microsoft.netcore.app.runtime.$rid/*/runtimes/$rid/native"
        $nugetPatterns += "microsoft.netcore.app.runtime.mono.$rid/*/runtimes/$rid/native"
    }
    foreach ($pattern in $nugetPatterns) {
        $searchDirs += @(Get-Item (Join-Path $nugetDir $pattern) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
    }

    # Search each directory for .dSYM bundles or bare DWARF files
    foreach ($dir in $searchDirs) {
        if (-not $dir -or -not (Test-Path $dir)) { continue }

        # Look for dSYM bundles matching the library name
        $dsymBundles = Get-ChildItem $dir -Filter '*.dSYM' -Directory -Recurse -ErrorAction SilentlyContinue
        foreach ($bundle in $dsymBundles) {
            if ($bundle.Name -notlike "*$libraryName*") { continue }

            $dwarfDir = Join-Path $bundle.FullName 'Contents/Resources/DWARF'
            if (-not (Test-Path $dwarfDir)) { continue }

            $dwarfFiles = Get-ChildItem $dwarfDir -File -ErrorAction SilentlyContinue
            foreach ($dwarfFile in $dwarfFiles) {
                $dsymUuid = Get-DsymUuid $dwarfFile.FullName
                if ($dsymUuid -and (Format-Uuid $dsymUuid) -eq $uuid) {
                    return $dwarfFile.FullName
                }
            }
        }

        # Also check for bare dylib/framework files (may have embedded DWARF)
        $candidates = Get-ChildItem $dir -Filter "$libraryName*" -File -Recurse -ErrorAction SilentlyContinue
        foreach ($candidate in $candidates) {
            $dsymUuid = Get-DsymUuid $candidate.FullName
            if ($dsymUuid -and (Format-Uuid $dsymUuid) -eq $uuid) {
                return $candidate.FullName
            }
        }
    }

    return $null
}

# Get UUID from a dSYM or Mach-O binary using dwarfdump
function Get-DsymUuid([string]$path) {
    try {
        $output = & dwarfdump --uuid $path 2>$null
        if ($output -match 'UUID:\s*([0-9A-Fa-f-]+)') {
            return $Matches[1]
        }
    }
    catch {
        Write-Verbose "dwarfdump failed for $path`: $_"
    }
    return $null
}

# Download DWARF symbols from the Microsoft symbol server using a Mach-O UUID
function Get-DebugSymbols([string]$uuid, [string]$cacheDir, [string]$serverUrl) {
    $dwarfFile = Join-Path $cacheDir "$uuid.dwarf"

    if (Test-Path $dwarfFile) {
        Write-Verbose "Using cached symbols for UUID $uuid"
        return $dwarfFile
    }

    $url = "$serverUrl/_.dwarf/mach-uuid-sym-$uuid/_.dwarf"
    Write-Verbose "Downloading symbols from $url"

    $savedProgressPreference = $ProgressPreference
    try {
        $ProgressPreference = 'SilentlyContinue'
        Invoke-WebRequest -Uri $url -OutFile $dwarfFile -TimeoutSec 120

        # Verify the download is a Mach-O file (64-bit LE magic: CF FA ED FE)
        $stream = [System.IO.File]::OpenRead($dwarfFile)
        try {
            $header = [byte[]]::new(4)
            $bytesRead = $stream.Read($header, 0, 4)
        }
        finally {
            $stream.Close()
        }
        if ($bytesRead -ge 4 -and $header[0] -eq 0xCF -and $header[1] -eq 0xFA -and $header[2] -eq 0xED -and $header[3] -eq 0xFE) {
            $size = (Get-Item $dwarfFile).Length
            Write-Verbose "Downloaded $([math]::Round($size / 1MB, 1)) MB symbols for UUID $uuid"
            return $dwarfFile
        }
        else {
            Write-Warning "Downloaded file for UUID $uuid is not a valid Mach-O file (symbols may not be published)"
            Remove-Item $dwarfFile -ErrorAction SilentlyContinue
            return $null
        }
    }
    catch {
        Write-Warning "Failed to download symbols for UUID $uuid`: $_"
        Remove-Item $dwarfFile -ErrorAction SilentlyContinue
        return $null
    }
    finally {
        $ProgressPreference = $savedProgressPreference
    }
}

# Convert a raw .dwarf file into a .dSYM bundle that atos can consume
function Convert-DwarfToDsym([string]$dwarfFile, [string]$libraryName, [string]$uuid, [string]$cacheDir) {
    # Sanitize library name to prevent path traversal
    $safeName = [System.IO.Path]::GetFileName($libraryName)
    $dsymBundle = Join-Path $cacheDir "$safeName-$uuid.dSYM"
    $dwarfDir = Join-Path $dsymBundle 'Contents/Resources/DWARF'
    $targetFile = Join-Path $dwarfDir $safeName

    if (Test-Path $targetFile) {
        # Verify that the cached dSYM matches the requested UUID
        $cachedUuid = Get-DsymUuid $targetFile
        if ($cachedUuid -and (Format-Uuid $cachedUuid) -eq $uuid) {
            Write-Verbose "Using cached dSYM bundle for $libraryName"
            return $targetFile
        }

        Write-Warning "Cached dSYM bundle for $libraryName has incorrect or unreadable UUID; recreating"
        try {
            Remove-Item $dsymBundle -Recurse -Force -ErrorAction SilentlyContinue
        }
        catch {
            Write-Warning "Failed to remove invalid cached dSYM bundle for ${libraryName}: $_"
        }
    }

    try {
        New-Item -ItemType Directory -Path $dwarfDir -Force | Out-Null
        Copy-Item -Path $dwarfFile -Destination $targetFile -Force

        # Verify UUID is readable from the converted bundle
        $verifyUuid = Get-DsymUuid $targetFile
        if (-not $verifyUuid) {
            Write-Warning "Converted dSYM for $libraryName failed UUID verification"
            Remove-Item $dsymBundle -Recurse -Force -ErrorAction SilentlyContinue
            return $null
        }

        Write-Verbose "Created dSYM bundle: $dsymBundle"
        return $targetFile
    }
    catch {
        Write-Warning "Failed to create dSYM bundle for $libraryName`: $_"
        Remove-Item $dsymBundle -Recurse -Force -ErrorAction SilentlyContinue
        return $null
    }
}

# Symbolicate a batch of addresses for one image using atos
function Resolve-Frames([string]$dsymPath, [string]$arch, [uint64]$loadAddress, [uint64[]]$addresses, [string]$atosPath) {
    $loadHex = '0x{0:x}' -f $loadAddress
    $addrArgs = $addresses | ForEach-Object { '0x{0:x}' -f $_ }

    try {
        $output = & $atosPath -arch $arch -o $dsymPath -l $loadHex @addrArgs 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $output) { return @() }

        $results = [System.Collections.Generic.List[object]]::new()
        $lines = @($output)
        for ($i = 0; $i -lt [Math]::Min($lines.Count, $addresses.Count); $i++) {
            $line = $lines[$i].Trim()
            # atos returns "0xADDRESS" for unresolved frames
            if ($line -match '^0x[0-9a-fA-F]+$') {
                $results.Add($null)
            }
            else {
                # Parse: "functionName (in libraryName) (sourcefile:line)"
                $funcName = $line
                $source = $null
                if ($line -match '^(.+?)\s+\(in\s+.+?\)\s+\((.+)\)$') {
                    $funcName = $Matches[1]
                    $source = $Matches[2]
                    # Strip CI build agent path prefixes
                    $source = $source -replace '^/__w/\d+/s/', ''
                }
                elseif ($line -match '^(.+?)\s+\(in\s+.+?\)$') {
                    $funcName = $Matches[1]
                }
                $results.Add([PSCustomObject]@{
                    Function = $funcName
                    Source   = $source
                })
            }
        }
        return $results.ToArray()
    }
    catch {
        Write-Verbose "atos failed: $_"
        return @()
    }
}

# Extract .NET runtime version from a crash log image path.
# macOS paths: /usr/local/share/dotnet/shared/Microsoft.NETCore.App/10.0.4/libcoreclr.dylib
# host/fxr:    /usr/local/share/dotnet/host/fxr/10.0.4/libhostfxr.dylib
# Mono:        /usr/local/share/dotnet/shared/Microsoft.NETCore.App/10.0.4/libmonosgen-2.0.dylib
function Get-RuntimeVersionFromPath([string]$imagePath) {
    if (-not $imagePath) { return $null }

    # Pattern: .../shared/Microsoft.NETCore.App/<version>/... or .../host/fxr/<version>/...
    # Version pattern: digits.digits.digits with optional pre-release suffix
    $patterns = @(
        '(?:shared|packs)/Microsoft\.NETCore\.App(?:\.Runtime\.[^/]+)?/([0-9]+\.[0-9]+\.[0-9]+[^/]*)/'
        'host/fxr/([0-9]+\.[0-9]+\.[0-9]+[^/]*)/'
    )

    foreach ($pat in $patterns) {
        if ($imagePath -match $pat) {
            return $Matches[1]
        }
    }
    return $null
}

# Detect the platform RID from a crash log image path.
# e.g., .../runtimes/osx-arm64/native/... → osx-arm64
function Get-RidFromPath([string]$imagePath) {
    if (-not $imagePath) { return $null }

    # NuGet layout: .../runtimes/<rid>/native/...
    if ($imagePath -match 'runtimes/([a-z]+-[a-z0-9]+)/native/') {
        return $Matches[1]
    }

    # macOS shared framework — infer from OS name and arch in the crash metadata
    if ($imagePath -match '/usr/local/share/dotnet/' -or $imagePath -match '\.dotnet/') {
        return $null  # caller infers from crash metadata
    }
    return $null
}

# Try to identify the .NET runtime version by matching a UUID against locally-installed packs
function Find-RuntimeVersion([string]$uuid, [string]$libraryName) {
    $packNames = @()
    foreach ($rid in $script:appleRids) {
        if ($libraryName -like '*monosgen*') {
            $packNames += "Microsoft.NETCore.App.Runtime.Mono.$rid"
        }
        elseif ($libraryName -like '*coreclr*') {
            $packNames += "Microsoft.NETCore.App.Runtime.$rid"
        }
        else {
            $packNames += "Microsoft.NETCore.App.Runtime.Mono.$rid"
            $packNames += "Microsoft.NETCore.App.Runtime.$rid"
        }
    }

    $searchRoots = @()

    # SDK packs
    $dotnetRoot = if ($env:DOTNET_ROOT) { $env:DOTNET_ROOT }
                  elseif (Test-Path (Join-Path $HOME '.dotnet')) { Join-Path $HOME '.dotnet' }
                  else { $null }
    if ($dotnetRoot) {
        foreach ($pn in $packNames) {
            $p = Join-Path $dotnetRoot "packs/$pn"
            if (Test-Path $p) { $searchRoots += $p }
        }
    }

    # NuGet cache
    $nugetDir = if ($env:NUGET_PACKAGES) { $env:NUGET_PACKAGES }
                else { Join-Path $HOME '.nuget/packages' }
    foreach ($pn in $packNames) {
        $p = Join-Path $nugetDir $pn.ToLowerInvariant()
        if (Test-Path $p) { $searchRoots += $p }
    }

    foreach ($root in $searchRoots) {
        foreach ($versionDir in (Get-ChildItem $root -Directory -ErrorAction SilentlyContinue)) {
            # Find the native directory — each pack has one RID under runtimes/
            $nativeDir = $null
            $runtimesDir = Join-Path $versionDir.FullName 'runtimes'
            if (Test-Path $runtimesDir) {
                foreach ($ridDir in (Get-ChildItem $runtimesDir -Directory -ErrorAction SilentlyContinue)) {
                    $nd = Join-Path $ridDir.FullName 'native'
                    if (Test-Path $nd) { $nativeDir = $nd; break }
                }
            }
            if (-not $nativeDir) { continue }

            # Check dSYM bundles first, then bare binaries
            $candidates = @()
            $dsymBundles = Get-ChildItem $nativeDir -Filter '*.dSYM' -Directory -ErrorAction SilentlyContinue
            foreach ($bundle in $dsymBundles) {
                if ($bundle.Name -like "*$libraryName*") {
                    $dwarfDir = Join-Path $bundle.FullName 'Contents/Resources/DWARF'
                    $candidates += @(Get-ChildItem $dwarfDir -File -ErrorAction SilentlyContinue)
                }
            }
            $candidates += @(Get-ChildItem $nativeDir -Filter "$libraryName*" -File -ErrorAction SilentlyContinue)

            foreach ($candidate in $candidates) {
                $localUuid = Get-DsymUuid $candidate.FullName
                if (-not $localUuid) { continue }
                if ((Format-Uuid $localUuid) -ne $uuid) { continue }

                # Match — extract commit hash from .nuspec
                $version = $versionDir.Name
                $commit = $null
                $nuspec = Get-ChildItem $versionDir.FullName -Filter '*.nuspec' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
                if ($nuspec) {
                    try {
                        $xml = [xml](Get-Content $nuspec.FullName -Raw)
                        $repoNode = $xml.package.metadata.repository
                        if ($repoNode -and $repoNode.commit) {
                            $commit = $repoNode.commit
                        }
                    }
                    catch {
                        Write-Verbose "Could not parse nuspec for version $version`: $_"
                    }
                }

                return [PSCustomObject]@{
                    Version  = $version
                    Commit   = $commit
                    PackPath = $versionDir.FullName
                }
            }
        }
    }

    return $null
}

# --- Main ---

if (-not (Test-Path $CrashFile)) {
    Write-Error "Crash file not found: $CrashFile"
    exit 1
}

# Detect file format: .ips JSON (iOS 15+) vs older .crash text
$firstLine = (Get-Content $CrashFile -TotalCount 1).Trim()
if (-not $firstLine.StartsWith('{')) {
    Write-Error "Unsupported crash log format. This script requires the .ips JSON format (iOS 15+). The file is not JSON — it may be a legacy .crash text format, an Android tombstone, or another non-.ips format."
    exit 1
}

# Parse .ips crash log
$crash = Read-IpsCrashLog $CrashFile
$metadata = $crash.Metadata
$body = $crash.Body

$appName = if ($metadata.app_name) { $metadata.app_name }
           elseif ($metadata.name) { $metadata.name }
           else { 'Unknown' }
$osVersion = if ($metadata.os_version) { $metadata.os_version } else { 'Unknown' }
Write-Host "Crash log: $appName on $osVersion" -ForegroundColor Cyan

# Check for Application Specific Information (often contains managed exception text)
$asi = if ($body.PSObject.Properties['asi']) { $body.asi } else { $null }
if ($asi) {
    Write-Host "`nApplication Specific Information:" -ForegroundColor Yellow
    # asi can be a hashtable/object or array — flatten to string
    $asiText = ($asi | ConvertTo-Json -Depth 5 -Compress)
    if ($asiText.Length -gt 500) { $asiText = $asiText.Substring(0, 500) + '...' }
    Write-Host "  $asiText"
}

# Exception info
$exc = $body.exception
if ($exc) {
    $excType = if ($exc.type) { $exc.type } else { '?' }
    $excSignal = if ($exc.signal) { $exc.signal } else { '?' }
    Write-Host "Exception: $excType / $excSignal" -ForegroundColor Yellow
}

# Build image table
$images = Get-ImageTable $body
$dotnetImages = @($images | Where-Object { $_.IsDotNet })
Write-Host "Found $($images.Count) binary images ($($dotnetImages.Count) .NET libraries)" -ForegroundColor Cyan

if ($dotnetImages.Count -eq 0) {
    Write-Warning "No .NET runtime libraries found in usedImages. Nothing to symbolicate."
    Write-Host "`nBinary images:" -ForegroundColor Yellow
    foreach ($img in $images) {
        Write-Host "  $($img.Name)  UUID: $($img.Uuid)"
    }
    exit 0
}

# Extract thread frames
$faultingThreadIdx = if ($null -ne $body.faultingThread) { [int]$body.faultingThread } else { 0 }
$threads = @()

if ($body.threads) {
    for ($t = 0; $t -lt $body.threads.Count; $t++) {
        if ($CrashingThreadOnly -and $t -ne $faultingThreadIdx) { continue }

        $threadData = $body.threads[$t]
        $threadName = if ($threadData.PSObject.Properties['name']) { $threadData.name } else { $null }
        $label = if ($t -eq $faultingThreadIdx) { "Thread $t (Crashed)" }
                 elseif ($threadName) { "Thread $t ($threadName)" }
                 else { "Thread $t" }

        $frames = @(Get-ThreadFrames $threadData $images)
        if ($frames.Count -gt 0) {
            $threads += [PSCustomObject]@{ Header = $label; Frames = @($frames) }
        }
    }
}

# Also check lastExceptionBacktrace if present
if ($body.PSObject.Properties['lastExceptionBacktrace'] -and $body.lastExceptionBacktrace -and -not $CrashingThreadOnly) {
    $lebtFrames = @(Get-ThreadFrames ([PSCustomObject]@{ frames = $body.lastExceptionBacktrace }) $images)
    if ($lebtFrames.Count -gt 0) {
        $threads = @([PSCustomObject]@{ Header = 'Last Exception Backtrace'; Frames = @($lebtFrames) }) + $threads
    }
}

$allFrames = @($threads | ForEach-Object { $_.Frames } | ForEach-Object { $_ })
$dotnetFrames = @($allFrames | Where-Object { $_.IsDotNet })

Write-Host "Found $($allFrames.Count) frames across $($threads.Count) thread(s) ($($dotnetFrames.Count) from .NET libraries)" -ForegroundColor Cyan

if ($dotnetFrames.Count -eq 0) {
    Write-Warning "No .NET runtime frames found in the backtrace. Nothing to symbolicate."
    exit 0
}

# Verify atos is available (before ParseOnly check so we can auto-fallback)
if (-not $ParseOnly) {
    $atosCmd = Get-Command $Atos -ErrorAction SilentlyContinue
    if (-not $atosCmd) {
        # Try xcrun atos
        $xcrunCmd = Get-Command xcrun -ErrorAction SilentlyContinue
        $xcrunAtos = if ($xcrunCmd) { & xcrun --find atos 2>$null } else { $null }
        if ($xcrunAtos -and (Test-Path $xcrunAtos)) {
            $Atos = $xcrunAtos
            $atosCmd = Get-Command $Atos -ErrorAction SilentlyContinue
        }
    }
    if (-not $atosCmd) {
        Write-Warning "atos not found (requires macOS with Xcode Command Line Tools). Falling back to parse-only output."
        $ParseOnly = $true
    } else {
        Write-Verbose "Using atos: $($atosCmd.Source)"
    }
}

# --- ParseOnly mode ---
if ($ParseOnly) {
    Write-Host "`n=== Crash Log Parse Report ===" -ForegroundColor Green
    Write-Host "App: $appName"
    Write-Host "OS: $osVersion"
    Write-Host "Threads: $($threads.Count)"
    Write-Host "Total frames: $($allFrames.Count)"
    Write-Host ".NET frames: $($dotnetFrames.Count)"

    Write-Host "`n--- .NET Libraries (with frames to symbolicate) ---"
    $libGroups = $dotnetFrames | Group-Object ImageName
    foreach ($g in $libGroups) {
        $sample = $g.Group | Select-Object -First 1
        $pathVersion = Get-RuntimeVersionFromPath $sample.ImagePath
        $versionTag = if ($pathVersion) { "  .NET $pathVersion" } else { '' }
        Write-Host "  $($g.Name)  UUID: $($sample.ImageUuid)  Arch: $($sample.ImageArch)  Load: 0x$($sample.LoadAddress.ToString('x'))  ($($g.Count) frame(s))$versionTag"
    }

    Write-Host "`n--- Frames to Symbolicate ---"
    foreach ($t in $threads) {
        if ($threads.Count -gt 1 -and $t.Header) {
            Write-Host "  [$($t.Header)]"
        }
        $frameIdx = 0
        foreach ($f in $t.Frames) {
            if ($f.IsDotNet) {
                Write-Host "    #$frameIdx  $($f.ImageName)  $($f.AddressHex)  (offset 0x$($f.Offset.ToString('x')))"
            }
            $frameIdx++
        }
    }

    Write-Host "`n--- atos Commands ---"
    foreach ($g in $libGroups) {
        $sample = $g.Group | Select-Object -First 1
        $addrs = ($g.Group | ForEach-Object { $_.AddressHex }) -join ' '
        Write-Host "  atos -arch $($sample.ImageArch) -o <path-to-$($g.Name).dSYM/Contents/Resources/DWARF/$($g.Name)> -l 0x$($sample.LoadAddress.ToString('x')) $addrs"
    }

    Write-Host "`n=== End Parse Report ==="
    exit 0
}

# Set up symbol cache directory
if (-not $SymbolCacheDir) {
    $SymbolCacheDir = Join-Path ([System.IO.Path]::GetTempPath()) 'dotnet-crash-symbols'
}
if (-not (Test-Path $SymbolCacheDir)) {
    New-Item -ItemType Directory -Path $SymbolCacheDir -Force | Out-Null
}
Write-Verbose "Symbol cache: $SymbolCacheDir"

# Search for dSYMs for each .NET library
$dsymMap = @{} # UUID -> dSYM DWARF path
$uniqueLibs = $dotnetFrames | Sort-Object ImageName, ImagePath, ImageUuid -Unique

Write-Host "Searching for dSYM debug symbols..." -ForegroundColor Cyan
foreach ($lib in $uniqueLibs) {
    Write-Host "  $($lib.ImageName) (UUID: $($lib.ImageUuid))" -ForegroundColor DarkGray
    $dsymPath = Find-Dsym $lib.ImageUuid $lib.ImageName $DsymSearchPaths
    if ($dsymPath) {
        $dsymMap[$lib.ImageUuid] = $dsymPath
        Write-Host "    Found: $dsymPath" -ForegroundColor Green
    }
    else {
        Write-Warning "    dSYM not found for $($lib.ImageName). UUID: $($lib.ImageUuid)"
    }
}

$foundCount = $dsymMap.Count
if ($foundCount -gt 0) {
    Write-Host "Found local dSYMs for $foundCount/$($uniqueLibs.Count) .NET library/libraries" -ForegroundColor Green
}

# Download symbols from the Microsoft symbol server for any libraries still missing
$missingAfterLocal = @($uniqueLibs | Where-Object { -not $dsymMap.ContainsKey($_.ImageUuid) })
if ($missingAfterLocal.Count -gt 0 -and -not $SkipSymbolDownload) {
    Write-Host "Downloading symbols for $($missingAfterLocal.Count) missing library/libraries from symbol server..." -ForegroundColor Cyan
    foreach ($lib in $missingAfterLocal) {
        $uuid = Format-Uuid $lib.ImageUuid
        Write-Host "  $($lib.ImageName) (UUID: $uuid)" -ForegroundColor DarkGray
        $dwarfFile = Get-DebugSymbols $uuid $SymbolCacheDir $SymbolServerUrl
        if ($dwarfFile) {
            $dsymPath = Convert-DwarfToDsym $dwarfFile $lib.ImageName $uuid $SymbolCacheDir
            if ($dsymPath) {
                # Verify the UUID matches what the crash expects
                $downloadedUuid = Get-DsymUuid $dsymPath
                if ($downloadedUuid -and (Format-Uuid $downloadedUuid) -eq $uuid) {
                    $dsymMap[$lib.ImageUuid] = $dsymPath
                    $size = [math]::Round((Get-Item $dwarfFile).Length / 1MB, 1)
                    Write-Host "    ✅ Downloaded and converted ($($size) MB)" -ForegroundColor Green
                }
                else {
                    Write-Warning "    UUID mismatch for $($lib.ImageName) — expected $uuid, got $(Format-Uuid $downloadedUuid)"
                    # Remove the .dSYM bundle and cached .dwarf to prevent repeated mismatch
                    try {
                        # Walk up from DWARF file to .dSYM bundle root
                        # Path is: <cache>/<name>.dSYM/Contents/Resources/DWARF/<name>
                        $dsymBundlePath = $null
                        $candidate = Split-Path -LiteralPath $dsymPath -Parent
                        while ($candidate -and $candidate -ne (Split-Path -LiteralPath $candidate -Parent)) {
                            if ($candidate -like '*.dSYM') {
                                $dsymBundlePath = $candidate
                                break
                            }
                            $candidate = Split-Path -LiteralPath $candidate -Parent
                        }

                        if ($dsymBundlePath -and (Test-Path -LiteralPath $dsymBundlePath)) {
                            Remove-Item -LiteralPath $dsymBundlePath -Recurse -Force -ErrorAction Stop
                            Write-Host "    Removed cached dSYM bundle at '$dsymBundlePath' due to UUID mismatch; it will be re-fetched on next run." -ForegroundColor DarkYellow
                        }
                        elseif (Test-Path -LiteralPath $dsymPath) {
                            Remove-Item -LiteralPath $dsymPath -Recurse -Force -ErrorAction Stop
                            Write-Host "    Removed cached dSYM at '$dsymPath' due to UUID mismatch; it will be re-fetched on next run." -ForegroundColor DarkYellow
                        }

                        # Also remove the cached .dwarf file so it won't be reused on next run
                        if ($dwarfFile -and (Test-Path -LiteralPath $dwarfFile)) {
                            Remove-Item -LiteralPath $dwarfFile -Force -ErrorAction Stop
                            Write-Host "    Removed cached DWARF file at '$dwarfFile' due to UUID mismatch." -ForegroundColor DarkYellow
                        }
                    }
                    catch {
                        Write-Warning "    Failed to remove cached mismatched dSYM/DWARF artifacts: $($_.Exception.Message)"
                    }
                }
            }
        }
    }
    $downloadedCount = $dsymMap.Count - $foundCount
    if ($downloadedCount -gt 0) {
        Write-Host "Downloaded symbols for $downloadedCount/$($missingAfterLocal.Count) library/libraries" -ForegroundColor Green
    }
}

$totalFound = $dsymMap.Count
if ($totalFound -eq 0) {
    Write-Warning "Could not locate or download dSYM for any .NET library. Outputting unsymbolicated backtrace."
}

# Version identification — try extracting from crash log image paths first (instant,
# works without local installs), then fall back to UUID matching against local packs.
$versionMap = @{} # UUID -> version info
if (-not $SkipVersionLookup) {
    Write-Host "Identifying .NET runtime version..." -ForegroundColor Cyan
    foreach ($lib in $uniqueLibs) {
        # Fast path: extract version directly from the image path in the crash log
        $pathVersion = Get-RuntimeVersionFromPath $lib.ImagePath
        if ($pathVersion) {
            $versionMap[$lib.ImageUuid] = [PSCustomObject]@{
                Version  = $pathVersion
                Commit   = $null
                PackPath = $null
                Source   = 'crash-path'
            }
            Write-Host "  $($lib.ImageName) → .NET $pathVersion (from crash log path)" -ForegroundColor Green
            continue
        }

        # Slow path: UUID match against locally-installed packs/NuGet cache
        $versionInfo = Find-RuntimeVersion $lib.ImageUuid $lib.ImageName
        if ($versionInfo) {
            $versionMap[$lib.ImageUuid] = $versionInfo
            $commitShort = if ($versionInfo.Commit) { " (commit $($versionInfo.Commit.Substring(0, [Math]::Min(12, $versionInfo.Commit.Length))))" } else { '' }
            Write-Host "  $($lib.ImageName) → .NET $($versionInfo.Version)$commitShort" -ForegroundColor Green
        }
    }
}

# If dSYMs are still missing after download, emit manual acquisition guidance as fallback
$missingDsymLibs = @($uniqueLibs | Where-Object { -not $dsymMap.ContainsKey($_.ImageUuid) })
if ($missingDsymLibs.Count -gt 0 -and $versionMap.Count -gt 0) {
    # Determine the version and RID for acquisition
    $anyVersion = ($versionMap.Values | Select-Object -First 1).Version
    $crashRid = $null
    foreach ($lib in $missingDsymLibs) {
        $rid = Get-RidFromPath $lib.ImagePath
        if ($rid) { $crashRid = $rid; break }
    }
    # Infer RID from crash metadata if not found in paths
    if (-not $crashRid) {
        $osVer = if ($metadata.os_version) { $metadata.os_version } else { '' }
        $cpuType = if ($body.cpuType) { $body.cpuType } else { '' }
        $archSuffix = if ($cpuType -eq 'ARM-64' -or $cpuType -eq 'arm64' -or $cpuType -eq 'arm64e') { 'arm64' } else { 'x64' }

        # Detect simulator vs device from image paths (CoreSimulator present = simulator)
        $isSimulator = $false
        if ($body.usedImages) {
            foreach ($img in $body.usedImages) {
                if ($img.path -like '*CoreSimulator*') { $isSimulator = $true; break }
            }
        }

        if ($osVer -match 'macOS|Mac OS') { $crashRid = "osx-$archSuffix" }
        elseif ($osVer -match 'iPhone OS|iOS') {
            $crashRid = if ($isSimulator) { "iossimulator-$archSuffix" } else { "ios-arm64" }
        }
        elseif ($osVer -match 'tvOS') {
            $crashRid = if ($isSimulator) { "tvossimulator-$archSuffix" } else { "tvos-arm64" }
        }
        elseif ($osVer -match 'Mac Catalyst') { $crashRid = "maccatalyst-$archSuffix" }
    }

    $missingNames = ($missingDsymLibs | ForEach-Object { $_.ImageName }) -join ', '
    Write-Host "`n⚠️  Still missing dSYMs for: $missingNames" -ForegroundColor Yellow
    if ($SkipSymbolDownload) {
        Write-Host "   (Symbol download was skipped — re-run without -SkipSymbolDownload to try automatic download)" -ForegroundColor Yellow
    }
    Write-Host "   Version detected: .NET $anyVersion" -ForegroundColor Yellow
    if ($crashRid) {
        $isOsx = $crashRid -like 'osx-*'
        $runtimePackBase = "Microsoft.NETCore.App.Runtime.$crashRid"
        Write-Host "   Manual acquisition fallback:" -ForegroundColor Yellow
        if ($isOsx) {
            Write-Host "     dotnet-symbol: curl -Lo runtime.nupkg https://www.nuget.org/api/v2/package/$runtimePackBase/$anyVersion && unzip -q runtime.nupkg -d runtime-extracted && dotnet-symbol --symbols -o symbols-out runtime-extracted/runtimes/$crashRid/native/*.dylib" -ForegroundColor DarkYellow
            Write-Host "     Then convert .dwarf to .dSYM: mkdir -p symbols-out/<lib>.dSYM/Contents/Resources/DWARF && cp symbols-out/<lib>.dwarf symbols-out/<lib>.dSYM/Contents/Resources/DWARF/<lib>" -ForegroundColor DarkYellow
            Write-Host "     Re-run with: -DsymSearchPaths ./symbols-out" -ForegroundColor DarkYellow
        }
        else {
            # iOS/tvOS/MacCatalyst: dSYM bundles ship in the main runtime package
            Write-Host "     curl -Lo runtime.nupkg https://www.nuget.org/api/v2/package/$runtimePackBase/$anyVersion && unzip -q runtime.nupkg -d runtime-extracted" -ForegroundColor DarkYellow
            Write-Host "     Re-run with: -DsymSearchPaths ./runtime-extracted/runtimes/$crashRid/native" -ForegroundColor DarkYellow
        }
    }
}

# Symbolicate frames, grouped by image for batch atos calls
Write-Host "`nSymbolicating backtrace..." -ForegroundColor Cyan
$resolveCache = @{} # "uuid:address" -> resolved result

# Pre-resolve: batch atos calls per image
foreach ($lib in $uniqueLibs) {
    if (-not $dsymMap.ContainsKey($lib.ImageUuid)) { continue }

    $libFrames = @($dotnetFrames | Where-Object { $_.ImageUuid -eq $lib.ImageUuid })
    $uniqueAddresses = @($libFrames | Select-Object -ExpandProperty Address -Unique)
    $sample = $libFrames | Select-Object -First 1

    $results = Resolve-Frames $dsymMap[$lib.ImageUuid] $sample.ImageArch $sample.LoadAddress $uniqueAddresses $Atos

    for ($i = 0; $i -lt [Math]::Min($results.Count, $uniqueAddresses.Count); $i++) {
        if ($results[$i]) {
            $key = "$($lib.ImageUuid):$($uniqueAddresses[$i])"
            $resolveCache[$key] = $results[$i]
        }
    }
}

# Format output
$output = @()
$resolvedCount = 0

foreach ($thread in $threads) {
    if ($threads.Count -gt 1 -and $thread.Header) {
        $output += ""
        $output += "--- $($thread.Header) ---"
    }

    $frameIdx = 0
    foreach ($frame in $thread.Frames) {
        $key = "$($frame.ImageUuid):$($frame.Address)"
        if ($frame.IsDotNet -and $resolveCache.ContainsKey($key)) {
            $resolved = $resolveCache[$key]
            $resolvedCount++
            $sourceInfo = if ($resolved.Source) {
                # Keep last 2-3 path segments
                $parts = $resolved.Source -split '/'
                if ($parts.Count -gt 3) { ($parts[-3..-1]) -join '/' }
                elseif ($parts.Count -gt 1) { ($parts[-2..-1]) -join '/' }
                else { $resolved.Source }
            } else { '' }
            $line = if ($sourceInfo) {
                "#{0:D2}  {1,-36} {2,-48} ({3})" -f $frameIdx, $frame.ImageName, $resolved.Function, $sourceInfo
            } else {
                "#{0:D2}  {1,-36} {2}" -f $frameIdx, $frame.ImageName, $resolved.Function
            }
            $output += $line
        }
        else {
            # Unresolved — show address and image info
            $line = "#{0:D2}  {1,-36} {2}  (offset 0x{3:x})" -f $frameIdx, $frame.ImageName, $frame.AddressHex, $frame.Offset
            $output += $line
        }
        $frameIdx++
    }
}

# Build final output
$header = @(
    "--- Symbolicated Backtrace ---"
    ""
)
$footer = @(
    ""
    "--- $resolvedCount of $($dotnetFrames.Count) .NET frame(s) symbolicated ---"
)

if ($versionMap.Count -gt 0) {
    $footer += ""
    $footer += "--- .NET Runtime Version ---"
    foreach ($uuid in $versionMap.Keys) {
        $vi = $versionMap[$uuid]
        $lib = ($uniqueLibs | Where-Object { $_.ImageUuid -eq $uuid } | Select-Object -First 1).ImageName
        $commitInfo = if ($vi.Commit) { "  Commit: https://github.com/dotnet/runtime/commit/$($vi.Commit)" } else { '' }
        $footer += "$lib → .NET $($vi.Version)"
        if ($commitInfo) { $footer += $commitInfo }
    }
}

$result = ($header + $output + $footer) -join "`n"

if ($OutputFile) {
    $result | Out-File -FilePath $OutputFile -Encoding utf8
    Write-Host "`nWrote symbolicated backtrace to $OutputFile" -ForegroundColor Green
}
else {
    Write-Host ""
    Write-Host $result
}

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
.NET (Microsoft)
Publisher
.NET (Microsoft) (Official)
Source
dotnet/skills · plugins/dotnet-diag/skills/apple-crash-symbolication · commit 96c3d68
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by .NET (Microsoft) or .NET (Microsoft).

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download apple-crash-symbolication.zip