using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace BlackbirdInterface { public partial class ThreadStackWindow : Window { private const string MissingText = "N/A"; private const int MaxSnapshotHistory = 128; public ObservableCollection UserFrames { get; } = new(); public ObservableCollection GeneralRegs { get; } = new(); private readonly Dictionary _generalRegByName = new(StringComparer.OrdinalIgnoreCase); private readonly int _pid; private readonly int _tid; private string _state; private readonly Func? _observationTimeUtcProvider; private readonly Func? _liveCaptureAvailableProvider; private readonly Func? _threadStateProvider; private readonly Action? _onSnapshotCaptured; private readonly DispatcherTimer _refreshTimer; private readonly List _snapshotHistory = new(); private bool _refreshInFlight; private ThreadStackSessionSnapshot? _lastAppliedSnapshot; public ThreadStackWindow(int pid, int tid, string state, IEnumerable? initialHistory = null, Action? onSnapshotCaptured = null, Func? observationTimeUtcProvider = null, Func? liveCaptureAvailableProvider = null, Func? threadStateProvider = null) { InitializeComponent(); WindowThemeHelper.WireThemeAwareTitleBar(this); _pid = pid; _tid = tid; _state = state; _onSnapshotCaptured = onSnapshotCaptured; _observationTimeUtcProvider = observationTimeUtcProvider; _liveCaptureAvailableProvider = liveCaptureAvailableProvider; _threadStateProvider = threadStateProvider; _refreshTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(2) }; _refreshTimer.Tick += async (_, __) => await RefreshDisplayAsync(); HeaderBlock.Text = $"Thread {_tid} Stack"; HeaderStateBlock.Text = string.IsNullOrWhiteSpace(_state) ? $"State: {MissingText}" : $"State: {_state}"; ApplyStateVisuals(_state); UserStackGrid.ItemsSource = UserFrames; GeneralRegsList.ItemsSource = GeneralRegs; if (initialHistory != null) { _snapshotHistory.AddRange(initialHistory.Select(x => x.Clone()).OrderBy(x => x.CapturedAtUtc)); } SeedRegisterPlaceholders(); Loaded += async (_, __) => { await RefreshDisplayAsync(); _refreshTimer.Start(); }; Closed += (_, __) => _refreshTimer.Stop(); } private void SeedRegisterPlaceholders() { GeneralRegs.Clear(); _generalRegByName.Clear(); string[] regs = { "RIP", "RSP", "RBP", "RAX", "RBX", "RCX", "RDX", "RSI", "RDI", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15" }; foreach (var r in regs) { var entry = new RegisterEntry(r, MissingText); GeneralRegs.Add(entry); _generalRegByName[r] = entry; } EFlagsMetaBlock.Text = MissingText; EFlagsDetailBlock.Text = MissingText; EFlagsEnglishBlock.Text = MissingText; DrxBitsBlock.Text = MissingText; StackSpanMetaBlock.Text = MissingText; ActiveSpanMetaBlock.Text = MissingText; StateMetaBlock.Text = string.IsNullOrWhiteSpace(_state) ? MissingText : _state; } private void ApplyStateVisuals(string? state) { Brush brush = GetStateBrush(state); HeaderStateBlock.Foreground = brush; HeaderStateBlock.FontWeight = FontWeights.SemiBold; StateMetaBlock.Foreground = brush; StateMetaBlock.FontWeight = FontWeights.SemiBold; } private static Brush GetStateBrush(string? state) { string value = (state ?? string.Empty).Trim().ToLowerInvariant(); if (value is "running" or "ready") { return ResolveResourceBrush("StatusConnectedBrush", Color.FromRgb(0x5B, 0xD1, 0x71)); } if (value is "wait" or "waiting" or "sleep" or "sleeping") { return ResolveResourceBrush("StatusWarningBrush", Color.FromRgb(0xE0, 0xC2, 0x6C)); } if (value is "suspended" or "blocked" or "terminated") { return ResolveResourceBrush("StatusErrorBrush", Color.FromRgb(0xE0, 0x78, 0x78)); } return ResolveResourceBrush("WinMutedTextBrush", Color.FromRgb(0xB0, 0xB0, 0xB0)); } private static Brush ResolveResourceBrush(string key, Color fallback) { if (Application.Current?.TryFindResource(key) is Brush brush) { return brush; } var created = new SolidColorBrush(fallback); created.Freeze(); return created; } private void Root_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { WindowChromeBehavior.HandleRootDragMove(this, e); } private async Task RefreshDisplayAsync() { if (_refreshInFlight) { return; } _refreshInFlight = true; try { string currentState = RefreshCurrentState(); DateTime observedUtc = GetObservedUtc(); bool liveCaptureAvailable = _liveCaptureAvailableProvider?.Invoke() ?? true; bool preferLive = ShouldResolveLive(observedUtc, liveCaptureAvailable); ThreadStackResolveResult? liveResult = null; if (preferLive) { NoteBlock.Text = "Resolving stack..."; liveResult = await Task.Run(() => ThreadStackResolver.Resolve(_pid, _tid, currentState)); if (HasResolvedStackData(liveResult)) { ThreadStackSessionSnapshot snapshot = CreateSnapshot(DateTime.UtcNow, liveResult); _snapshotHistory.Add(snapshot); if (_snapshotHistory.Count > MaxSnapshotHistory) { _snapshotHistory.RemoveRange(0, _snapshotHistory.Count - MaxSnapshotHistory); } _onSnapshotCaptured?.Invoke(snapshot.Clone()); ApplySnapshot(snapshot, $"Live | {snapshot.CapturedAtUtc:HH:mm:ss.fff}"); return; } } ThreadStackSessionSnapshot? historical = FindHistoricalSnapshot(observedUtc); if (historical != null) { ApplySnapshot(historical, $"Historical | {historical.CapturedAtUtc:HH:mm:ss.fff}"); return; } if (_lastAppliedSnapshot != null) { NoteBlock.Text = $"Stale | {_lastAppliedSnapshot.CapturedAtUtc:HH:mm:ss.fff} | no newer stack available"; return; } string noDataReason = liveCaptureAvailable ? "No captured stack for the selected time." : "Thread is no longer live and no captured stack exists for the selected time."; if (!string.IsNullOrWhiteSpace(liveResult?.Note)) { noDataReason = liveResult.Note; } ApplyNoDataState(noDataReason); } catch { if (_lastAppliedSnapshot != null) { NoteBlock.Text = $"Stale | {_lastAppliedSnapshot.CapturedAtUtc:HH:mm:ss.fff} | refresh failed"; } else { ApplyNoDataState("Failed to resolve stack."); } } finally { _refreshInFlight = false; } } private DateTime GetObservedUtc() => _observationTimeUtcProvider?.Invoke() ?? DateTime.UtcNow; private string RefreshCurrentState() { string? current = _threadStateProvider?.Invoke(_tid); if (!string.IsNullOrWhiteSpace(current)) { _state = current.Trim(); } HeaderStateBlock.Text = string.IsNullOrWhiteSpace(_state) ? $"State: {MissingText}" : $"State: {_state}"; ApplyStateVisuals(_state); return _state; } private bool ShouldResolveLive(DateTime observedUtc, bool liveCaptureAvailable) { if (!liveCaptureAvailable) { return false; } if (_snapshotHistory.Count == 0) { return true; } DateTime latestCaptureUtc = _snapshotHistory[^1].CapturedAtUtc; return observedUtc >= latestCaptureUtc.AddMilliseconds(-750); } private ThreadStackSessionSnapshot? FindHistoricalSnapshot(DateTime observedUtc) { return _snapshotHistory.Where(x => x.CapturedAtUtc <= observedUtc) .OrderByDescending(x => x.CapturedAtUtc) .FirstOrDefault(); } private static bool HasResolvedStackData(ThreadStackResolveResult result) { return result.Frames.Count > 0 || result.StackPointer != 0 || result.TebAddress != 0 || result.ContextSnapshot != null; } private ThreadStackSessionSnapshot CreateSnapshot(DateTime capturedAtUtc, ThreadStackResolveResult result) { return new ThreadStackSessionSnapshot { CapturedAtUtc = capturedAtUtc, TebAddress = result.TebAddress, StackBase = result.StackBase, StackTop = result.StackTop, TebFlags = result.TebFlags, StackPointer = result.StackPointer, ContextSnapshot = CloneContextSnapshot(result.ContextSnapshot), Frames = result.Frames.Select(CloneFrame).ToList() }; } private void ApplySnapshot(ThreadStackSessionSnapshot snapshot, string note) { _lastAppliedSnapshot = snapshot.Clone(); HideNoDataOverlay(); ResetHeaderMetadata(); PidBlock.Text = _pid.ToString(); TidBlock.Text = _tid.ToString(); TebBlock.Text = snapshot.TebAddress == 0 ? MissingText : $"0x{snapshot.TebAddress:X}"; StackBaseMetaBlock.Text = snapshot.StackBase == 0 ? MissingText : $"0x{snapshot.StackBase:X}"; StackTopMetaBlock.Text = snapshot.StackTop == 0 ? MissingText : $"0x{snapshot.StackTop:X}"; RspMetaBlock.Text = snapshot.StackPointer == 0 ? MissingText : $"0x{snapshot.StackPointer:X}"; TebFlagsMetaBlock.Text = snapshot.TebFlags.HasValue ? $"0x{snapshot.TebFlags.Value:X4}" : MissingText; StateMetaBlock.Text = string.IsNullOrWhiteSpace(_state) ? MissingText : _state; ulong stackSpan = snapshot.StackBase > snapshot.StackTop ? snapshot.StackBase - snapshot.StackTop : 0UL; StackSpanMetaBlock.Text = stackSpan == 0 ? MissingText : $"0x{stackSpan:X} ({stackSpan} B)"; ActiveSpanMetaBlock.Text = MissingText; UserFrames.Clear(); foreach (StackFrameRow frame in snapshot.Frames.Select(CloneFrame)) { UserFrames.Add(frame); } ComputeFrameSpans(snapshot.StackBase, snapshot.StackTop); for (int i = 0; i < UserFrames.Count; i += 1) { UserFrames[i].IsCurrent = i == 0; } ulong rip = UserFrames.Select(x => x.InstructionPointerRaw).FirstOrDefault(v => v != 0); RipMetaBlock.Text = rip == 0 ? MissingText : $"0x{rip:X}"; SetReg("RIP", rip == 0 ? (ulong?)null : rip); SetReg("RSP", snapshot.StackPointer == 0 ? (ulong?)null : snapshot.StackPointer); SetReg("RBP", UserFrames.Select(x => x.FramePointerRaw).FirstOrDefault(v => v != 0)); ApplyResolvedContext(snapshot.ContextSnapshot); NoteBlock.Text = $"{note} | {UserFrames.Count} user frame(s)"; } private void ApplyNoDataState(string reason) { ResetHeaderMetadata(); PidBlock.Text = _pid.ToString(); TidBlock.Text = _tid.ToString(); StateMetaBlock.Text = string.IsNullOrWhiteSpace(_state) ? MissingText : _state; SeedRegisterPlaceholders(); UserFrames.Clear(); ShowNoDataOverlay(reason); NoteBlock.Text = reason; } private void ResetHeaderMetadata() { TebBlock.Text = MissingText; StackBaseMetaBlock.Text = MissingText; StackTopMetaBlock.Text = MissingText; RipMetaBlock.Text = MissingText; RspMetaBlock.Text = MissingText; TebFlagsMetaBlock.Text = MissingText; EFlagsMetaBlock.Text = MissingText; EFlagsDetailBlock.Text = MissingText; EFlagsEnglishBlock.Text = MissingText; DrxBitsBlock.Text = MissingText; StackSpanMetaBlock.Text = MissingText; ActiveSpanMetaBlock.Text = MissingText; } private void ShowNoDataOverlay(string message) { if (StackNoDataOverlay != null) { StackNoDataOverlay.Visibility = Visibility.Visible; } if (StackNoDataMessageBlock != null) { StackNoDataMessageBlock.Text = message; } } private void HideNoDataOverlay() { if (StackNoDataOverlay != null) { StackNoDataOverlay.Visibility = Visibility.Collapsed; } } private void SetReg(string name, ulong? value) { if (_generalRegByName.TryGetValue(name, out var entry)) { entry.ValueText = value.HasValue ? $"0x{value.Value:X}" : MissingText; } } private void ApplyResolvedContext(ThreadContextSnapshot? snapshot) { if (snapshot == null) { return; } (string Name, ulong Value)[] regs = { ("RAX", snapshot.Rax), ("RBX", snapshot.Rbx), ("RCX", snapshot.Rcx), ("RDX", snapshot.Rdx), ("RSI", snapshot.Rsi), ("RDI", snapshot.Rdi), ("R8", snapshot.R8), ("R9", snapshot.R9), ("R10", snapshot.R10), ("R11", snapshot.R11), ("R12", snapshot.R12), ("R13", snapshot.R13), ("R14", snapshot.R14), ("R15", snapshot.R15) }; foreach (var reg in regs) { SetReg(reg.Name, reg.Value); } EFlagsMetaBlock.Text = $"0x{snapshot.EFlags:X8}"; EFlagsDetailBlock.Text = DecodeEFlags(snapshot.EFlags); EFlagsEnglishBlock.Text = DescribeEFlagsEnglish(snapshot.EFlags); DrxBitsBlock.Text = BuildDrxBitView(snapshot); } private static string DecodeEFlags(uint eflags) { string[] bits = { ((eflags & (1u << 0)) != 0) ? "CF" : "", ((eflags & (1u << 2)) != 0) ? "PF" : "", ((eflags & (1u << 4)) != 0) ? "AF" : "", ((eflags & (1u << 6)) != 0) ? "ZF" : "", ((eflags & (1u << 7)) != 0) ? "SF" : "", ((eflags & (1u << 8)) != 0) ? "TF" : "", ((eflags & (1u << 9)) != 0) ? "IF" : "", ((eflags & (1u << 10)) != 0) ? "DF" : "", ((eflags & (1u << 11)) != 0) ? "OF" : "" }; string joined = string.Join(" ", bits.Where(x => !string.IsNullOrWhiteSpace(x))); if (string.IsNullOrWhiteSpace(joined)) { joined = "none"; } uint iopl = (eflags >> 12) & 0x3u; return $"{joined} IOPL={iopl}"; } private static string DescribeEFlagsEnglish(uint eflags) { var items = new List(); items.Add(((eflags & (1u << 6)) != 0) ? "Zero result" : "Non-zero result"); items.Add(((eflags & (1u << 7)) != 0) ? "Negative sign" : "Positive sign"); items.Add(((eflags & (1u << 0)) != 0) ? "Carry occurred" : "No carry"); items.Add(((eflags & (1u << 11)) != 0) ? "Signed overflow" : "No signed overflow"); items.Add(((eflags & (1u << 9)) != 0) ? "Interrupts enabled" : "Interrupts disabled"); items.Add(((eflags & (1u << 10)) != 0) ? "Direction down" : "Direction up"); uint iopl = (eflags >> 12) & 0x3u; items.Add($"I/O privilege level {iopl}"); return string.Join(" | ", items); } private static string BuildDrxBitView(ThreadContextSnapshot snapshot) { string drLine0 = $"DR0 {Hex64(snapshot.Dr0)} DR1 {Hex64(snapshot.Dr1)}"; string drLine1 = $"DR2 {Hex64(snapshot.Dr2)} DR3 {Hex64(snapshot.Dr3)}"; string drLine2 = $"DR6 {Hex64(snapshot.Dr6)} DR7 {Hex64(snapshot.Dr7)}"; string dr6Bits = $"DR6 bits: B0={Bit(snapshot.Dr6, 0)} B1={Bit(snapshot.Dr6, 1)} B2={Bit(snapshot.Dr6, 2)} B3={Bit(snapshot.Dr6, 3)} BD={Bit(snapshot.Dr6, 13)} BS={Bit(snapshot.Dr6, 14)} BT={Bit(snapshot.Dr6, 15)}"; string dr7Bits = $"DR7 bits: L0={Bit(snapshot.Dr7, 0)} G0={Bit(snapshot.Dr7, 1)} L1={Bit(snapshot.Dr7, 2)} G1={Bit(snapshot.Dr7, 3)} L2={Bit(snapshot.Dr7, 4)} G2={Bit(snapshot.Dr7, 5)} L3={Bit(snapshot.Dr7, 6)} G3={Bit(snapshot.Dr7, 7)}"; return string.Join(Environment.NewLine, drLine0, drLine1, drLine2, dr6Bits, dr7Bits); } private static string Hex64(ulong value) { return $"0x{value:X16}"; } private static int Bit(ulong value, int bit) { return ((value >> bit) & 1UL) != 0 ? 1 : 0; } private void ComputeFrameSpans(ulong stackBase, ulong stackTop) { if (UserFrames.Count == 0) { return; } var points = UserFrames.Select(x => x.FramePointerRaw) .Where(v => v >= stackTop && v <= stackBase) .Distinct() .OrderByDescending(v => v) .ToList(); if (points.Count == 0) { foreach (var frame in UserFrames) { frame.FrameSpanBytes = 0; } return; } var spansByPointer = new Dictionary(); for (int i = 0; i < points.Count; i += 1) { ulong hi = points[i]; if (i + 1 >= points.Count) { spansByPointer[hi] = 0; continue; } ulong lo = points[i + 1]; long span = hi > lo ? (long)(hi - lo) : 0L; spansByPointer[hi] = span; } foreach (var frame in UserFrames) { frame.FrameSpanBytes = spansByPointer.TryGetValue(frame.FramePointerRaw, out long span) ? span : 0L; } } private void Close_Click(object sender, RoutedEventArgs e) => Close(); private static StackFrameRow CloneFrame(StackFrameRow frame) { return new StackFrameRow { Index = frame.Index, Address = frame.Address, Module = frame.Module, Symbol = frame.Symbol, InstructionPointerRaw = frame.InstructionPointerRaw, FramePointerRaw = frame.FramePointerRaw, FrameSpanBytes = frame.FrameSpanBytes, IsCurrent = frame.IsCurrent }; } private static ThreadContextSnapshot? CloneContextSnapshot(ThreadContextSnapshot? snapshot) => snapshot == null ? null : new ThreadStackSessionSnapshot { ContextSnapshot = snapshot }.Clone().ContextSnapshot; } public sealed class RegisterEntry : INotifyPropertyChanged { public string Name { get; } private string _valueText; public string ValueText { get => _valueText; set { if (string.Equals(_valueText, value, StringComparison.Ordinal)) { return; } _valueText = value; OnPropertyChanged(); } } public RegisterEntry(string name, string valueText) { Name = name; _valueText = valueText; } public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }