为什么 C# 中的每个方法中都有 cmp + je 调试版本中的 JIT 汇编代码

why is cmp + je in every method in C# JIT assembly code in Debug build

提问人:juwens 提问时间:10/19/2023 最后编辑:juwens 更新时间:10/19/2023 访问量:57

问:

当你放一个简单的类时。

public sealed class C {
    public static void M() {
    }
}

https://sharplab.io/

它翻译为(带有我的注释):(来源)

C.M()
    L0000: push ebp      /////////////////////
    L0001: mov ebp, esp  // function frame initialization
    L0003: push edi      /////////////////////
    L0004: cmp dword ptr [0x281dc19c], 0 // if (0 == ???)
    L000b: je short L0012  // then: jump to actual method body
    L000d: call 0x727a8790 // else: call ??? what ???
    L0012: nop           // the actual method body
    L0013: nop           // the actual method body
    L0014: pop edi       /////////////////////
    L0015: pop ebp       // function frame teardown/exit
    L0016: ret           /////////////////////

L0004 到 L000d 的目的是什么?

    L0004: cmp dword ptr [0x281dc19c], 0 // if (0 == ???)
    L000b: je short L0012  // then: jump to actual method body
    L000d: call 0x727a8790 // else: call ??? what ??

什么是所谓的函数?
它是否终止了该过程? 为什么 C# JIT 在每个方法中都放这个?

我认为这可能是继承的东西,但我密封了类并使方法静态以消除该选项。

是某种理智检查吗?就像支票一样:

  • method 重载
  • 代码损坏
  • 堆栈溢出
  • 段错误

IL 没有给出线索:

    .method public hidebysig static 
        void M () cil managed 
    {
        // Method begins at RVA 0x2069
        // Code size 2 (0x2)
        .maxstack 8

        IL_0000: nop
        IL_0001: ret
    }

更新:

@Dai,将其设置为 release 会删除代码。为了确保完全删除不是由空方法体大小写的,我添加了一个简单的语句

    public static void M() {
        System.Console.WriteLine(7);
    }

发布模式下的 JIT 会导致(来源):

C.M()
    L0000: mov ecx, 7
    L0005: call dword ptr [0x10a25768]
    L000b: ret

就像@Dai说的没有提到的 CMP-JE

C# .NET 程序集 编译器优化 Roslyn

评论

3赞 Dai 10/19/2023
将您的 sharplab 更改为 Release(而不是 Debug)并且它会消失 - 所以这是一个强烈的提示,它是为了调试器或工具的利益(例如,NOP 允许函数蹦床和绕道,但我不知道 CMP+JE)
0赞 juwens 10/19/2023
@Dai谢谢,非常好的输入
3赞 Dai 10/19/2023
在 WinDbg 中进行了一些挖掘后,的分支是 .jecall 0x7...CORINFO_HELP_DBG_IS_JUST_MY_CODE

答:

4赞 Dai 10/19/2023 #1

让我们玩一个有趣的游戏,“这在我的代码中做了什么?

第 1 步:本地编译

我用(和其他选项)编译了它。csc /define:DEBUG; /debug+ /debug:portable

我添加了阻塞调用,这样我们就不需要在调试器中设置断点。Console.ReadLine()

using System;

namespace JitHmm
{
    class Program
    {
        static void Main( string[] args )
        {
            C.M();
        }
    }

    public static class C
    {
        public static void M()
        {
            Console.WriteLine( "Foo" );
            _ = Console.ReadLine();
        }
    }
}

第 2 步:通过 WinDbg + SOS 启动程序:


  1. 当程序运行时,它会将“Foo”打印到 stdout,然后在里面等待。Console.ReadLine()

  2. 如果告诉 WinDbg 中断,它将显示 .NET 主线程 () 在里面等待 (,就像尝试从 stdin 读取一样) 。apphostNtReadFileConsole.ReadLine

  3. 不要忘记加载符号,因此 WinDbg 的“反汇编”窗口将显示指令的解析函数名称,而不是原始内存地址。call

  4. 打开 Stack Trace 窗口,然后走到表示函数的框架 - 这将是第一个框架之前的框架。M()System_Console

    • WinDbg 似乎没有使用 CLR 符号在“堆栈”窗口中显示 C# 方法名称,但如果运行,则会在主命令输出窗口中看到转储的堆栈。!DumpStack -EEC.M()
  5. 导航到该函数,“反汇编”窗口应显示与 Sharplab 大致相同的内容:C.M()

    00007ffe`9a615eff 005548           add     byte ptr [rbp+48h], dl
    00007ffe`9a615f02 83ec30           sub     esp, 30h
    00007ffe`9a615f05 488d6c2430       lea     rbp, [rsp+30h]
    00007ffe`9a615f0a 33c0             xor     eax, eax
    00007ffe`9a615f0c 8945fc           mov     dword ptr [rbp-4], eax
    00007ffe`9a615f0f 488945f0         mov     qword ptr [rbp-10h], rax
    00007ffe`9a615f13 833d16cb090000   cmp     dword ptr [7FFE9A6B2A30h], 0
    00007ffe`9a615f1a 7405             je      00007FFE9A615F21
    00007ffe`9a615f1c e8cf26c85f       call    00007FFEFA2985F0
    00007ffe`9a615f21 90               nop     
    00007ffe`9a615f22 33c9             xor     ecx, ecx
    00007ffe`9a615f24 894dfc           mov     dword ptr [rbp-4], ecx
    00007ffe`9a615f27 488b0c258030701a mov     rcx, qword ptr [1A703080h]
    00007ffe`9a615f2f e8acffffff       call    00007FFE9A615EE0
    00007ffe`9a615f34 90               nop     
    00007ffe`9a615f35 e836ffffff       call    00007FFE9A615E70
    00007ffe`9a615f3a 488945f0         mov     qword ptr [rbp-10h], rax
    
  6. 指示 WinDbg 加载符号,短暂等待后,你将看到反汇编窗口中的行更改为call 00007FFEFA2985F0call coreclr!JIT_DbgIsJustMyCode (7ffefa2985f0)

  7. ...那么到底是什么?JIT_DbgIsJustMyCode

  8. 运行命令 - 它将打印一个带注释的函数反汇编,它向我显示:!u 00007ffe`9a615f1300007ffe`9a615f13

    Normal JIT generated code
    JitHmm.C.M()
    ilAddr is 0000000000592064 pImport is 0000000002BFD460
    Begin 00007FFE9A615F00, size 46
    
    C:\git\_bollocks\JitHmm\Program.cs @ 16:
    00007ffe`9a615f00 55              push    rbp
    00007ffe`9a615f01 4883ec30        sub     rsp,30h
    00007ffe`9a615f05 488d6c2430      lea     rbp,[rsp+30h]
    00007ffe`9a615f0a 33c0            xor     eax,eax
    00007ffe`9a615f0c 8945fc          mov     dword ptr [rbp-4],eax
    00007ffe`9a615f0f 488945f0        mov     qword ptr [rbp-10h],rax
    >>> 00007ffe`9a615f13 833d16cb090000  cmp     dword ptr [00007ffe`9a6b2a30],0
    00007ffe`9a615f1a 7405            je      00007ffe`9a615f21
    00007ffe`9a615f1c e8cf26c85f      call    coreclr!GetCLRRuntimeHost+0x82700 (00007ffe`fa2985f0) (JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
    00007ffe`9a615f21 90              nop
    
    C:\git\_bollocks\JitHmm\Program.cs @ 17:
    00007ffe`9a615f22 33c9            xor     ecx,ecx
    00007ffe`9a615f24 894dfc          mov     dword ptr [rbp-4],ecx
    
    C:\git\_bollocks\JitHmm\Program.cs @ 19:
    00007ffe`9a615f27 488b0c258030701a mov     rcx,qword ptr [1A703080h] ("Foo")
    00007ffe`9a615f2f e8acffffff      call    00007ffe`9a615ee0
    00007ffe`9a615f34 90              nop
    
    C:\git\_bollocks\JitHmm\Program.cs @ 20:
    00007ffe`9a615f35 e836ffffff      call    00007ffe`9a615e70
    00007ffe`9a615f3a 488945f0        mov     qword ptr [rbp-10h],rax
    00007ffe`9a615f3e 90              nop
    
    C:\git\_bollocks\JitHmm\Program.cs @ 21:
    00007ffe`9a615f3f 90              nop
    00007ffe`9a615f40 488d6500        lea     rsp,[rbp]
    00007ffe`9a615f44 5d              pop     rbp
    00007ffe`9a615f45 c3              ret
    
  9. 注意部分 - 这是我们可以搜索的东西。NET 的主要 GitHub 存储库(JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)

  10. ...它解析为 CLR 本身内置的这个实际函数:void JIT_DbgIsJustMyCode()

  11. ...这记录在 debug/ee/debugger.h 以及 coreclr/vm/jithelpers.cpp

    // The jit injects probes in debuggable managed methods that look like:
    // if (*pFlag != 0) call JIT_DbgIsJustMyCode.
    // pFlag is unique per-method constant determined by GetJMCFlagAddr.
    // JIT_DbgIsJustMyCode will get the ip & fp and call OnMethodEnter.
    // pIP is an ip within the method, right after the prolog.
    
    // Callback for Just-My-Code probe
    // Probe looks like:
    //  if (*pFlag != 0) call JIT_DbgIsJustMyCode
    // So this is only called if the flag (obtained by GetJMCFlagAddr) is
    //  non-zero.
    
  12. 因此,神秘和指令对应于 的发出指令。cmpjeif(*pFlag != 0)

  13. CLR 不提供实际功能。GetJMCFlagAddr


那么,它有什么作用呢?

它允许调试器在程序的执行点到达用户函数(而不是库函数)时收到通知,这就是 Visual Studio 中“只是我的代码”调试器选项的工作方式。

...虽然我不确定他们为什么这样做,而不是(例如)仅使用 PDB 符号。

评论

0赞 juwens 10/19/2023
谢谢,你是个天才:)这比我敢希望的更详细、更有趣。