2

I'm making a managed .NET debugger using MDBG sample.

Consider some simple async example:

1:          private async void OnClick(EventArgs args){
2:              var obj = new SomeClass();
3:              bool res = await StaticHelper.DoSomeAsyncStuff();
4:              if(res){
5:                  Debug.WriteLine("result is True");
6:              }
7:              else{
8:                  Debug.WriteLine("result is False");
9:              }
10:             someField = obj.Name + "is:" + res.ToString();
11:         }

12:         public static async Task<bool> DoSomeAsyncStuff(){
13:             await Task.Delay(5000);
14:             return true;
15:         }

Debugging this code with my debugger I encounter 2 major issues:

  1. Local variables names are changed(CS$4$0000, CS$0$0001, ect) or are missing (cannot find obj in debugger's local variables)
  2. Stepping behaves unpredictably : a) Stepping Over line 3 and so on should normally go to line 4 after waiting for evaluation to complete. But instead debugger jumps to line 13 and continues stepping from there. StepOver behaviour on video

    b) Stepping In on line 3 and so on should just step on each line: line 3 -> line 12 -> line 13(hang for a while) -> line 14 -> line 15 -> line 4. But instead after stepping in on line 13, where I would expect debugger to wait for evaluation result, stepping continues to line 3 for some reason. After that debugger waits for result and execution continues as expected. StepIn behaviour on video

    c) If there is some other work scheduled while awaiting the response, debugger switches to that code. For example, if there is some timer that elapsed during await of response, evaluation after line 13 continues on that timer code. Instead, like visual studio, I would expect debugger to stick with it's current scope and not leave it until it's executed completely. Parallel behaviour on video

Partially I understand the source of these problems: compiler creates a state machine which is represented by a nested struct, where logic is encapsulated inside MoveNext method. That at least explains me why stepping is not working as I would expect for case a) and b). When I'm stepping in some code without symbols(and I don't have symbols for code generated by compiler), I'm making one or more steps to get to some code of mine. It's solution that @Brian Reichle suggested in this related question

Regarding the local variable names change I thought it's happening because of Stack Spilling("What's happening" chapter). But analysing my assembly with ILDASM I haven't found anything saved to the t__stack field of the generated structure. So I'm out of guesses why the variable names are not persisted for async methods.

Nevertheless VisualStudio has all these problems avoided somehow.

So how managed .net debugger should handle stepping and local variable resolution in async/await scenario?

There is a lot of implementation code behind this, but I'm not sure which part would be reasonable to show...

Community
  • 1
  • 1
3615
  • 3,787
  • 3
  • 20
  • 35
  • A lot of work was done in the managed debugger to hide the method rewriting side-effects. You are not going to find that back in MDbg, its features were frozen at .NET 2.0 with just a minor update to make it compatible with .NET 4.0. Mike Stall moved on and is no longer a member of the .NET group. – Hans Passant Aug 22 '16 at 11:31
  • @HansPassant I've noticed that Mike Stall has moved on by looking at his most recent posts. Anyway I'm not looking for a solution inside MDbg for these particular problems, but MDbg served great as kickstart. Now I'm trying somehow to extend the features of MDbg... – 3615 Aug 22 '16 at 11:37
  • The variables are probably missing because they were moved to fields on the state machine. I believe the field-local mappings can be extracted from a blob available through the [Symbol Store api](https://msdn.microsoft.com/en-us/library/ms233503(v=vs.110).aspx), but IIRC, the format is undocumented and compiler dependent (though exposed through the Roslyn library). – Brian Reichle Aug 22 '16 at 14:13
  • 1
    You might want to look into [ISymUnmanagedAsyncMethod](https://msdn.microsoft.com/en-us/library/hh968957(v=vs.110).aspx). IIRC, it should be implemented on the ISymUnmanagedMethod object for the state machines 'MoveNext' method (the method containing the "body" of the async method). This can provide the IL offsets where the state machine will resume, you can probably use it to place breakpoints ... It's after midnight, so now I have to go recapture my mind before it starts something diabolical. – Brian Reichle Aug 22 '16 at 14:14
  • @BrianReichle Thank you for the directions! I'll try to dig a bit and see where I'll be ending up. – 3615 Aug 22 '16 at 14:28
  • The b case is actually quite logical - the compiler creates a Task on line 13 and returns it to the calling line - 3, which then awaits it. The actual awaiting happens there, so I wouldn't change it in the debugger. – IS4 Aug 22 '16 at 18:51
  • @IllidanS4 Yes, it definitely has some explanation behind it... But I've just rechecked VS behavior: VS stops on the line 13 and not getting back to line 3 until the function is completed. I would stick with VS behavior, that seems more intuitive(you can debug async code like it is sync). – 3615 Aug 22 '16 at 21:09
  • @BrianReichle Following your advice I've been able to get some information about where Async Method started and will be finished. It's not yet working, but at least I'm not totally stuck. Meanwhile for missing variables it's more complecated. Indeed I've found some(some probably are optimized and not present in IL?) variables on that generated struct. But I haven't found any way to get mapping between symbol variable and IL variable. Looking at Roslyn source didn't help either, I'm blind as usually... Could you please provide some more information about extracting variables? – 3615 Aug 29 '16 at 12:54
  • 1
    I never really looked too deeply into it because the field names were sufficient for my needs and I didn't like the idea of being too dependent on compiler implementation details, but I suspect it's persisted as a [SymAttrybute](https://msdn.microsoft.com/en-us/library/ms232550(v=vs.110).aspx). I thought I saw some code relating to it in the Roslyn code base the last time I looked into it (over a year ago), but the closest I can find now is [GetCustomDebugInfoRecords](http://source.roslyn.io/#Roslyn.Test.PdbUtilities/CustomDebugInfoReader.cs,421a1bce4bd7da8b,references). – Brian Reichle Aug 30 '16 at 12:38
  • I would never guess that SymAttribute is responsible for such mapping. In Roslyn source I was looking at this PdbToXml part, but couldn't figure it out either. I'll give it a try. Thank you once more! – 3615 Aug 30 '16 at 14:33
  • @BrianReichle I've spent some time looking at GetCustomDebugInfoRecords. I've altered some of existing unit tests to create async method and placed breakpoints in that method, but I haven't found anything related to variables mapping. But later I have found [GetDisplayClassVariables method](http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.ExpressionCompiler/CompilationContext.cs,86268d53020048c2)(line 1404) that seems to cut variable name from compiler generated name. Well, that's a bad news... Now I see what compiler dependency you meant... – 3615 Sep 01 '16 at 14:51
  • @3615 Did you manage to achieve the stepping behavior for async methods? I'm trying to implement it myself. – anakic Nov 11 '21 at 12:20

0 Answers0