在调试器中正确地使.NET语言步进

lep*_*pie 134 .net debugging clr visual-studio-2010 visual-studio

首先,我为这个问题的长度道歉.

我是IronScheme的作者.最近我一直在努力发出不错的调试信息,以便我可以使用'native'.NET调试器.

虽然这部分成功,但我遇到了一些问题.

第一个问题与踩踏有关.

由于Scheme是一种表达式语言,一切都倾向于用括号括起来,不像主要的.NET语言似乎是基于语句(或行).

原始代码(Scheme)看起来像:

(define (baz x)
  (cond
    [(null? x) 
      x]
    [(pair? x) 
      (car x)]
    [else
      (assertion-violation #f "nooo" x)]))
Run Code Online (Sandbox Code Playgroud)

我故意在换行符上列出每个表达式.

发出的代码转换为C#(通过ILSpy),如下所示:

public static object ::baz(object x)
{
  if (x == null)
  {
    return x;
  }
  if (x is Cons)
  {
    return Builtins.Car(x);
  }
  return #.ironscheme.exceptions::assertion-violation+(
     RuntimeHelpers.False, "nooo", Builtins.List(x));
}
Run Code Online (Sandbox Code Playgroud)

如你所见,非常简单.

注意:如果代码在C#中转换为条件表达式(?:),那么整个过程只需要一个调试步骤,请记住这一点.

这是带有源和行号的IL输出:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       56 (0x38)
    .maxstack  6
    .line 15,15 : 1,2 ''
//000014: 
//000015: (define (baz x)
    IL_0000:  nop
    .line 17,17 : 6,15 ''
//000016:   (cond
//000017:     [(null? x) 
    IL_0001:  ldarg.0
    IL_0002:  brtrue     IL_0009

    .line 18,18 : 7,8 ''
//000018:       x]
    IL_0007:  ldarg.0
    IL_0008:  ret

    .line 19,19 : 6,15 ''
//000019:     [(pair? x) 
    .line 19,19 : 6,15 ''
    IL_0009:  ldarg.0
    IL_000a:  isinst [IronScheme]IronScheme.Runtime.Cons
    IL_000f:  ldnull
    IL_0010:  cgt.un
    IL_0012:  brfalse    IL_0020

    IL_0017:  ldarg.0
    .line 20,20 : 7,14 ''
//000020:       (car x)]
    IL_0018:  tail.
    IL_001a:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_001f:  ret

    IL_0020:  ldsfld object 
         [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_0025:  ldstr      "nooo"
    IL_002a:  ldarg.0
    IL_002b:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
//000021:     [else
//000022:       (assertion-violation #f "nooo" x)]))
    IL_0030:  tail.
    IL_0032:  call object [ironscheme.boot]#::
       'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_0037:  ret
  } // end of method 'eval-core(033)'::'::baz'
Run Code Online (Sandbox Code Playgroud)

注意:为了防止调试器只是突出显示整个方法,我将方法入口点设为1列宽.

如您所见,每个表达式都正确映射到一条线.

现在步进的问题(在VS2010上测试,但VS2008上的相同/类似问题):

这些都IgnoreSymbolStoreSequencePoints没有应用.

  1. 使用null arg调用baz,它可以正常工作.(null?x)后跟x.
  2. 用Cons arg调用baz,它可以正常工作.(null?x)然后(pair?x)然后(car x).
  3. 用其他arg调用baz,它失败了.(null?x)然后(pair?x)然后(car x)然后(断言违规......).

申请时IgnoreSymbolStoreSequencePoints(按照建议):

  1. 使用null arg调用baz,它可以正常工作.(null?x)后跟x.
  2. 用Cons arg调用baz,它失败了.(null?x)然后(对?x).
  3. 用其他arg调用baz,它失败了.(null?x)然后(pair?x)然后(car x)然后(断言违规......).

我还发现在这种模式下,某些线条(此处未显示)未正确突出显示,它们偏离1.

以下是一些可能是原因的想法:

  • Tailcalls混淆了调试器
  • 重叠位置(此处未显示)会混淆调试器(在设置断点时它非常好)
  • ????

第二个也是严重的问题是调试器在某些情况下无法中断/命中断点.

我可以让调试器正确(和一致地)中断的唯一地方是方法入口点.

如果IgnoreSymbolStoreSequencePoints不应用,情况会好一些.

结论

可能是VS调试器只是普通的错误:(

参考文献:

  1. 使CLR/.NET语言可调试

更新1:

Mdbg不适用于64位程序集.这样就出来了.我没有更多的32位机器来测试它.更新:我相信这不是什么大问题,有人有修复吗?编辑:是的,傻我,只需在x64命令提示符下启动mdbg :)

更新2:

我创建了一个C#应用程序,并尝试剖析行信息.

我的发现:

  • 在任何brXXX指令之后你需要有一个序列点(如果无效,又称'#line hidden',则发出一个nop).
  • 在任何brXXX指令之前,发出'#line hidden'和a nop.

然而,应用此功能并不能解决问题(单独?).

但添加以下内容,给出了预期的结果:)

  • 之后ret,发出'#line hidden'和a nop.

这是使用IgnoreSymbolStoreSequencePoints未应用的模式.应用时,仍会跳过某些步骤:(

以下是应用以上时的IL输出:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  6
    .line 15,15 : 1,2 ''
    IL_0000:  nop
    .line 17,17 : 6,15 ''
    IL_0001:  ldarg.0
    .line 16707566,16707566 : 0,0 ''
    IL_0002:  nop
    IL_0003:  brtrue     IL_000c

    .line 16707566,16707566 : 0,0 ''
    IL_0008:  nop
    .line 18,18 : 7,8 ''
    IL_0009:  ldarg.0
    IL_000a:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_000b:  nop
    .line 19,19 : 6,15 ''
    .line 19,19 : 6,15 ''
    IL_000c:  ldarg.0
    IL_000d:  isinst     [IronScheme]IronScheme.Runtime.Cons
    IL_0012:  ldnull
    IL_0013:  cgt.un
    .line 16707566,16707566 : 0,0 ''
    IL_0015:  nop
    IL_0016:  brfalse    IL_0026

    .line 16707566,16707566 : 0,0 ''
    IL_001b:  nop
    IL_001c:  ldarg.0
    .line 20,20 : 7,14 ''
    IL_001d:  tail.
    IL_001f:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_0024:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_0025:  nop
    IL_0026:  ldsfld object 
      [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_002b:  ldstr      "nooo"
    IL_0030:  ldarg.0
    IL_0031:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
    IL_0036:  tail.
    IL_0038:  call object [ironscheme.boot]#::
      'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_003d:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_003e:  nop
  } // end of method 'eval-core(033)'::'::baz'
Run Code Online (Sandbox Code Playgroud)

更新3:

以上'半修复'的问题.Peverify报告由于nopafter 而导致的所有方法的错误ret.我真的不明白这个问题.怎么可以nop在a之后进行中断验证ret.它就像死代码(除了它甚至不是代码)......哦,实验继续.

更新4:

现在回到家里,删除了在VS2008上运行的'无法验证的'代码,事情变得更糟.也许为了正确调试而运行无法验证的代码可能就是答案.在'release'模式下,所有输出仍然可以验证.

更新5:

我现在已经确定我的上述想法是目前唯一可行的选择.虽然生成的代码无法验证,但我还没有找到任何代码VerificationException.我不知道这种情况对最终用户有什么影响.

作为奖励,我的第二个问题也得到了解决.:)

这是我最终得到的一个小小的截屏视频.它可以击中断点,进行适当的步进(进/出/结束)等等.总而言之,它可以达到预期的效果.

但是,我仍然不接受这样做.对我来说,我觉得过于苛刻.确认真正的问题会很好.

更新6:

刚刚对VS2010上的代码进行了测试,似乎存在一些问题:

  1. 第一次调用现在没有正确执行.(断言违规......)被击中.其他情况下工作正常.一些旧代码发出不必要的位置.删除代码,按预期工作.:)
  2. 更严重的是,断点在第二次调用程序时失败(使用内存中编译,将汇编转储到文件似乎使断点再次开心).

这两种情况在VS2008下都能正常工作.主要的区别是在VS2010下,整个应用程序是针对.NET 4编译的,在VS2008下,编译为.NET 2.两者都运行64位.

更新7:

就像提到的那样,我让mdbg在64位下运行.不幸的是,它还有断点问题,如果我重新运行程序它不会破坏(这意味着它被重新编译,所以不使用相同的程序集,但仍使用相同的源).

更新8:

我在MS Connect网站上提交了有关断点问题的错误.

更新:已修复

更新9:

经过一番漫长的思考,使调试器开心的唯一方法似乎是做SSA,所以每一步都可以是隔离的和顺序的.我还是要证明这个概念.但这看似合乎逻辑.显然,从SSA清除临时值会破坏调试,但这很容易切换,留下它们没有太多开销.

Luk*_*Kim 26

我是Visual Studio Debugger团队的工程师.

如果我错了,请纠正我,但听起来唯一的问题是当从PDB切换到.NET 4动态编译符号格式时,一些断点正在被遗漏.

我们可能需要一个repro来准确诊断问题,但是这里有一些可能有用的注释.

  1. VS(2008+)可以作为非管理员运行
  2. 是否第二次加载任何符号?您可以通过中断(通过异常或调用System.Diagnostic.Debugger.Break())进行测试
  3. 假设符号加载,是否有可以发送给我们的repro?
  4. 可能的区别是动态编译代码的符号格式在.NET 2(PDB流)和.NET 4(我认为他们称之为IL DB?)之间是100%不同的.
  5. 'nop的声音是正确的.请参阅下面的生成隐式序列点的规则.
  6. 实际上你不需要在不同的行上发射东西.默认情况下,VS将执行'symbol-statements',作为编译器编写者,您可以定义'symbol-statement'的含义.因此,如果您希望每个表达式在符号文件中都是一个单独的东西,那就可以了.

JIT基于以下规则创建隐式序列点:1.IL nop指令2. IL堆栈空点3.紧接着调用指令的IL指令

如果事实证明我们确实需要一个repro来解决您的问题,您可以提交连接错误并通过该媒介安全地上传文件.

更新:

我们鼓励遇到此问题的其他用户从http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27543尝试Dev11的开发者预览版并对任何反馈发表评论.(必须以4.5为目标)

更新2:

Leppie已经在http://www.microsoft.com/visualstudio/11/en-us/downloads上的Dev11 Beta版本上验证了为他工作的修复程序,如连接错误https://connect.microsoft中所述. com/VisualStudio/feedback/details/684089 /.

谢谢,

卢克