671 lines
19 KiB
C#
671 lines
19 KiB
C#
// <file>
|
|
// <copyright see="prj:///doc/copyright.txt"/>
|
|
// <license see="prj:///doc/license.txt"/>
|
|
// <owner name="David Srbecký" email="dsrbecky@gmail.com"/>
|
|
// <version>$Revision: 2285 $</version>
|
|
// </file>
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.IO;
|
|
|
|
using Debugger.Wrappers.CorDebug;
|
|
using Debugger.Wrappers.CorSym;
|
|
using Debugger.Wrappers.MetaData;
|
|
|
|
namespace Debugger
|
|
{
|
|
/// <summary>
|
|
/// A function (or also a method or frame) which is being executed on
|
|
/// some thread. Use to obtain arguments or local variables.
|
|
/// </summary>
|
|
public class Function: DebuggerObject, IExpirable
|
|
{
|
|
Process process;
|
|
|
|
Module module;
|
|
ICorDebugFunction corFunction;
|
|
ICorDebugILFrame corILFrame;
|
|
object corILFramePauseSession;
|
|
|
|
Stepper stepOutStepper;
|
|
|
|
bool steppedOut = false;
|
|
Thread thread;
|
|
FrameID frameID;
|
|
|
|
MethodProps methodProps;
|
|
|
|
/// <summary> The process in which this function is executed </summary>
|
|
[Debugger.Tests.Ignore]
|
|
public Process Process {
|
|
get {
|
|
return process;
|
|
}
|
|
}
|
|
|
|
/// <summary> The name of the function (eg "ToString") </summary>
|
|
public string Name {
|
|
get {
|
|
return methodProps.Name;
|
|
}
|
|
}
|
|
|
|
/// <summary> Metadata token of the function </summary>
|
|
[Debugger.Tests.Ignore]
|
|
public uint Token {
|
|
get {
|
|
return methodProps.Token;
|
|
}
|
|
}
|
|
|
|
/// <summary> A module in which the function is defined </summary>
|
|
[Debugger.Tests.SummaryOnly]
|
|
public Module Module {
|
|
get {
|
|
return module;
|
|
}
|
|
}
|
|
|
|
/// <summary> A thread in which the function is executed </summary>
|
|
[Debugger.Tests.Ignore]
|
|
public Thread Thread {
|
|
get {
|
|
return thread;
|
|
}
|
|
}
|
|
|
|
/// <summary> True if the function is static </summary>
|
|
public bool IsStatic {
|
|
get {
|
|
return methodProps.IsStatic;
|
|
}
|
|
}
|
|
|
|
/// <summary> True if the function has symbols defined.
|
|
/// (That is has accesss to the .pdb file) </summary>
|
|
public bool HasSymbols {
|
|
get {
|
|
return GetSegmentForOffet(0) != null;
|
|
}
|
|
}
|
|
|
|
/// <summary> The class that defines this function </summary>
|
|
internal ICorDebugClass ContaingClass { // TODO: Use DebugType
|
|
get {
|
|
return corFunction.Class;
|
|
}
|
|
}
|
|
|
|
/// <summary> True if function stepped out and is not longer valid. </summary>
|
|
public bool HasExpired {
|
|
get {
|
|
return steppedOut || Module.Unloaded;
|
|
}
|
|
}
|
|
|
|
/// <summary> Occurs when function expires and is no longer usable </summary>
|
|
public event EventHandler Expired;
|
|
|
|
/// <summary> Is called when function expires and is no longer usable </summary>
|
|
internal protected virtual void OnExpired(EventArgs e)
|
|
{
|
|
if (!steppedOut) {
|
|
steppedOut = true;
|
|
process.TraceMessage("Function " + this.ToString() + " expired");
|
|
if (Expired != null) {
|
|
Expired(this, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal Function(Thread thread, FrameID frameID, ICorDebugILFrame corILFrame)
|
|
{
|
|
this.process = thread.Process;
|
|
this.thread = thread;
|
|
this.frameID = frameID;
|
|
this.CorILFrame = corILFrame;
|
|
corFunction = corILFrame.Function;
|
|
module = process.GetModule(corFunction.Module);
|
|
|
|
methodProps = module.MetaData.GetMethodProps(corFunction.Token);
|
|
|
|
// Force some callback when function steps out so that we can expire it
|
|
stepOutStepper = new Stepper(this, "Function Tracker");
|
|
stepOutStepper.StepOut();
|
|
stepOutStepper.PauseWhenComplete = false;
|
|
|
|
process.TraceMessage("Function " + this.ToString() + " created");
|
|
}
|
|
|
|
/// <summary> Returns diagnostic description of the frame </summary>
|
|
public override string ToString()
|
|
{
|
|
return methodProps.Name + "(" + frameID.ToString() + ")";
|
|
}
|
|
|
|
internal ICorDebugILFrame CorILFrame {
|
|
get {
|
|
if (HasExpired) throw new DebuggerException("Function has expired");
|
|
if (corILFramePauseSession != process.PauseSession) {
|
|
CorILFrame = thread.GetFrameAt(frameID).As<ICorDebugILFrame>();
|
|
}
|
|
return corILFrame;
|
|
}
|
|
set {
|
|
if (value == null) throw new DebuggerException("Can not set frame to null");
|
|
corILFrame = value;
|
|
corILFramePauseSession = process.PauseSession;
|
|
}
|
|
}
|
|
|
|
internal uint corInstructionPtr {
|
|
get {
|
|
uint corInstructionPtr;
|
|
CorILFrame.GetIP(out corInstructionPtr);
|
|
return corInstructionPtr;
|
|
}
|
|
}
|
|
|
|
public ISymUnmanagedReader symReader {
|
|
get {
|
|
if (module.SymbolsLoaded == false) return null;
|
|
if (module.SymReader == null) return null;
|
|
return module.SymReader;
|
|
|
|
}
|
|
}
|
|
|
|
public ISymUnmanagedMethod symMethod {
|
|
get {
|
|
if (symReader == null) {
|
|
return null;
|
|
} else {
|
|
try {
|
|
return symReader.GetMethod(methodProps.Token);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary> Step into next instruction </summary>
|
|
public void StepInto()
|
|
{
|
|
Step(true);
|
|
}
|
|
|
|
/// <summary> Step over next instruction </summary>
|
|
public void StepOver()
|
|
{
|
|
Step(false);
|
|
}
|
|
|
|
/// <summary> Step out of the function </summary>
|
|
public void StepOut()
|
|
{
|
|
new Stepper(this, "Function step out").StepOut();
|
|
process.Continue();
|
|
}
|
|
|
|
|
|
private unsafe void Step(bool stepIn)
|
|
{
|
|
if (Module.SymbolsLoaded == false) {
|
|
throw new DebuggerException("Unable to step. No symbols loaded.");
|
|
}
|
|
SourcecodeSegment nextSt;
|
|
nextSt = NextStatement;
|
|
if (nextSt == null) {
|
|
throw new DebuggerException("Unable to step. Next statement not aviable");
|
|
}
|
|
|
|
if (stepIn) {
|
|
new Stepper(this, "Function step in").StepIn(nextSt.StepRanges);
|
|
// Without JMC step in which ends in code without symblols is cotinued.
|
|
// The next step over ensures that we at least do step over.
|
|
new Stepper(this, "Safety step over").StepOver(nextSt.StepRanges);
|
|
} else {
|
|
new Stepper(this, "Function step over").StepOver(nextSt.StepRanges);
|
|
}
|
|
|
|
process.Continue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the information about the next statement to be executed.
|
|
///
|
|
/// Returns null on error.
|
|
/// </summary>
|
|
public SourcecodeSegment NextStatement {
|
|
get {
|
|
return GetSegmentForOffet(corInstructionPtr);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns null on error.
|
|
///
|
|
/// 'ILStart <= ILOffset <= ILEnd' and this range includes at least
|
|
/// the returned area of source code. (May incude some extra compiler generated IL too)
|
|
/// </summary>
|
|
public SourcecodeSegment GetSegmentForOffet(uint offset)
|
|
{
|
|
ISymUnmanagedMethod symMethod;
|
|
|
|
symMethod = this.symMethod;
|
|
if (symMethod == null) {
|
|
return null;
|
|
}
|
|
|
|
uint sequencePointCount = symMethod.SequencePointCount;
|
|
SequencePoint[] sequencePoints = symMethod.SequencePoints;
|
|
|
|
SourcecodeSegment retVal = new SourcecodeSegment();
|
|
|
|
// Get i for which: offsets[i] <= offset < offsets[i + 1]
|
|
// or fallback to first element if offset < offsets[0]
|
|
for (int i = (int)sequencePointCount - 1; i >= 0; i--) // backwards
|
|
if (sequencePoints[i].Offset <= offset || i == 0) {
|
|
// Set inforamtion about current IL range
|
|
int codeSize = (int)corFunction.ILCode.Size;
|
|
|
|
retVal.ILOffset = (int)offset;
|
|
retVal.ILStart = (int)sequencePoints[i].Offset;
|
|
retVal.ILEnd = (i + 1 < sequencePointCount) ? (int)sequencePoints[i+1].Offset : codeSize;
|
|
|
|
// 0xFeeFee means "code generated by compiler"
|
|
// If we are in generated sequence use to closest real one instead,
|
|
// extend the ILStart and ILEnd to include the 'real' sequence
|
|
int k = i;
|
|
// Look ahead for 'real' sequence
|
|
while (i + 1 < sequencePointCount && sequencePoints[i].Line == 0xFeeFee){
|
|
i++;
|
|
retVal.ILEnd = (i + 1 < sequencePointCount) ? (int)sequencePoints[i+1].Offset : codeSize;
|
|
}
|
|
// Look back for 'real' sequence
|
|
while (i - 1 >= 0 && sequencePoints[i].Line == 0xFeeFee) {
|
|
i--;
|
|
retVal.ILStart = (int)sequencePoints[i].Offset;
|
|
}
|
|
// Wow, there are no 'real' sequences
|
|
if (sequencePoints[i].Line == 0xFeeFee) {
|
|
return null;
|
|
}
|
|
|
|
retVal.ModuleFilename = module.FullPath;
|
|
|
|
retVal.SourceFullFilename = sequencePoints[i].Document.URL;
|
|
/*if (retVal.SourceFullFilename != null && Path.GetFileName(retVal.SourceFullFilename).StartsWith("_temp$"))
|
|
{
|
|
retVal.SourceFullFilename = Path.Combine(Path.GetDirectoryName(retVal.SourceFullFilename),Path.GetFileName(retVal.SourceFullFilename).Substring(6));
|
|
System.Diagnostics.Debug.WriteLine(retVal.SourceFullFilename);
|
|
}*/
|
|
retVal.StartLine = (int)sequencePoints[i].Line;
|
|
retVal.StartColumn = (int)sequencePoints[i].Column;
|
|
retVal.EndLine = (int)sequencePoints[i].EndLine;
|
|
retVal.EndColumn = (int)sequencePoints[i].EndColumn;
|
|
|
|
|
|
List<int> stepRanges = new List<int>();
|
|
for (int j = 0; j < sequencePointCount; j++) {
|
|
// Step over compiler generated sequences and current statement
|
|
// 0xFeeFee means "code generated by compiler"
|
|
if (sequencePoints[j].Line == 0xFeeFee || j == i || ( j > i && (sequencePoints[j].Line == sequencePoints[i].Line && sequencePoints[i].Document.URL == sequencePoints[j].Document.URL))) { // line + document must be same
|
|
// Add start offset or remove last end (to connect two ranges into one)
|
|
if (stepRanges.Count > 0 && stepRanges[stepRanges.Count - 1] == sequencePoints[j].Offset) {
|
|
stepRanges.RemoveAt(stepRanges.Count - 1);
|
|
}
|
|
else {
|
|
stepRanges.Add((int)sequencePoints[j].Offset);
|
|
}
|
|
// Add end offset | handle last sequence point
|
|
if (j + 1 < sequencePointCount) {
|
|
stepRanges.Add((int)sequencePoints[j+1].Offset);
|
|
} else {
|
|
stepRanges.Add(codeSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
retVal.StepRanges = stepRanges.ToArray();
|
|
|
|
return retVal;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine whether the instrustion pointer can be set to given location
|
|
/// </summary>
|
|
/// <returns> Best possible location. Null is not possible. </returns>
|
|
public SourcecodeSegment CanSetIP(string filename, int line, int column)
|
|
{
|
|
return SetIP(true, filename, line, column);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the instrustion pointer to given location
|
|
/// </summary>
|
|
/// <returns> Best possible location. Null is not possible. </returns>
|
|
public SourcecodeSegment SetIP(string filename, int line, int column)
|
|
{
|
|
return SetIP(false, filename, line, column);
|
|
}
|
|
|
|
SourcecodeSegment SetIP(bool simulate, string filename, int line, int column)
|
|
{
|
|
process.AssertPaused();
|
|
|
|
SourcecodeSegment suggestion = new SourcecodeSegment(filename, line, column, column);
|
|
ICorDebugFunction corFunction;
|
|
int ilOffset;
|
|
if (!suggestion.GetFunctionAndOffset(this.Module, false, out corFunction, out ilOffset)) {
|
|
return null;
|
|
} else {
|
|
if (corFunction.Token != methodProps.Token) {
|
|
return null;
|
|
} else {
|
|
try {
|
|
if (simulate) {
|
|
CorILFrame.CanSetIP((uint)ilOffset);
|
|
} else {
|
|
// invalidates all frames and chains for the current thread
|
|
CorILFrame.SetIP((uint)ilOffset);
|
|
process.NotifyPaused(new PauseSession(PausedReason.SetIP));
|
|
process.Pause(false);
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return GetSegmentForOffet((uint)ilOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary> Gets value of given name which is accessible from this function </summary>
|
|
/// <returns> Null if not found </returns>
|
|
public NamedValue GetValue(string name)
|
|
{
|
|
if (name == "this") {
|
|
return ThisValue;
|
|
}
|
|
if (Arguments.Contains(name)) {
|
|
return Arguments[name];
|
|
}
|
|
if (LocalVariables.Contains(name)) {
|
|
return LocalVariables[name];
|
|
}
|
|
if (ContaingClassVariables.Contains(name)) {
|
|
return ContaingClassVariables[name];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all variables in the lexical scope of the function.
|
|
/// That is, arguments, local variables and varables of the containing class.
|
|
/// </summary>
|
|
[Debugger.Tests.Ignore] // Accessible though others
|
|
public NamedValueCollection Variables {
|
|
get {
|
|
return new NamedValueCollection(GetVariables());
|
|
}
|
|
}
|
|
|
|
IEnumerable<NamedValue> GetFields()
|
|
{
|
|
foreach(NamedValue nv in Fields)
|
|
{
|
|
yield return nv;
|
|
}
|
|
}
|
|
|
|
IEnumerable<NamedValue> GetVariables()
|
|
{
|
|
if (!IsStatic) {
|
|
yield return ThisValue;
|
|
}
|
|
foreach(NamedValue namedValue in Arguments) {
|
|
yield return namedValue;
|
|
}
|
|
foreach(NamedValue namedValue in LocalVariables) {
|
|
yield return namedValue;
|
|
}
|
|
foreach(NamedValue namedValue in ContaingClassVariables) {
|
|
yield return namedValue;
|
|
}
|
|
}
|
|
|
|
NamedValue thisValueCache;
|
|
|
|
/// <summary>
|
|
/// Gets the instance of the class asociated with the current frame.
|
|
/// That is, 'this' in C#.
|
|
/// </summary>
|
|
public NamedValue ThisValue {
|
|
get {
|
|
if (IsStatic) throw new DebuggerException("Static method does not have 'this'.");
|
|
if (thisValueCache == null) {
|
|
thisValueCache = new NamedValue(
|
|
"this",
|
|
process,
|
|
new IExpirable[] {this},
|
|
new IMutable[] {},
|
|
delegate { return ThisCorValue; }
|
|
);
|
|
}
|
|
return thisValueCache;
|
|
}
|
|
}
|
|
|
|
ICorDebugValue ThisCorValue {
|
|
get {
|
|
if (this.HasExpired) throw new CannotGetValueException("FUNCTION_HAS_EXPIRED");
|
|
try {
|
|
return CorILFrame.GetArgument(0);
|
|
} catch (COMException e) {
|
|
// System.Runtime.InteropServices.COMException (0x80131304): An IL variable is not available at the current native IP. (See Forum-8640)
|
|
if ((uint)e.ErrorCode == 0x80131304) throw new CannotGetValueException("Not available in the current state");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all accessible members of the class that defines this function.
|
|
/// </summary>
|
|
public NamedValueCollection ContaingClassVariables {
|
|
get {
|
|
// TODO: Should work for static
|
|
if (!IsStatic) {
|
|
return ThisValue.GetMembers();
|
|
} else {
|
|
return Fields;
|
|
}
|
|
}
|
|
}
|
|
|
|
private DebugType declaringType;
|
|
|
|
public DebugType DeclaringType
|
|
{
|
|
get
|
|
{
|
|
if (declaringType != null) return declaringType;
|
|
declaringType = DebugType.Create(process,ContaingClass);
|
|
return declaringType;
|
|
}
|
|
}
|
|
|
|
/// <summary> Gets the name of given parameter </summary>
|
|
/// <param name="index"> Zero-based index </param>
|
|
public string GetParameterName(int index)
|
|
{
|
|
// index = 0 is return parameter
|
|
try {
|
|
return module.MetaData.GetParamForMethodIndex(methodProps.Token, (uint)index + 1).Name;
|
|
} catch {
|
|
return String.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary> Total number of arguments (excluding implicit 'this' argument) </summary>
|
|
public int ArgumentCount {
|
|
get {
|
|
ICorDebugValueEnum argumentEnum = CorILFrame.EnumerateArguments();
|
|
uint argCount = argumentEnum.Count;
|
|
if (!IsStatic) {
|
|
argCount--; // Remove 'this' from count
|
|
}
|
|
return (int)argCount;
|
|
}
|
|
}
|
|
|
|
/// <summary> Gets argument with a given index </summary>
|
|
/// <param name="index"> Zero-based index </param>
|
|
public MethodArgument GetArgument(int index)
|
|
{
|
|
return new MethodArgument(
|
|
GetParameterName(index),
|
|
index,
|
|
process,
|
|
new IExpirable[] {this},
|
|
new IMutable[] {process.DebugeeState},
|
|
delegate { return GetArgumentCorValue(index); }
|
|
);
|
|
}
|
|
|
|
ICorDebugValue GetArgumentCorValue(int index)
|
|
{
|
|
if (this.HasExpired) throw new CannotGetValueException("FUNCTION_HAS_EXPIRED");
|
|
|
|
try {
|
|
// Non-static functions include 'this' as first argument
|
|
return CorILFrame.GetArgument((uint)(IsStatic? index : (index + 1)));
|
|
} catch (COMException e) {
|
|
if ((uint)e.ErrorCode == 0x80131304) throw new CannotGetValueException("Unavailable in optimized code");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
NamedValueCollection argumentsCache;
|
|
|
|
/// <summary> Gets all arguments of the function. </summary>
|
|
public NamedValueCollection Arguments {
|
|
get {
|
|
if (argumentsCache == null) {
|
|
DateTime startTime = Util.HighPrecisionTimer.Now;
|
|
|
|
argumentsCache = new NamedValueCollection(ArgumentsEnum);
|
|
|
|
TimeSpan totalTime = Util.HighPrecisionTimer.Now - startTime;
|
|
process.TraceMessage("Loaded Arguments for " + this.ToString() + " (" + totalTime.TotalMilliseconds + " ms)");
|
|
}
|
|
return argumentsCache;
|
|
}
|
|
}
|
|
|
|
IEnumerable<NamedValue> ArgumentsEnum {
|
|
get {
|
|
for (int i = 0; i < ArgumentCount; i++) {
|
|
yield return GetArgument(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
NamedValueCollection localVariablesCache;
|
|
NamedValueCollection fieldsCache;
|
|
|
|
public NamedValueCollection Fields {
|
|
get
|
|
{
|
|
if (fieldsCache == null)
|
|
{
|
|
fieldsCache = new NamedValueCollection(FieldsEnum);
|
|
}
|
|
return fieldsCache;
|
|
}
|
|
}
|
|
/// <summary> Gets all local variables of the function. </summary>
|
|
public NamedValueCollection LocalVariables {
|
|
get {
|
|
if (localVariablesCache == null) {
|
|
DateTime startTime = Util.HighPrecisionTimer.Now;
|
|
|
|
localVariablesCache = new NamedValueCollection(LocalVariablesEnum);
|
|
|
|
TimeSpan totalTime = Util.HighPrecisionTimer.Now - startTime;
|
|
process.TraceMessage("Loaded LocalVariables for " + this.ToString() + " (" + totalTime.TotalMilliseconds + " ms)");
|
|
}
|
|
return localVariablesCache;
|
|
}
|
|
}
|
|
|
|
IEnumerable<NamedValue> LocalVariablesEnum {
|
|
get {
|
|
if (symMethod != null) { // TODO: Is this needed?
|
|
ISymUnmanagedScope symRootScope = symMethod.RootScope;
|
|
foreach(LocalVariable var in GetLocalVariablesInScope(symRootScope)) {
|
|
if (!var.Name.StartsWith("CS$")) { // TODO: Generalize
|
|
yield return var;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
IEnumerable<NamedValue> FieldsEnum {
|
|
get
|
|
{
|
|
if (symMethod != null)
|
|
{
|
|
ISymUnmanagedScope symRootScope = symMethod.RootScope;
|
|
IList<FieldInfo> flds = DeclaringType.GetFields(BindingFlags.All);
|
|
foreach (FieldInfo fi in flds)
|
|
if (!fi.Name.Contains("$") && fi.IsStatic)
|
|
yield return fi.GetValue(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
IEnumerable<LocalVariable> GetLocalVariablesInScope(ISymUnmanagedScope symScope)
|
|
{
|
|
foreach (ISymUnmanagedVariable symVar in symScope.Locals) {
|
|
yield return GetLocalVariable(symVar);
|
|
}
|
|
foreach(ISymUnmanagedScope childScope in symScope.Children) {
|
|
foreach(LocalVariable var in GetLocalVariablesInScope(childScope)) {
|
|
yield return var;
|
|
}
|
|
}
|
|
}
|
|
|
|
LocalVariable GetLocalVariable(ISymUnmanagedVariable symVar)
|
|
{
|
|
return new LocalVariable(
|
|
symVar.Name,
|
|
process,
|
|
new IExpirable[] {this},
|
|
new IMutable[] {process.DebugeeState},
|
|
delegate { return GetCorValueOfLocalVariable(symVar); }
|
|
);
|
|
}
|
|
|
|
ICorDebugValue GetCorValueOfLocalVariable(ISymUnmanagedVariable symVar)
|
|
{
|
|
if (this.HasExpired) throw new CannotGetValueException("FUNCTION_HAS_EXPIRED");
|
|
|
|
try {
|
|
return CorILFrame.GetLocalVariable((uint)symVar.AddressField1);
|
|
} catch (COMException e) {
|
|
if ((uint)e.ErrorCode == 0x80131304) throw new CannotGetValueException("Unavailable in optimized code");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
}
|