-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[cDAC] Implement core stackwalking #111759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
a213ec2
thread context fetching
8f2b7e7
iterate and debug print Frames
6f2f4ab
add more functionality needed for stack walking
0259f83
implement R2R unwind info fetching
cd8854c
stackwalking on amd64
17e34fd
AMD64 stack unwinding impl
22a3c9a
Merge branch 'cdac-stackwalk' of github.com:max-charlamb/runtime into…
5d9317a
crossplatform LazyMachState fixes
2a1e410
add support for building arm64 and amd64 unwinders
da73082
add GetPlatform
a794374
AMD64/ARM64 stackwalking backend
3d22034
AMD64 stackwalking through SOS
18e63a3
clean up PR
25f25c2
Merge remote-tracking branch 'origin/main' into cdac-stackwalk
831fb14
fix issues caused by merge from origin/main
4f97647
remove unused datatype
721042c
only build native unwinders on windows amd64
2887c4c
change cmakelists order
1df3093
remove eventing_header dependency for cdac_unwinders
3696838
fix loading cDAC on non-windows platforms
373f9ef
use unused variable
max-charlamb 5edb98d
move to testhost for library tests
1d1beda
Merge branch 'cdac-stackwalk' of github.com:max-charlamb/runtime into…
a1703a0
don't build cDAC in Libraries_WithPackages run
71d5590
isolate context datastructures from logic
fb4109c
clean up
4fc3f22
address easy to fix feedback
bfb63ee
move frame down casting logic to FrameIterator
1961e4d
wip improve documentation
ce13fdc
typo
1d3799e
Merge remote-tracking branch 'origin/main' into cdac-stackwalk
4e40652
use FrameIdentifier instead of VPtr
af93b5d
docs wip
0d2c3d9
improve docs
82e9ece
be more consistent when referring to Windows "style" unwinding/context
333a932
refactor unwinder CMakeLists.txt
bcdf2e4
move UNWINDER_ASSERT define into base unwinder
d3fdf0b
refactor unwinder to use same InstructionBuffer for DAC/cDAC
a246775
prefix class members with m_
90fd607
implement cDAC native unwinder assertions through refactor using thre…
93a8345
clean up diff
746f584
use MSTypes
bd41361
make comment inclusive of cDAC
da0425a
convert to use IEnumerable<IStackDataFrameHandle>
c7ad322
Merge remote-tracking branch 'origin/main' into cdac-stackwalk
bf1743b
rework docs to better explain skipped Frame check
0a03272
Merge branch 'main' into cdac-stackwalk
max-charlamb 0295a95
improve ExecutionManager docs
fa50b1c
add tests for GetModuleBaseAddress
54e3cb3
fix build issue
ba2a8ab
re-add Create(...)
4945be7
fix StressLogAnalyzer support
cd15192
rename GetModuleBaseAddress -> GetUnwindInfoBaseAddress
62c2c27
simplify doc wording
ec25af2
change Target threadcontext API to support ulong
16639ad
change to truncate threadID
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| # Contract StackWalk | ||
|
|
||
| This contract encapsulates support for walking the stack of managed threads. | ||
|
|
||
| ## APIs of contract | ||
|
|
||
| ```csharp | ||
| public interface IStackDataFrameHandle { }; | ||
| ``` | ||
|
|
||
| ```csharp | ||
| // Creates a stack walk and returns a handle | ||
| IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData); | ||
|
|
||
| // Gets the thread context at the given stack dataframe. | ||
| byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle); | ||
| // Gets the Frame address at the given stack dataframe. Returns TargetPointer.Null if the current dataframe does not have a valid Frame. | ||
| TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle); | ||
| ``` | ||
|
|
||
| ## Version 1 | ||
| To create a full walk of the managed stack, two types of 'stacks' must be read. | ||
|
|
||
| 1. True call frames on the thread's stack | ||
| 2. Capital "F" Frames (referred to as Frames as opposed to frames) which are used by the runtime for book keeping purposes. | ||
|
|
||
| Capital "F" Frames are pushed and popped to a singly-linked list on the runtime's Thread object and are accessible using the [IThread](./Thread.md) contract. These capital "F" Frames are allocated within a functions call frame, meaning they also live on the stack. A subset of Frame types store extra data allowing us to recover a portion of the context from when they were created For our purposes, these are relevant because they mark every transition where managed code calls native code. For more information about Frames see: [BOTR Stack Walking](https://github.com/dotnet/runtime/blob/44b7251f94772c69c2efb9daa7b69979d7ddd001/docs/design/coreclr/botr/stackwalking.md). | ||
|
|
||
| Unwinding call frames on the stack usually requires an OS specific implementation. However, in our particular circumstance of unwinding only **managed function** call frames, the runtime uses Windows style unwind logic/codes for all platforms (this isn't true for NativeAOT). Therefore we can delegate to the existing native unwinding code located in `src/coreclr/unwinder/`. For more information on the Windows unwinding algorithm and unwind codes see the following docs: | ||
|
|
||
| * [Windows x64](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64) | ||
| * [Windows ARM64](https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling) | ||
|
|
||
| This contract depends on the following descriptors: | ||
|
|
||
| | Data Descriptor Name | Field | Meaning | | ||
| | --- | --- | --- | | ||
| | `Frame` | `Next` | Pointer to next from on linked list | | ||
| | `InlinedCallFrame` | `CallSiteSP` | SP saved in Frame | | ||
| | `InlinedCallFrame` | `CallerReturnAddress` | Return address saved in Frame | | ||
| | `InlinedCallFrame` | `CalleeSavedFP` | FP saved in Frame | | ||
| | `SoftwareExceptionFrame` | `TargetContext` | Context object saved in Frame | | ||
| | `SoftwareExceptionFrame` | `ReturnAddress` | Return address saved in Frame | | ||
|
|
||
| Global variables used: | ||
| | Global Name | Type | Purpose | | ||
| | --- | --- | --- | | ||
| | For each FrameType `<frameType>`, `<frameType>##Identifier` | FrameIdentifier enum value | Identifier used to determine concrete type of Frames | | ||
|
|
||
| Contracts used: | ||
| | Contract Name | | ||
| | --- | | ||
| | `ExecutionManager` | | ||
| | `Thread` | | ||
|
|
||
|
|
||
| ### Stackwalk Algorithm | ||
| The intuition for walking a managed stack is relatively simply: unwind managed portions of the stack until we hit native code then use capital "F" Frames as checkpoints to get into new sections of managed code. Because Frames are added at each point before managed code (higher SP value) calls native code (lower SP values), we are guaranteed that a Frame exists at the top (lower SP value) of each managed call frame run. | ||
|
|
||
| In reality, the actual algorithm is a little more complex fow two reasons. It requires pausing to return the current context and Frame at certain points and it checks for "skipped Frames" which can occur if an capital "F" Frame is allocated in a managed stack frame (e.g. an inlined P/Invoke call). | ||
|
|
||
| 1. Setup | ||
| 1. Set the current context `currContext` to be the thread's context. Fetched as part of the [ICorDebugDataTarget](https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/debugging/icordebugdatatarget-getthreadcontext-method) COM interface. | ||
| 2. Create a stack of the thread's capital "F" Frames `frameStack`. | ||
| 2. **Return the current context**. | ||
| 3. While the `currContext` is in managed code or `frameStack` is not empty: | ||
| 1. If `currContext` is native code, pop the top Frame from `frameStack` update the context using the popped Frame. **Return the updated context** and **go to step 3**. | ||
| 2. If `frameStack` is not empty, check for skipped Frames. Peek `frameStack` to find a Frame `frame`. Compare the address of `frame` (allocated on the stack) with the caller of the current context's stack pointer (found by unwinding current context one iteration). | ||
| If the address of the `frame` is less than the caller's stack pointer, **return the current context**, pop the top Frame from `frameStack`, and **go to step 3**. | ||
| 3. Unwind `currContext` using the Windows style unwinder. **Return the current context**. | ||
|
|
||
|
|
||
| #### Simple Example | ||
|
|
||
| In this example we walk through the algorithm without instances of skipped Frames. | ||
|
|
||
| Given the following call stack and capital "F" Frames linked list, we can apply the above algorithm. | ||
| <table> | ||
| <tr> | ||
| <th> Call Stack (growing down)</th> | ||
| <th> Capital "F" Frames Linked List </th> | ||
| </tr> | ||
| <tr> | ||
| <td> | ||
|
|
||
| ``` | ||
| Managed Call: ----------- | ||
|
|
||
| | Native | <- <A>'s SP | ||
| - | | | ||
| |-----------| <- <B>'s SP | ||
| | | | ||
| | Managed | | ||
| | | | ||
| |-----------| <- <C>'s SP | ||
| | | | ||
| | Native | | ||
| + | | | ||
| | StackBase | | ||
| ``` | ||
| </td> | ||
| <td> | ||
|
|
||
| ``` | ||
| SoftwareExceptionFrame | ||
| (Context = <B>) | ||
|
|
||
| || | ||
| \/ | ||
|
|
||
| NULL TERMINATOR | ||
| ``` | ||
|
|
||
| </td> | ||
| </tr> | ||
| </table> | ||
|
|
||
| 1. (1) Set `currContext` to the thread context `<A>`. Create a stack of Frames `frameStack`. | ||
| 2. (2) Return the `currContext` which has the threads context. | ||
| 3. (3) `currContext` is in unmanaged code (native) however, because `frameStack` is not empty, we begin processing the context. | ||
| 4. (3.1) Since `currContext` is unmanaged. We pop the SoftwareExceptionFrame from `frameStack` and use it to update `currContext`. The SoftwareExceptionFrame is holding context `<B>` which we set `currContext` to. Return the current context and go back to step 3. | ||
| 5. (3) Now `currContext` is in managed code as shown by `<B>`'s SP. Therefore, we begin to process the context. | ||
| 6. (3.1) Since `currContext` is managed, skip step 3.1. | ||
| 7. (3.2) Since `frameStack` is empty, we do not check for skipped Frames. | ||
| 8. (3.3) Unwind `currContext` a single iteration to `<C>` and return the current context. | ||
| 9. (3) `currContext` is now at unmanaged (native) code and `frameStack` is empty. Therefore we are done. | ||
|
|
||
| The following C# code could yield a stack similar to the example above: | ||
| ```csharp | ||
| void foo() | ||
| { | ||
| // Call native code or function that calls down to native. | ||
| Console.ReadLine(); | ||
| // Capture stack trace while inside native code. | ||
| } | ||
| ``` | ||
|
|
||
| #### Skipped Frame Example | ||
| The skipped Frame check is important when managed code calls managed code through an unmanaged boundary. | ||
| This occurs when calling a function marked with `[UnmanagedCallersOnly]` as an unmanaged delegate from a managed caller. | ||
| In this case, if we ignored the skipped Frame check we would miss the unmanaged boundary. | ||
|
|
||
| Given the following call stack and capital "F" Frames linked list, we can apply the above algorithm. | ||
| <table> | ||
| <tr> | ||
| <th> Call Stack (growing down)</th> | ||
| <th> Capital "F" Frames Linked List </th> | ||
| </tr> | ||
| <tr> | ||
| <td> | ||
|
|
||
| ``` | ||
| Unmanaged Call: -X-X-X-X-X- | ||
| Managed Call: ----------- | ||
| InlinedCallFrame location: [ICF] | ||
|
|
||
| | Managed | <- <A>'s SP | ||
| - | | | ||
| | | | ||
| |-X-X-X-X-X-| <- <B>'s SP | ||
| | [ICF] | | ||
| | Managed | | ||
| | | | ||
| |-----------| <- <C>'s SP | ||
| | | | ||
| | Native | | ||
| + | | | ||
| | StackBase | | ||
| ``` | ||
| </td> | ||
| <td> | ||
|
|
||
| ``` | ||
| InlinedCallFrame | ||
| (Context = <B>) | ||
|
|
||
| || | ||
| \/ | ||
|
|
||
| NULL TERMINATOR | ||
| ``` | ||
|
|
||
| </td> | ||
| </tr> | ||
| </table> | ||
|
|
||
| 1. (1) Set `currContext` to the thread context `<A>`. Create a stack of Frames `frameStack`. | ||
| 2. (2) Return the `currContext` which has the threads context. | ||
| 3. (3) Since `currContext` is in managed code, we begin to process the context. | ||
| 4. (3.1) Since `currContext` is managed, skip step 3.1. | ||
| 5. (3.2) Check for skipped Frames. Copy `currContext` into `parentContext` and unwind `parentContext` once using the Windows style unwinder. As seen from the call stack, unwinding `currContext=<A>` will yield `<C>`. We peek the top of `frameStack` and find an InlinedCallFrame (shown in call stack above as `[ICF]`). Since `parentContext`'s SP is greater than the address of `[ICF]` there are no skipped Frames. | ||
| 6. (3.3) Unwind `currContext` a single iteration to `<B>` and return the current context. | ||
| 7. (3) Since `currContext` is still in managed code, we continue processing the context. | ||
| 8. (3.1) Since `currContext` is managed, skip step 3.1. | ||
| 9. (3.2) Check for skipped Frames. Copy `currContext` into `parentContext` and unwind `parentContext` once using the Windows style unwinder. As seen from the call stack, unwinding `currContext=<B>` will yield `<C>`. We peek the top of `frameStack` and find an InlinedCallFrame (shown in call stack above as `[ICF]`). This time the the address of `[ICF]` is less than `parentContext`'s SP. Therefore we return the current context then pop the InlinedCallFrame from `frameStack` which is now empty and return to step 3. | ||
| 10. (3) Since `currContext` is still in managed code, we continue processing the context. | ||
| 11. (3.1) Since `currContext` is managed, skip step 3.1. | ||
| 12. (3.2) Since `frameStack` is empty, we do not check for skipped Frames. | ||
| 13. (3.3) Unwind `currContext` a single iteration to `<C>` and return the current context. | ||
| 14. (3) `currContext` is now at unmanaged (native) code and `frameStack` is empty. Therefore we are done. | ||
|
|
||
| The following C# code could yield a stack similar to the example above: | ||
| ```csharp | ||
| void foo() | ||
| { | ||
| var fptr = (delegate* unmanaged<void>)&bar; | ||
| fptr(); | ||
| } | ||
|
|
||
| [UnmanagedCallersOnly] | ||
| private static void bar() | ||
| { | ||
| // Do something | ||
| // Capture stack trace while in here | ||
| } | ||
| ``` | ||
|
|
||
| ### APIs | ||
|
|
||
| The majority of the contract's complexity is the stack walking algorithm (detailed above) implemented as part of `CreateStackWalk`. | ||
| The `IEnumerable<IStackDataFrame>` return value is computed lazily. | ||
|
|
||
| ```csharp | ||
| IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData); | ||
| ``` | ||
|
|
||
| The rest of the APIs convey state about the stack walk at a given point which fall out of the stack walking algorithm relatively simply. | ||
|
|
||
| `GetRawContext` Retrieves the raw Windows style thread context of the current frame as a byte array. The size and shape of the context is platform dependent. | ||
|
|
||
| * On Windows the context is defined directly in Windows header `winnt.h`. See [CONTEXT structure](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-context) for more info. | ||
| * On non-Windows platform the context's are defined in `src/coreclr/pal/inc/pal.h` and should mimic the Windows structure. | ||
|
|
||
| This context is not guaranteed to be complete. Not all capital "F" Frames store the entire context, some only store the IP/SP/FP. Therefore, at points where the context is based on these Frames it will be incomplete. | ||
| ```csharp | ||
| byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle); | ||
| ``` | ||
|
|
||
|
|
||
| `GetFrameAddress` gets the address of the current capital "F" Frame. This is only valid if the `IStackDataFrameHandle` is at a point where the context is based on a capital "F" Frame. For example, it is not valid when when the current context was created by using the stack frame unwinder. | ||
| If the Frame is not valid, returns `TargetPointer.Null`. | ||
|
|
||
| ```csharp | ||
| TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle); | ||
| ``` | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed with @jkoritzinsky , cDAC should not currently be built in the
Libraries_WithPackagesrun. This started failing because the native cDAC components were not available.