@@ -41,9 +41,10 @@ public override void Invoke()
4141 }
4242
4343 MemoryRange range ;
44- if ( Bounds is not null || Bounds . Length == 0 )
44+ MemoryRange additionalRange = default ;
45+ if ( Bounds is null || Bounds . Length == 0 )
4546 {
46- range = GetStackRange ( ) ;
47+ range = GetStackRangeWithAltStackDetection ( out additionalRange ) ;
4748 }
4849 else if ( Bounds . Length == 2 )
4950 {
@@ -66,7 +67,7 @@ public override void Invoke()
6667 throw new ArgumentException ( $ "Invalid range { range . Start : x} - { range . End : x} ") ;
6768 }
6869
69- PrintStackObjects ( range ) ;
70+ PrintStackObjects ( range , additionalRange ) ;
7071 }
7172
7273 [ HelpInvoke ]
@@ -84,15 +85,37 @@ interested in objects with invalid fields.
8485The abbreviation 'dso' can be used for brevity.
8586" ;
8687
87- private void PrintStackObjects ( MemoryRange stack )
88+ private void PrintStackObjects ( MemoryRange stack , MemoryRange additionalRange )
8889 {
8990 Console . WriteLine ( $ "OS Thread Id: 0x{ CurrentThread . ThreadId : x} ({ CurrentThread . ThreadIndex } )") ;
9091
92+ if ( additionalRange . Start != 0 && additionalRange . End != 0 )
93+ {
94+ Console . WriteLine ( $ "Note: Thread appears to be on alternate signal stack. Also scanning normal thread stack { additionalRange . Start : x} -{ additionalRange . End : x} .") ;
95+ }
96+
9197 Table output = new ( Console , Pointer , DumpObj , TypeName ) ;
9298 output . WriteHeader ( "SP/REG" , "Object" , "Name" ) ;
9399
94100 int regCount = ThreadService . Registers . Count ( ) ;
95- foreach ( ( ulong address , ClrObject obj ) in EnumerateValidObjectsWithinRange ( stack ) . OrderBy ( r => r . StackAddress ) )
101+ IEnumerable < ( ulong StackAddress , ClrObject Object ) > results = EnumerateValidObjectsWithinRange ( stack ) ;
102+
103+ // If we have an additional range (e.g., normal thread stack when on alt stack),
104+ // also scan that range for managed objects.
105+ if ( additionalRange . Start != 0 && additionalRange . End != 0 )
106+ {
107+ results = results . Concat ( EnumerateValidObjectsWithinRange ( additionalRange ) ) ;
108+ }
109+
110+ // Deduplicate by StackAddress to avoid duplicate register entries and
111+ // any other duplicates that may arise from scanning multiple ranges.
112+ IEnumerable < ( ulong StackAddress , ClrObject Object ) > orderedResults =
113+ results
114+ . GroupBy ( r => r . StackAddress )
115+ . Select ( g => g . First ( ) )
116+ . OrderBy ( r => r . StackAddress ) ;
117+
118+ foreach ( ( ulong address , ClrObject obj ) in orderedResults )
96119 {
97120 Console . CancellationToken . ThrowIfCancellationRequested ( ) ;
98121
@@ -368,6 +391,143 @@ private MemoryRange GetStackRange()
368391 return new ( AlignDown ( stackPointer ) , AlignUp ( end ) ) ;
369392 }
370393
394+ /// <summary>
395+ /// Gets the stack range for the current thread, also returning an additional
396+ /// range if the thread appears to be executing on an alternate signal stack.
397+ /// On Linux, when a signal handler runs on the alternate stack (e.g. stack
398+ /// overflow), the SP points to the handler stack while managed objects live
399+ /// on the normal thread stack. macOS uses Mach exceptions instead of
400+ /// sigaltstack so this scenario does not arise there, but no platform check
401+ /// is needed because the frame-based gap detection is harmless when there is
402+ /// no disjoint stack.
403+ /// </summary>
404+ private MemoryRange GetStackRangeWithAltStackDetection ( out MemoryRange additionalRange )
405+ {
406+ additionalRange = default ;
407+
408+ int spIndex = ThreadService . StackPointerIndex ;
409+ if ( ! CurrentThread . TryGetRegisterValue ( spIndex , out ulong stackPointer ) )
410+ {
411+ throw new DiagnosticsException ( $ "Unable to get the stack pointer for thread { CurrentThread . ThreadId : x} .") ;
412+ }
413+
414+ ulong end = 0 ;
415+
416+ // On Windows we have the TEB to know where to end the walk.
417+ ulong teb = CurrentThread . GetThreadTeb ( ) ;
418+ if ( teb != 0 )
419+ {
420+ // The stack base is after the first pointer, see TEB and NT_TIB.
421+ MemoryService . ReadPointer ( teb + ( uint ) MemoryService . PointerSize , out end ) ;
422+ }
423+
424+ if ( end != 0 )
425+ {
426+ return new ( AlignDown ( stackPointer ) , AlignUp ( end ) ) ;
427+ }
428+
429+ // TEB not available (Linux/Unix). Use managed stack frames to determine
430+ // the real stack boundaries. This is important when the thread is on an
431+ // alternate signal stack, where the SP points to the alt stack memory
432+ // but managed objects reside on the normal thread stack.
433+ ClrThread clrThread = Runtime . Threads . FirstOrDefault ( t => t . OSThreadId == CurrentThread . ThreadId ) ;
434+ if ( clrThread is not null )
435+ {
436+ List < ulong > frameSPs = new ( 64 ) ;
437+ foreach ( ClrStackFrame frame in clrThread . EnumerateStackTrace ( ) )
438+ {
439+ if ( frame . StackPointer != 0 )
440+ {
441+ frameSPs . Add ( frame . StackPointer ) ;
442+ }
443+ }
444+
445+ if ( frameSPs . Count > 0 )
446+ {
447+ frameSPs . Sort ( ) ;
448+
449+ // Detect disjoint stack regions by finding large gaps between
450+ // consecutive frame SPs. This happens when a signal handler runs
451+ // on an alternate or handler stack — the managed frames span two
452+ // non-contiguous memory regions. The threshold is set to 4MB to
453+ // avoid false positives from large stackalloc or deep native call
454+ // chains between managed frames while still being far smaller than the
455+ // address-space gap between disjoint stack regions (typically in the
456+ // multi-GB range due to separate mmap regions in the virtual address space).
457+ const ulong GapThreshold = 0x400000 ; // 4 MB
458+
459+ ulong largestGap = 0 ;
460+ int gapIndex = - 1 ;
461+ for ( int i = 1 ; i < frameSPs . Count ; i ++ )
462+ {
463+ ulong gap = frameSPs [ i ] - frameSPs [ i - 1 ] ;
464+ if ( gap > largestGap )
465+ {
466+ largestGap = gap ;
467+ gapIndex = i ;
468+ }
469+ }
470+
471+ if ( largestGap > GapThreshold && gapIndex > 0 )
472+ {
473+ // Frames span two disjoint memory regions.
474+ ulong lowerMin = frameSPs [ 0 ] ;
475+ ulong lowerMax = frameSPs [ gapIndex - 1 ] ;
476+ ulong upperMin = frameSPs [ gapIndex ] ;
477+ ulong upperMax = frameSPs [ frameSPs . Count - 1 ] ;
478+
479+ MemoryRange lowerRange = new ( AlignDown ( lowerMin ) , AlignUp ( lowerMax + 0x2000 ) ) ;
480+ MemoryRange upperRange = new ( AlignDown ( upperMin ) , AlignUp ( upperMax + 0x2000 ) ) ;
481+
482+ // Use Math.Max/Min to avoid underflow when frame addresses are near zero.
483+ if ( stackPointer >= Math . Max ( lowerMin , GapThreshold ) - GapThreshold && stackPointer <= lowerMax + 0x2000 )
484+ {
485+ lowerRange = new ( AlignDown ( Math . Min ( stackPointer , lowerMin ) ) , lowerRange . End ) ;
486+ additionalRange = upperRange ;
487+ return lowerRange ;
488+ }
489+ else if ( stackPointer >= Math . Max ( upperMin , GapThreshold ) - GapThreshold && stackPointer <= upperMax + 0x2000 )
490+ {
491+ upperRange = new ( AlignDown ( Math . Min ( stackPointer , upperMin ) ) , upperRange . End ) ;
492+ additionalRange = lowerRange ;
493+ return upperRange ;
494+ }
495+ else
496+ {
497+ // SP is in neither range. Scan around the SP and use
498+ // the larger frame region as the additional range.
499+ ulong lowerSize = lowerMax - lowerMin ;
500+ ulong upperSize = upperMax - upperMin ;
501+ additionalRange = upperSize >= lowerSize ? upperRange : lowerRange ;
502+ return new ( AlignDown ( stackPointer ) , AlignUp ( stackPointer + 0xFFFF ) ) ;
503+ }
504+ }
505+
506+ // No large gap — all frames are on one contiguous stack.
507+ ulong minFrameSp = frameSPs [ 0 ] ;
508+ ulong maxFrameSp = frameSPs [ frameSPs . Count - 1 ] ;
509+ ulong frameEnd = maxFrameSp + 0x2000 ;
510+
511+ bool spBelowFrames = stackPointer < minFrameSp ;
512+ bool spAboveFrames = stackPointer > frameEnd ;
513+ bool isLikelyAltStack = spAboveFrames
514+ || ( spBelowFrames && ( minFrameSp - stackPointer ) > GapThreshold ) ;
515+
516+ if ( isLikelyAltStack )
517+ {
518+ additionalRange = new ( AlignDown ( minFrameSp ) , AlignUp ( frameEnd ) ) ;
519+ return new ( AlignDown ( stackPointer ) , AlignUp ( stackPointer + 0xFFFF ) ) ;
520+ }
521+
522+ ulong start = Math . Min ( stackPointer , minFrameSp ) ;
523+ return new ( AlignDown ( start ) , AlignUp ( frameEnd ) ) ;
524+ }
525+ }
526+
527+ // Fallback: no frame info available
528+ return new ( AlignDown ( stackPointer ) , AlignUp ( stackPointer + 0xFFFF ) ) ;
529+ }
530+
371531 private ulong AlignDown ( ulong address )
372532 {
373533 ulong mask = ~ ( ( ulong ) MemoryService . PointerSize - 1 ) ;
0 commit comments