为什么以下C#方法CallViaStruct
的X86包含cmp
指令?
struct Struct {
public void NoOp() { }
}
struct StructDisptach {
Struct m_struct;
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
这是一个更完整的程序,可以使用各种(发布)解压缩作为注释进行编译。我预计CallViaStruct
和ClassDispatch
类型中StructDispatch
的X86相同,但StructDispatch
(上面提取的)中的版本包含cmp
指令另一个没有。
cmp
指令似乎是用来确保变量不为空的习惯用法;取消引用值为0的寄存器会触发av
转换为NullReferenceException
。但是在StructDisptach.CallViaStruct
中,我无法想象ecx
因为它指向一个结构而为空的方法。
更新:我希望接受的答案将包含导致NRE被StructDisptach.CallViaStruct
抛出的代码,方法是让cmp
指令取消引用归零{{1}注册。请注意,使用ecx
方法可以很容易地设置CallViaClass
而无法使用m_class = null
,因为没有ClassDisptach.CallViaStruct
指令。
cmp
更新:事实证明,可以在非虚拟函数上使用using System.Runtime.CompilerServices;
namespace NativeImageTest {
struct Struct {
public void NoOp() { }
}
class Class {
public void NoOp() { }
}
class ClassDisptach {
Class m_class;
Struct m_struct;
internal ClassDisptach(Class cls) {
m_class = cls;
m_struct = new Struct();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaClass() {
m_class.NoOp();
//push ebp
//mov ebp,esp
//mov eax,dword ptr [ecx+4]
//cmp byte ptr [eax],al
//pop ebp
//ret
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//pop ebp
//ret
}
}
struct StructDisptach {
Class m_class;
Struct m_struct;
internal StructDisptach(Class cls) {
m_class = cls;
m_struct = new Struct();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaClass() {
m_class.NoOp();
//push ebp
//mov ebp,esp
//mov eax,dword ptr [ecx]
//cmp byte ptr [eax],al
//pop ebp
//ret
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
class Program {
static void Main(string[] args) {
var classDispatch = new ClassDisptach(new Class());
classDispatch.CallViaClass();
classDispatch.CallViaStruct();
var structDispatch = new StructDisptach(new Class());
structDispatch.CallViaClass();
structDispatch.CallViaStruct();
}
}
}
,该函数具有null检查this指针的副作用。虽然这是callvirt
调用点的情况(这就是我们在那里看到空检查的原因)CallViaClass
使用StructDispatch.CallViaStruct
指令。
call
更新:有人建议.method public hidebysig instance void CallViaClass() cil managed noinlining
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld class NativeImageTest.Class NativeImageTest.StructDisptach::m_class
IL_0006: callvirt instance void NativeImageTest.Class::NoOp()
IL_000b: ret
} // end of method StructDisptach::CallViaClass
.method public hidebysig instance void CallViaStruct() cil managed noinlining
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldflda valuetype NativeImageTest.Struct NativeImageTest.StructDisptach::m_struct
IL_0006: call instance void NativeImageTest.Struct::NoOp()
IL_000b: ret
} // end of method StructDisptach::CallViaStruct
可以捕获cmp
此指针未在呼叫站点被捕获的情况。如果是这种情况,那么我希望null
在方法的顶部出现一次。但是,每次调用cmp
时都会显示一次:
NoOp
答案 0 :(得分:3)
简短回答:JITter无法证明结构未被指针引用,并且必须至少在每次调用NoOp()时至少取消引用一次以获得正确的行为。
答案很长:结构很奇怪。
JITter很保守。只要有可能,它只能以非常某些产生正确行为的方式优化代码。 “大部分都是正确的”还不够好。
所以现在这里是一个示例场景,如果JITter优化了取消引用,它将会中断。请考虑以下事实:
首先:请记住,结构可以(并且可以!)存在于C#之外 - 例如,指向StructDispatch的指针可能来自非托管代码。正如卢卡斯所指出的,你可以用指针作弊;但是JITter无法确定您是否在代码中的其他位置使用了指向StructDispatch的指针。
第二:请记住,在非托管代码中,这是结构首先存在的最大原因,所有的赌注都是关闭的。仅仅因为你只是从内存中读取一个值并不意味着它将是相同的值,甚至是一个值,下次你读取相同的确切地址。线程化和多处理,可以在下一个时钟滴答声中改变那些值,更不用说像DMA这样的非CPU演员了。一个并行线程可以VirtualFree()包含该结构的页面,而JITter必须防范它。你要求从内存读取,所以你从内存中读取。我的猜测是,如果你踢了优化器,它会删除其中一条cmp指令,但我非常怀疑它会删除它们。
第三:例外也是真实的代码。 NullReferenceException不一定会停止程序;它可以被抓住和处理。这意味着从JITter的角度来看,NRE更像是一个if语句而不是goto:它是一种必须在每个内存解除引用时处理和考虑的条件分支。
所以现在把这些碎片放在一起。
JITter不知道 - 也无法知道 - 您没有使用不安全的C#或其他地方的外部源来与StructDispatch的内存进行交互。它不会产生CallViaStruct()的单独实现,一个用于“可能安全的C#代码”,另一个用于“可能有风险的外部代码”;它始终为可能存在风险的场景生成保守版本。这意味着它不能完全切断对NoOp()的调用,因为不能保证StructDispatch不会映射到甚至没有被分页到内存中的地址。
它知道NoOp()是空的并且可以省略(调用可以消失),但它至少必须通过戳结构的内存地址来模拟 ldfla,因为那里可能是代码取决于提出的NRE。内存解除引用就像if语句:它们可能导致分支,并且无法导致分支可能导致程序损坏。微软无法做出假设,只是说,“你的代码不应该依赖于它。”想象一下,如果NRE没有被写入业务的错误日志,那么只是因为JITter认为它不是一个“重要的”NRE而不是首先触发的,因此给微软发起了愤怒的电话。 JITter别无选择,只能解析该地址至少一次以确保正确的语义。
课程没有任何这些问题;一个班级没有强制记忆怪异。但结构却更加古怪。