Skip to content

Commit bef5696

Browse files
committed
decompile support v1
1 parent a9a5fd9 commit bef5696

File tree

9 files changed

+336
-31
lines changed

9 files changed

+336
-31
lines changed

src/SharpDbg.Application/DebugAdapter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private void SubscribeToDebuggerEvents()
7272
});
7373
};
7474

75-
_debugger.OnStopped2 += (threadId, filePath, line, reason) =>
75+
_debugger.OnStopped2 += (threadId, filePath, line, reason, decompiledSourceInfo) =>
7676
{
7777
var source = new Source { Path = filePath };
7878
var stoppedEvent = new StoppedEvent
@@ -83,6 +83,7 @@ private void SubscribeToDebuggerEvents()
8383
};
8484
stoppedEvent.AdditionalProperties["source"] = JToken.FromObject(source);
8585
stoppedEvent.AdditionalProperties["line"] = JToken.FromObject(line);
86+
stoppedEvent.AdditionalProperties["decompiledSourceInfo"] = decompiledSourceInfo is null ? null : JToken.FromObject(decompiledSourceInfo);
8687
Protocol.SendEvent(stoppedEvent);
8788
};
8889

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System.Collections.Immutable;
2+
using System.Reflection.Metadata;
3+
using System.Reflection.PortableExecutable;
4+
using ICSharpCode.Decompiler.Metadata;
5+
6+
namespace SharpDbg.Infrastructure.Debugger.Decompilation;
7+
// 🤖
8+
internal sealed class DebuggingAssemblyResolver(List<string> modulePaths) : IAssemblyResolver
9+
{
10+
private readonly List<string> _modulePaths = modulePaths;
11+
private readonly record struct AssemblyIdentity(string Name, Version Version, ImmutableArray<byte> PublicKeyToken);
12+
13+
public Task<MetadataFile?> ResolveAsync(IAssemblyReference name) => Task.FromResult(Resolve(name));
14+
public Task<MetadataFile?> ResolveModuleAsync(MetadataFile mainModule, string moduleName) => Task.FromResult(ResolveModule(mainModule, moduleName));
15+
16+
public MetadataFile? Resolve(IAssemblyReference name)
17+
{
18+
string? exactMatch = null;
19+
string? highestVersionMatch = null;
20+
Version? highestVersion = null;
21+
22+
foreach (var path in _modulePaths)
23+
{
24+
if (!File.Exists(path)) continue;
25+
26+
var identity = TryReadAssemblyIdentity(path);
27+
if (identity is null) continue;
28+
29+
if (!string.Equals(identity.Value.Name, name.Name, StringComparison.OrdinalIgnoreCase)) continue;
30+
31+
var requestedToken = name.PublicKeyToken ?? [];
32+
var identityToken = identity.Value.PublicKeyToken;
33+
34+
if (identity.Value.Version == name.Version && identityToken.SequenceEqual(requestedToken))
35+
{
36+
exactMatch = path;
37+
break;
38+
}
39+
40+
if (highestVersion is null || identity.Value.Version > highestVersion)
41+
{
42+
highestVersion = identity.Value.Version;
43+
highestVersionMatch = path;
44+
}
45+
}
46+
47+
var chosen = exactMatch ?? highestVersionMatch;
48+
if (chosen is null) return null;
49+
50+
return new PEFile(chosen, PEStreamOptions.PrefetchMetadata);
51+
}
52+
53+
public MetadataFile? ResolveModule(MetadataFile mainModule, string moduleName)
54+
{
55+
// Multi-module assemblies: look for the module file next to the main module.
56+
var baseDirectory = Path.GetDirectoryName(mainModule.FileName);
57+
if (baseDirectory is null)
58+
return null;
59+
60+
var moduleFileName = Path.Combine(baseDirectory, moduleName);
61+
if (!File.Exists(moduleFileName))
62+
return null;
63+
64+
return new PEFile(moduleFileName, PEStreamOptions.PrefetchMetadata);
65+
}
66+
67+
private static AssemblyIdentity? TryReadAssemblyIdentity(string path)
68+
{
69+
try
70+
{
71+
using var peReader = new PEReader(File.OpenRead(path));
72+
if (peReader.HasMetadata is false) return null;
73+
74+
var metadataReader = peReader.GetMetadataReader();
75+
var assemblyDef = metadataReader.GetAssemblyDefinition();
76+
77+
var name = metadataReader.GetString(assemblyDef.Name);
78+
var version = assemblyDef.Version;
79+
var publicKeyToken = ComputePublicKeyToken(metadataReader, assemblyDef);
80+
81+
return new AssemblyIdentity(name, version, publicKeyToken);
82+
}
83+
catch
84+
{
85+
return null;
86+
}
87+
}
88+
89+
// Works for now, why not just use MVID?
90+
private static ImmutableArray<byte> ComputePublicKeyToken(MetadataReader reader, AssemblyDefinition assemblyDef)
91+
{
92+
var publicKeyOrToken = reader.GetBlobBytes(assemblyDef.PublicKey);
93+
if (publicKeyOrToken.Length == 0)
94+
return ImmutableArray<byte>.Empty;
95+
96+
// If this is a full public key (not already a token), hash it down to an 8-byte token.
97+
if ((assemblyDef.Flags & System.Reflection.AssemblyFlags.PublicKey) != 0 &&
98+
publicKeyOrToken.Length > 8)
99+
{
100+
using var sha1 = System.Security.Cryptography.SHA1.Create();
101+
var hash = sha1.ComputeHash(publicKeyOrToken);
102+
// Public key token = last 8 bytes of SHA-1 hash, reversed
103+
var token = new byte[8];
104+
for (int i = 0; i < 8; i++)
105+
token[i] = hash[hash.Length - 1 - i];
106+
return [.. token];
107+
}
108+
109+
return [.. publicKeyOrToken];
110+
}
111+
}

src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public partial class ManagedDebugger : IDisposable
2727

2828
public event Action<int, string>? OnStopped;
2929
// ThreadId, FilePath, Line, Reason
30-
public event Action<int, string, int, string>? OnStopped2;
30+
public event Action<int, string, int, string, DecompiledSourceInfo?>? OnStopped2;
3131
public event Action<int>? OnContinued;
3232
public event Action? OnExited;
3333
public event Action? OnTerminated;

src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using ClrDebug;
1+
using System.Diagnostics;
2+
using ClrDebug;
23
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator;
34
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter;
45

@@ -66,8 +67,10 @@ private void HandleModuleLoaded(object? sender, LoadModuleCorDebugManagedCallbac
6667
_logger?.Invoke($" Error loading symbols for {moduleName}: {ex.Message}");
6768
}
6869

69-
// Store module info
70-
var moduleInfo = new ModuleInfo(corModule, modulePath, symbolReader);
70+
// EnC is enabled for assemblies/projects that are authored by the user, so we can use it as a heuristic to determine if this is user code or system code.
71+
var isUserCode = corModule.JITCompilerFlags is CorDebugJITCompilerFlags.CORDEBUG_JIT_DISABLE_OPTIMIZATION or CorDebugJITCompilerFlags.CORDEBUG_JIT_ENABLE_ENC;
72+
73+
var moduleInfo = new ModuleInfo(corModule, modulePath, symbolReader, isUserCode);
7174
_modules[baseAddress] = moduleInfo;
7275

7376
if (moduleName is "System.Private.CoreLib.dll")
@@ -146,7 +149,7 @@ private async void HandleBreakpoint(object? sender, BreakpointCorDebugManagedCal
146149
var managedBreakpoint = _breakpointManager.FindByCorBreakpoint(functionBreakpoint.Raw);
147150
ArgumentNullException.ThrowIfNull(managedBreakpoint);
148151
IsRunning = false;
149-
OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, "breakpoint");
152+
OnStopped2?.Invoke(corThread.Id, managedBreakpoint.FilePath, managedBreakpoint.Line, "breakpoint", null);
150153
}
151154
catch (Exception e)
152155
{
@@ -166,15 +169,20 @@ private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallb
166169
var stepper = _stepper ?? throw new InvalidOperationException("No stepper found for step complete");
167170
stepper.Deactivate(); // I really don't know if its necessary to deactivate the steppers once done
168171
_stepper = null;
169-
var symbolReader = _modules[ilFrame.Function.Module.BaseAddress].SymbolReader;
170-
if (symbolReader is null)
172+
var module = _modules[ilFrame.Function.Module.BaseAddress];
173+
var sourceInfo = GetSourceInfoAtFrame(ilFrame);
174+
if (sourceInfo is null)
171175
{
172-
// We don't have symbols, but we're going to step in, in case this code calls user code that would be missed if we stepped out or over
173-
// Alternative is to use JMC true - we'll never stop in non-user code, so in theory symbolReader would never be null
174-
SetupStepper(corThread, AsyncStepper.StepType.StepIn);
176+
// sourceInfo will be null if we could not find a PDB for the module
177+
// Bottom line - if we have no PDB, we have no source info, and there is no possible way for the user to map the stop location to a source file/line
178+
// (Until we implement Source Link and/or Decompilation support)
179+
// So for now, if this occurs, we are going to do a step out to get us back to a stop location with source info
180+
// TODO: This should probably be more sophisticated - mark the CorDebugFunction as non user code - `JMCStatus = false`, enable JMC for the stepper and then step over, in case the non user code calls user code, e.g. LINQ methods
181+
SetupStepper(corThread, AsyncStepper.StepType.StepOver);
175182
Continue();
176183
return;
177184
}
185+
var symbolReader = module.SymbolReader ?? throw new UnreachableException("Source info was found, but no symbol reader is available for the module - this should never happen");
178186

179187
var (currentIlOffset, nextUserCodeIlOffset) = symbolReader.GetFrameCurrentIlOffsetAndNextUserCodeIlOffset(ilFrame);
180188
if (stepCompleteEventArgs.Reason is CorDebugStepReason.STEP_CALL && currentIlOffset < nextUserCodeIlOffset)
@@ -199,21 +207,8 @@ private void HandleStepComplete(object? sender, StepCompleteCorDebugManagedCallb
199207
}
200208
}
201209

202-
var sourceInfo = GetSourceInfoAtFrame(ilFrame);
203-
if (sourceInfo is null)
204-
{
205-
// sourceInfo will be null if we could not find a PDB for the module
206-
// Bottom line - if we have no PDB, we have no source info, and there is no possible way for the user to map the stop location to a source file/line
207-
// (Until we implement Source Link and/or Decompilation support)
208-
// So for now, if this occurs, we are going to do a step out to get us back to a stop location with source info
209-
// TODO: This should probably be more sophisticated - mark the CorDebugFunction as non user code - `JMCStatus = false`, enable JMC for the stepper and then step over, in case the non user code calls user code, e.g. LINQ methods
210-
SetupStepper(corThread, AsyncStepper.StepType.StepOver);
211-
Continue();
212-
return;
213-
}
214-
215-
var (sourceFilePath, line, _) = sourceInfo.Value;
216-
OnStopped2?.Invoke(corThread.Id, sourceFilePath, line, "step");
210+
var (sourceFilePath, line, _, decompiledSourceInfo) = sourceInfo.Value;
211+
OnStopped2?.Invoke(corThread.Id, sourceFilePath, line, "step", decompiledSourceInfo);
217212
}
218213

219214
private void HandleBreak(object? sender,
Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,172 @@
1-
using ClrDebug;
1+
using System.Reflection.PortableExecutable;
2+
using ClrDebug;
3+
using ICSharpCode.Decompiler;
4+
using ICSharpCode.Decompiler.CSharp;
5+
using ICSharpCode.Decompiler.CSharp.Transforms;
6+
using ICSharpCode.Decompiler.DebugInfo;
7+
using ICSharpCode.Decompiler.Metadata;
8+
using SharpDbg.Infrastructure.Debugger.Decompilation;
29

310
namespace SharpDbg.Infrastructure.Debugger;
411

12+
public readonly record struct SourceInfo(string FilePath, int StartLine, int StartColumn, DecompiledSourceInfo? DecompilationReproductionInfo);
13+
public class DecompiledSourceInfo
14+
{
15+
public required string TypeFullName { get; init; }
16+
public required AssemblyPathAndMvid Assembly { get; init; }
17+
public required string CallingUserCodeAssemblyPath { get; init; }
18+
}
19+
public record struct AssemblyPathAndMvid(string AssemblyPath, Guid Mvid);
520
public partial class ManagedDebugger
621
{
722
/// This appears to be 1 based, ie requires no adjustment when returned to the user
8-
private (string FilePath, int StartLine, int StartColumn)? GetSourceInfoAtFrame(CorDebugFrame frame)
23+
private SourceInfo? GetSourceInfoAtFrame(CorDebugFrame frame)
924
{
1025
if (frame is not CorDebugILFrame ilFrame)
1126
throw new InvalidOperationException("Active frame is not an IL frame");
1227
var function = ilFrame.Function;
1328
var module = _modules[function.Module.BaseAddress];
29+
if (module.SymbolReader is null)
30+
{
31+
if (module.IsUserCode) throw new InvalidOperationException("The module we are decompiling is user code - this should never happen, we should only be decompiling non user code modules");
32+
// No PDB on disk — generate one via decompilation and update the module entry
33+
var result = GetCachedOrGeneratePdb(module);
34+
if (result is not null)
35+
{
36+
module.SymbolReader = result;
37+
module.SymbolReaderFromDecompiled = true;
38+
}
39+
}
40+
1441
if (module.SymbolReader is not null)
1542
{
1643
var ilOffset = ilFrame.IP.pnOffset;
1744
var methodToken = function.Token;
1845
var sourceInfo = module.SymbolReader.GetSourceLocationForOffset(methodToken, ilOffset);
1946
if (sourceInfo != null)
2047
{
21-
return (sourceInfo.Value.sourceFilePath, sourceInfo.Value.startLine, sourceInfo.Value.startColumn);
48+
DecompiledSourceInfo? decompiledSourceInfo = null;
49+
if (module.SymbolReaderFromDecompiled)
50+
{
51+
var metadataImport = module.Module.GetMetaDataInterface().MetaDataImport;
52+
var mvid = metadataImport.ScopeProps.pmvid;
53+
var containingTypeDef = metadataImport.GetMethodProps(methodToken).pClass;
54+
var typeProps = metadataImport.GetTypeDefProps(containingTypeDef);
55+
var typeName = typeProps.szTypeDef;
56+
57+
string? callingUserCodeAssemblyPath = null;
58+
var caller = frame.Caller;
59+
while (callingUserCodeAssemblyPath is null)
60+
{
61+
if (caller is null) break;
62+
63+
if (caller is CorDebugILFrame callerIlFrame)
64+
{
65+
var callerFunction = callerIlFrame.Function;
66+
var callerModule = _modules[callerFunction.Module.BaseAddress];
67+
if (callerModule.IsUserCode)
68+
{
69+
callingUserCodeAssemblyPath = callerModule.ModulePath;
70+
break;
71+
}
72+
}
73+
74+
caller = caller.Caller;
75+
}
76+
77+
decompiledSourceInfo = new DecompiledSourceInfo
78+
{
79+
TypeFullName = typeName,
80+
Assembly = new AssemblyPathAndMvid(module.ModulePath, mvid),
81+
CallingUserCodeAssemblyPath = callingUserCodeAssemblyPath ?? throw new InvalidOperationException("Could not find a user code caller in the call stack")
82+
};
83+
}
84+
85+
return new SourceInfo(sourceInfo.Value.sourceFilePath, sourceInfo.Value.startLine, sourceInfo.Value.startColumn, decompiledSourceInfo);
2286
}
2387
}
2488

2589
return null;
2690
}
91+
92+
private SymbolReader? GetCachedOrGeneratePdb(ModuleInfo moduleInfo)
93+
{
94+
var sharpIdeSymbolCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "SharpIdeSymbolCache");
95+
var metadataImport = moduleInfo.Module.GetMetaDataInterface().MetaDataImport;
96+
var mvid = metadataImport.ScopeProps.pmvid;
97+
var assemblyName = Path.GetFileNameWithoutExtension(moduleInfo.ModuleName);
98+
var pdbPath = Path.Combine(sharpIdeSymbolCachePath, assemblyName, mvid.ToString(), $"{assemblyName}.decompiled.pdb");
99+
if (File.Exists(pdbPath))
100+
{
101+
var symbolReader = SymbolReader.TryLoadWithPdbPath(moduleInfo.ModulePath, pdbPath);
102+
if (symbolReader is null)
103+
{
104+
_logger?.Invoke($"GetCachedOrGeneratePdb: SymbolReader could not load cached PDB '{pdbPath}'");
105+
return null;
106+
}
107+
return symbolReader;
108+
}
109+
return GeneratePdb(moduleInfo, pdbPath);
110+
}
111+
112+
113+
private SymbolReader? GeneratePdb(ModuleInfo moduleInfo, string pdbPathToWriteTo)
114+
{
115+
var assemblyPath = moduleInfo.ModulePath;
116+
if (!File.Exists(assemblyPath)) return null;
117+
118+
var allModulePaths = _modules.Values.Select(m => m.ModulePath).Where(p => !string.IsNullOrEmpty(p)).ToList();
119+
var resolver = new DebuggingAssemblyResolver(allModulePaths);
120+
121+
PEFile file;
122+
try
123+
{
124+
file = new PEFile(assemblyPath, PEStreamOptions.PrefetchEntireImage);
125+
}
126+
catch (Exception ex)
127+
{
128+
_logger?.Invoke($"GeneratePdb: failed to open PE file '{assemblyPath}': {ex.Message}");
129+
return null;
130+
}
131+
132+
using (file)
133+
{
134+
var decompilerSettings = new DecompilerSettings();
135+
var decompiler = new CSharpDecompiler(file, resolver, decompilerSettings)
136+
{
137+
AstTransforms = {
138+
new TransformFieldAndConstructorInitializers(),
139+
new AddXmlDocumentationTransform(),
140+
new EscapeInvalidIdentifiers(),
141+
new FixNameCollisions(),
142+
new ReplaceMethodCallsWithOperators()
143+
}
144+
};
145+
146+
_logger?.Invoke($"GeneratePdb: writing PDB to '{pdbPathToWriteTo}' for '{assemblyPath}'");
147+
try
148+
{
149+
var pdbDirectory = Path.GetDirectoryName(pdbPathToWriteTo)!;
150+
if (!Directory.Exists(pdbDirectory)) Directory.CreateDirectory(pdbDirectory);
151+
using var pdbStream = File.Create(pdbPathToWriteTo);
152+
// noLogo: true until https://github.com/icsharpcode/ILSpy/pull/3667 is merged
153+
PortablePdbWriter.WritePdb(file, decompiler, decompilerSettings, pdbStream, noLogo: true);
154+
}
155+
catch (Exception ex)
156+
{
157+
_logger?.Invoke($"GeneratePdb: exception writing PDB: {ex}");
158+
return null;
159+
}
160+
161+
var symbolReader = SymbolReader.TryLoadWithPdbPath(assemblyPath, pdbPathToWriteTo);
162+
if (symbolReader is null)
163+
{
164+
_logger?.Invoke($"GeneratePdb: SymbolReader could not load generated PDB '{pdbPathToWriteTo}'");
165+
return null;
166+
}
167+
168+
_logger?.Invoke($"GeneratePdb: successfully loaded generated PDB for '{Path.GetFileName(assemblyPath)}'");
169+
return symbolReader;
170+
}
171+
}
27172
}

0 commit comments

Comments
 (0)