Skip to content
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

Native AOT StackFrame method info #92869

Closed
vaind opened this issue Oct 2, 2023 · 13 comments
Closed

Native AOT StackFrame method info #92869

vaind opened this issue Oct 2, 2023 · 13 comments

Comments

@vaind
Copy link

vaind commented Oct 2, 2023

When published as a native AOT binary, System.Diagnostics.StackFrame doesn't have method information (as returned by GetMethod). Interestingly, some info is available at runtime because it is printed by ToString(). It would be preferrable to expose the module & method name via appropriate methods so that we don't have to rely on parsing ToString() output. See an example output below:

_options.LogDebug("### ToString = {0}", stackFrame.ToString());
_options.LogDebug("### Has Native Image = {0}", stackFrame.HasNativeImage());
_options.LogDebug("### Native Image Base = 0x{0:x}", stackFrame.GetNativeImageBase());
_options.LogDebug("### Native Interface pointer = 0x{0:x}", stackFrame.GetNativeIP());
_options.LogDebug("### Offset = {0}", stackFrame.GetNativeOffset());
_options.LogDebug("### FileName = {0}", stackFrame.GetFileName());
_options.LogDebug("### Line no = {0}", stackFrame.GetFileLineNumber());
_options.LogDebug("### HasMethod = {0}", stackFrame.HasMethod());
_options.LogDebug("### Method = {0}", stackFrame.GetMethod());
_options.LogDebug("### ILOffset = {0}", stackFrame.GetILOffset());
ToString = System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task, ConfigureAwaitOptions) + 0x4b at offset 75 in file:line:column <filename unknown>:0:0
Has Native Image = True
Native Image Base = 0x7ff7ee540000
Native Interface pointer = 0x7ff7ee6cfb7b
Offset = 75
FileName =
Line no = 0
HasMethod = False
Method =
ILOffset = -1

As a side note, how does runtime resolve the method name in ToString() to such a nicely-looking string? If I try to symbolicate via the native debug info in the generated .PDB, the symbol name I get is S_P_CoreLib_System_Runtime_CompilerServices_TaskAwaiter__HandleNonSuccessAndDebuggerNotification

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Oct 2, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 2, 2023
@vitek-karas
Copy link
Member

By default NativeAOT apps carry enough information about each method to be able to print out the ToString you see. This can be disabled to improve application size, since it's only used for this purpose, in which case the ToString uses method address instead.

The real reason this is not implemented currently is that the GetMethod would have to return a made up MethodInfo, with only a subset of the normal functionality. That is pretty complex to implement correctly such that it provides good experience across all of reflection. You could use that MethodInfo in other reflection calls and that should fail in a reasonable way.

What is the scenario where you want the detailed breakdown of the method information from a stack trace?

@ghost
Copy link

ghost commented Oct 2, 2023

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

When published as a native AOT binary, System.Diagnostics.StackFrame doesn't have method information (as returned by GetMethod). Interestingly, some info is available at runtime because it is printed by ToString(). It would be preferrable to expose the module & method name via appropriate methods so that we don't have to rely on parsing ToString() output. See an example output below:

_options.LogDebug("### ToString = {0}", stackFrame.ToString());
_options.LogDebug("### Has Native Image = {0}", stackFrame.HasNativeImage());
_options.LogDebug("### Native Image Base = 0x{0:x}", stackFrame.GetNativeImageBase());
_options.LogDebug("### Native Interface pointer = 0x{0:x}", stackFrame.GetNativeIP());
_options.LogDebug("### Offset = {0}", stackFrame.GetNativeOffset());
_options.LogDebug("### FileName = {0}", stackFrame.GetFileName());
_options.LogDebug("### Line no = {0}", stackFrame.GetFileLineNumber());
_options.LogDebug("### HasMethod = {0}", stackFrame.HasMethod());
_options.LogDebug("### Method = {0}", stackFrame.GetMethod());
_options.LogDebug("### ILOffset = {0}", stackFrame.GetILOffset());
ToString = System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task, ConfigureAwaitOptions) + 0x4b at offset 75 in file:line:column <filename unknown>:0:0
Has Native Image = True
Native Image Base = 0x7ff7ee540000
Native Interface pointer = 0x7ff7ee6cfb7b
Offset = 75
FileName =
Line no = 0
HasMethod = False
Method =
ILOffset = -1

As a side note, how does runtime resolve the method name in ToString() to such a nicely-looking string? If I try to symbolicate via the native debug info in the generated .PDB, the symbol name I get is S_P_CoreLib_System_Runtime_CompilerServices_TaskAwaiter__HandleNonSuccessAndDebuggerNotification

Author: vaind
Assignees: -
Labels:

untriaged, area-NativeAOT-coreclr, needs-area-label

Milestone: -

@vitek-karas vitek-karas removed the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Oct 2, 2023
@vaind
Copy link
Author

vaind commented Oct 2, 2023

Thanks for the quick response @vitek-karas

What is the scenario where you want the detailed breakdown of the method information from a stack trace?

To show stack trace for error-reporting SDKs, namely Sentry. It's possible to do ("offline") symbolication on the server using the generated PDB but, as I've mentioned in the original post, the symbol name doesn't look as good as the C# method info returned by ToString().

AFAIK the only way to get the "nice" method name at this time is to try and parse ToString() output on the client. Or is there another way to get access to the "information about each method to be able to print out the ToString you see"?
I understand this can be disabled by setting StackTraceSupport=false in which case it's understandable to just use what's in the PDB, but if the info is available, it would be preferrable to use it.

Alternatively, is it possible to get the correct c# module, class and method names from PDB(or DWARF for linux and macOS)?

@MichalStrehovsky
Copy link
Member

To show stack trace for error-reporting SDKs, namely Sentry. It's possible to do ("offline") symbolication on the server using the generated PDB but, as I've mentioned in the original post, the symbol name doesn't look as good as the C# method info returned by ToString().

Is offline symbolication the approach that Sentry uses for IL2CPP? If so, similar approach could be used to generate stacks for Native AOT. I assume the problem right now is that the debug information generated by native AOT still doesn't look enough like C++. The name mangling scheme native AOT uses is not reversible.

@vaind
Copy link
Author

vaind commented Oct 3, 2023

Is offline symbolication the approach that Sentry uses for IL2CPP? If so, similar approach could be used to generate stacks for Native AOT.

For IL2CPP (in Unity) we go through a couple more hoops that are not necessary with .NET Native AOT - I've already successfully symbolicated stack traces based on the list of loaded native images (from native APIs) and the image & symbol addresses as provided by the .net stacktrace runtime API. So that all works as expected.

I assume the problem right now is that the debug information generated by native AOT still doesn't look enough like C++. The name mangling scheme native AOT uses is not reversible.

Exactly, that's why I'm looking at improving the information from PDB with whatever is available at runtime. This info (before mangling) is available at runtime but as I've mentioned in the original post, it's only part of the ToString() result.
Since you're saying " The name mangling scheme native AOT uses is not reversible." - how would you say we can get the demangled info at runtime (short of parsing ToString() output)?

@MichalStrehovsky
Copy link
Member

Exactly, that's why I'm looking at improving the information from PDB with whatever is available at runtime. This info (before mangling) is available at runtime but as I've mentioned in the original post, it's only part of the ToString() result.
Since you're saying " The name mangling scheme native AOT uses is not reversible." - how would you say we can get the demangled info at runtime (short of parsing ToString() output)?

Parsing ToString is the only thing that will work for .NET 7/8.

For .NET 9, we could consider these, in order of preference:

  1. Fix debug info emission to work better. We already generate some information about types and methods that supplements the mangled method name. The compiler already generates something (the code is around here) but I've never seen it surfaced anywhere in a debugger. The people who originally wrote all of this code are long gone and all of this is now owned by "everyone" so it's not owned by anyone and nobody is up-to-speed on it. If we want to work on it, we'd want to wait for Discussion: ObjWriter in C# #77178.
  2. Introduce a new API on StackFrame that would let one obtain the information that is needed, but is not a MethodBase. (e.g. do we just need a stringified type name and stringified method name? Do we want stringified parameter types?).
  3. Start generating fake MethodBase for these that only contain the information to the extent that compiler keeps track of it. E.g. asking for method flags would throw, asking for declaring type would return some Type that will also be severely messed up (not knowing its assembly, or if its nested, etc.), etc.

3 is my least preferred option because we'd need to very carefully document the small subset of things that work reliably. one can absolutely get a 100% usable MethodBase if the method on the stack frame was considered reflection-visible by the compiler. But anything else will get the hobbled MethodBase. That makes it somewhat more annoying to test. We do not want to add more stuff to the stack trace metadata to make the MethodBase more useful because stack trace metadata already accounts for almost 10% of the executable size (try publishing an app with native AOT and then again with <PropertyGroup><StackTraceSupport>false</StackTraceSupport></PropertyGroup> to see the overhead even of this minimal metadata).

@vaind
Copy link
Author

vaind commented Oct 3, 2023

Thanks @MichalStrehovsky

I completely agree with your reasoning and would be looking forward to get this info in the future. In the meantime, I'll resort to parsing ToString() output.

@vaind
Copy link
Author

vaind commented Oct 4, 2023

When looking at ToString() for various frames, I've found that it won't give us the package information unless the method info is missing. See the following code:

public virtual string CreateStackTraceString(IntPtr ip, bool includeFileInfo, out bool isStackTraceHidden)
{
string methodName = GetMethodName(ip, out IntPtr methodStart, out isStackTraceHidden);
if (methodName != null)
{
if (ip != methodStart)
{
methodName = $"{methodName} + 0x{(ip - methodStart):x}";
}
return methodName;
}
// If we don't have precise information, try to map it at least back to the right module.
string moduleFullFileName = RuntimeAugments.TryGetFullPathToApplicationModule(ip, out IntPtr moduleBase);
// Without any callbacks or the ability to map ip correctly we better admit that we don't know
if (string.IsNullOrEmpty(moduleFullFileName))
{
return "<unknown>";
}
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(moduleFullFileName.AsSpan());
int rva = (int)(ip - moduleBase);
return $"{fileNameWithoutExtension}!<BaseAddress>+0x{rva:x}";
}

It would be useful to have the package name (not just the namespace & class) whenever available. What's the chance of this changing?

@MichalStrehovsky
Copy link
Member

When you say "package information", do you mean the native module name and native address?

If so, you can force stack traces to only report things in that format by setting this AppContext switch:

AppContext.TryGetSwitch("Diagnostics.DisableMetadataStackTraceResolution", out bool disableMetadata);

(Either call AppContext.SetSwitch programmatically at startup or places this value as a RuntimeHostConfigurationOption into the MSBuild project file)

I just realized we don't have any testing for this switch. If you do take a dependency on it, please submit a test to https://github.com/dotnet/runtime/tree/main/src/tests/nativeaot/SmokeTests/StackTraceMetadata to test this so that we don't regress it (the test can simply set the appcontext switch and inspect a stack trace afterwards).

@vaind
Copy link
Author

vaind commented Oct 5, 2023

When you say "package information", do you mean the native module name and native address?

I meant the module name.

As for the switch - that would disable the class+namespace+method name resolution right? That's not what I'm looking for. Instead, I'd like to be able to get the class+namespace+method name and the module name at the same time.

As you can see in the CreateStackTraceString() above, if a method name is available, we don't get any info about the module. And with all these calls being internal, the only way I can get any info is ToString(), as we've already established.

@MichalStrehovsky
Copy link
Member

As for the switch - that would disable the class+namespace+method name resolution right? That's not what I'm looking for. Instead, I'd like to be able to get the class+namespace+method name and the module name at the same time.

The module name we print is not the module name you think it is. It is the name of the native module, not the original source assembly.

The original source assembly name gets dropped during native compilation because we don't have any use for it (it's not part of the "official" stack trace format string). We could potentially include it if we go with option 2 in the above comment. (It might be a good exercise if you could propose a shape for this API that you would need).

@vaind
Copy link
Author

vaind commented Oct 5, 2023

Ah, that clears things up then, thanks. I'll ignore it then.

@MichalStrehovsky
Copy link
Member

I wrote up a proposal for trim-safe/aot-safe version of this API: #96528.

I'm going to close this since it basically supersedes this.

@MichalStrehovsky MichalStrehovsky closed this as not planned Won't fix, can't repro, duplicate, stale Jan 5, 2024
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jan 5, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Feb 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Archived in project
Development

No branches or pull requests

3 participants