提问人:juwens 提问时间:10/19/2023 最后编辑:juwens 更新时间:10/19/2023 访问量:57
为什么 C# 中的每个方法中都有 cmp + je 调试版本中的 JIT 汇编代码
why is cmp + je in every method in C# JIT assembly code in Debug build
问:
当你放一个简单的类时。
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
答:
让我们玩一个有趣的游戏,“这在我的代码中做了什么?
第 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 启动程序:
- SOS 是“罢工之子”,是 WinDbg 的 .NET/CLR 调试扩展。
- 文档可在通常的地方找到。
- 另请参阅。
当程序运行时,它会将“Foo”打印到 stdout,然后在里面等待。
Console.ReadLine()
如果告诉 WinDbg 中断,它将显示 .NET 主线程 () 在里面等待 (,就像尝试从 stdin 读取一样) 。
apphost
NtReadFile
Console.ReadLine
不要忘记加载符号,因此 WinDbg 的“反汇编”窗口将显示指令的解析函数名称,而不是原始内存地址。
call
打开 Stack Trace 窗口,然后走到表示函数的框架 - 这将是第一个框架之前的框架。
M()
System_Console
- WinDbg 似乎没有使用 CLR 符号在“堆栈”窗口中显示 C# 方法名称,但如果运行,则会在主命令输出窗口中看到转储的堆栈。
!DumpStack -EE
C.M()
- WinDbg 似乎没有使用 CLR 符号在“堆栈”窗口中显示 C# 方法名称,但如果运行,则会在主命令输出窗口中看到转储的堆栈。
导航到该函数,“反汇编”窗口应显示与 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
指示 WinDbg 加载符号,短暂等待后,你将看到反汇编窗口中的行更改为
call 00007FFEFA2985F0
call coreclr!JIT_DbgIsJustMyCode (7ffefa2985f0)
...那么到底是什么?
JIT_DbgIsJustMyCode
运行命令 - 它将打印一个带注释的函数反汇编,它向我显示:
!u 00007ffe`9a615f13
00007ffe`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
注意部分 - 这是我们可以搜索的东西。NET 的主要 GitHub 存储库
(JitHelp: CORINFO_HELP_DBG_IS_JUST_MY_CODE)
...它解析为 CLR 本身内置的这个实际函数:
void JIT_DbgIsJustMyCode()
...这记录在
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.
因此,神秘和指令对应于 的发出指令。
cmp
je
if(*pFlag != 0)
CLR 不提供实际功能。
GetJMCFlagAddr
那么,它有什么作用呢?
它允许调试器在程序的执行点到达用户函数(而不是库函数)时收到通知,这就是 Visual Studio 中“只是我的代码”调试器选项的工作方式。
...虽然我不确定他们为什么这样做,而不是(例如)仅使用 PDB 符号。
评论
je
call 0x7...
CORINFO_HELP_DBG_IS_JUST_MY_CODE