为什么以下C#方法的X86 CallViaStruct包含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
}
}
Run Code Online (Sandbox Code Playgroud)
这是一个更完整的程序,可以使用各种(发布)解压缩作为注释进行编译.我期望CallViaStruct两者ClassDispatch和StructDispatch类型中的X86 相同但是StructDispatch(上面提取的)中的版本包括cmp指令而另一个没有.
看来该cmp指令是一个成语用于确保变量不为空; 取消引用值为0的寄存器会触发一个av转为a的寄存器NullReferenceException.然而,在StructDisptach.CallViaStruct我ecx指向一个结构时,我无法设想为null 的方法.
更新:我希望接受的答案将包括StructDisptach.CallViaStruct通过使其cmp指令取消引用归零ecx寄存器而导致NRE被抛出的代码.请注意,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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
更新:原来可以callvirt在非虚函数上使用,该函数具有null检查this指针的副作用.虽然这是CallViaClasscallsite 的情况(这就是我们在那里看到null检查的原因)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
Run Code Online (Sandbox Code Playgroud)
更新:有人建议cmp可以捕获null这个指针没有被困在调用站点的情况.如果是这种情况,那么我希望cmp在方法的顶部出现一次.但是,每次调用都会出现一次NoOp:
struct StructDisptach {
Struct m_struct;
[MethodImpl(MethodImplOptions.NoInlining)]
public void CallViaStruct() {
m_struct.NoOp();
m_struct.NoOp();
//push ebp
//mov ebp,esp
//cmp byte ptr [ecx],al
//cmp byte ptr [ecx],al
//pop ebp
//ret
}
}
Run Code Online (Sandbox Code Playgroud)
简短回答:JITter 无法证明该结构没有被指针引用,并且必须在每次调用 NoOp() 时至少取消引用一次以获得正确的行为。
\n\n长答案:结构很奇怪。
\n\nJITter 是保守的。只要有可能,它只能以绝对确定产生正确行为的方式优化代码。“大部分正确”还不够好。
\n\n现在这里有一个示例场景,如果 JITter 优化取消引用,该场景就会中断。考虑以下事实:
\n\n第一:请记住,结构可以(并且确实!)存在于 C# \xe2\x80\x94 之外,例如,指向 StructDispatch 的指针可能来自非托管代码。正如卢卡斯指出的,你可以使用指针来作弊;但 JITter 无法确定您没有在代码中的其他位置使用指向 StructDispatch 的指针。
\n\n第二:请记住,在非托管代码中(这是结构存在的最大原因),所有的赌注都将被取消。仅仅因为您刚刚从内存中读取了一个值,并不意味着它会是相同的值,甚至在您下次读取相同的精确地址时也是一个值。线程和多处理实际上可以在下一个时钟周期改变该值,更不用说像 DMA 这样的非 CPU 参与者了。并行线程可以 VirtualFree() 包含该结构的页面,并且 JITter 必须防范这种情况。您要求从内存中读取数据,因此您会从内存中读取数据。我的猜测是,如果您启动优化器,它会删除这些 cmp 指令之一,但我非常怀疑它是否会删除这两条指令。
\n\n第三:异常也是真实的代码。NullReferenceException 并不一定会停止程序;它可以被抓住并处理。这意味着从 JITter 的角度来看,NRE 更像是 if 语句而不是 goto:它是一种必须在每次内存取消引用时处理和考虑的条件分支。
\n\n现在把这些部分放在一起。
\n\nJITter 不知道 \xe2\x80\x94 并且无法知道 \xe2\x80\x94 您没有使用不安全的 C# 或其他地方的外部源来与 StructDispatch 的内存进行交互。它不会生成 CallViaStruct() 的单独实现,一种用于“可能安全的 C# 代码”,另一种用于“可能有风险的外部代码”;它总是为可能存在风险的情况生成保守版本。这意味着它不能完全删除对 NoOp() 的调用,因为不能保证 StructDispatch 不会映射到甚至没有分页到内存中的地址。
\n\n它知道 NoOp() 是空的并且可以被省略(调用可以消失),但它至少必须通过戳结构的内存地址来模拟ldfla,因为可能存在依赖于引发的 NRE 的代码。内存取消引用就像 if 语句:它们可以导致分支,而无法导致分支可能会导致程序损坏。微软不能做出假设并只是说“你的代码不应该依赖于此”。想象一下,如果仅仅因为 JITter 认为它不是一个“足够重要”的 NRE 而没有将 NRE 写入企业的错误日志,就会愤怒地打电话给 Microsoft。JITter 别无选择,只能取消引用该地址至少一次以确保正确的语义。
\n\n类没有任何这些问题;类中没有强制的记忆怪异。但结构却更加古怪。
\n| 归档时间: |
|
| 查看次数: |
510 次 |
| 最近记录: |