在调试 C# 程序时使用 Debug + Windows + Disassembly 所看到的内容是这些术语的良好指南。这是我在启用 JIT 优化的发布配置中编译用 C# 编写的“hello world”程序时的注释版本:
static void Main(string[] args) {
Console.WriteLine("Hello world");
00000000 55 push ebp ; save stack frame pointer
00000001 8B EC mov ebp,esp ; setup current frame
00000003 E8 30 BE 03 6F call 6F03BE38 ; Console.Out property getter
00000008 8B C8 mov ecx,eax ; setup "this"
0000000a 8B 15 88 20 BD 02 mov edx,dword ptr ds:[02BD2088h] ; arg = "Hello world"
00000010 8B 01 mov eax,dword ptr [ecx] ; TextWriter reference
00000012 FF 90 D8 00 00 00 call dword ptr [eax+000000D8h] ; TextWriter.WriteLine()
00000018 5D pop ebp ; restore stack frame pointer
}
00000019 C3 ret ; done, return
右键单击窗口并勾选“显示代码字节”以获得类似的显示。
左边一列是机器码地址。它的值是由调试器伪造的,代码实际上位于其他地方。但这可能在任何地方,具体取决于 JIT 编译器选择的位置,因此调试器只是在方法开始时从 0 开始对地址进行编号。
第二列是机器码。 CPU 执行的实际 1 和 0。像这里一样,机器代码通常以十六进制显示。说明性的可能是 0x8B 选择了 MOV 指令,额外的字节在那里告诉 CPU 确切需要移动什么。还要注意CALL指令的两种风格,0xE8是直接调用,0xFF是间接调用指令。
第三列是汇编代码。汇编是一种简单的语言,旨在使编写机器代码更容易。它与被编译为 IL 的 C# 相比。用于翻译汇编代码的编译器称为“汇编器”。你的机器上可能有 Microsoft 汇编器,它的可执行文件名为 ml.exe,ml64.exe 为 64 位版本。有两种常用的汇编语言版本正在使用中。你看到的是英特尔和 AMD 使用的那个。在开源世界中,AT&T 表示法的汇编很常见。语言语法很大程度上取决于编写的 CPU 类型,PowerPC 的汇编语言非常不同。
好的,这解决了您问题中的两个术语。 “本机代码”是一个模糊的术语,它通常用于描述非托管语言中的代码。启发性也许是看看 C 编译器生成了什么样的机器代码。这是 C 语言中的“hello world”版本:
int _tmain(int argc, _TCHAR* argv[])
{
00401010 55 push ebp
00401011 8B EC mov ebp,esp
printf("Hello world");
00401013 68 6C 6C 45 00 push offset ___xt_z+128h (456C6Ch)
00401018 E8 13 00 00 00 call printf (401030h)
0040101D 83 C4 04 add esp,4
return 0;
00401020 33 C0 xor eax,eax
}
00401022 5D pop ebp
00401023 C3 ret
我没有注释它,主要是因为它与 C# 程序生成的机器代码非常相似。 printf() 函数调用与 Console.WriteLine() 调用完全不同,但其他一切都差不多。另请注意,调试器现在正在生成真实的机器代码地址,并且它对符号更智能。 在生成机器代码后生成调试信息的副作用,就像非托管编译器经常做的那样。我还应该提到,我关闭了一些机器代码优化选项以使机器代码看起来相似。 C/C++ 编译器有更多时间可用于优化代码,结果通常难以解释。而且非常很难调试。
这里的关键点是,JIT 编译器从托管语言生成的机器代码与本机代码编译器生成的机器代码之间几乎没有区别。这就是 C# 语言可以与原生代码编译器竞争的主要原因。它们之间唯一真正的区别是支持函数调用。其中许多是在 CLR 中实现的。这主要围绕垃圾收集器展开。