【发布时间】:2016-06-18 01:14:41
【问题描述】:
为什么以下 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
}
}
这是一个更完整的程序,可以用各种(发布)反编译作为 cmets 进行编译。我预计ClassDispatch 和StructDispatch 类型中CallViaStruct 的X86 是相同的,但是StructDispatch(上面提取)中的版本包括cmp 指令,而另一个没有。
看来cmp 指令是一个习惯用法,用于确保变量不为空;取消引用值为 0 的寄存器会触发一个 av,它会变成一个 NullReferenceException。但是在StructDisptach.CallViaStruct 中,鉴于ecx 指向一个结构,我无法想象它为空的方法。
更新:我希望接受的答案将包括导致 NRE 被 StructDisptach.CallViaStruct 抛出的代码,方法是让 cmp 指令取消引用归零的 ecx 寄存器。请注意,通过设置m_class = null 可以轻松使用CallViaClass 方法中的任何一个,而使用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();
}
}
}
更新:事实证明可以在非虚拟函数上使用callvirt,该函数具有对 this 指针进行空检查的副作用。虽然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 时它会出现一次:
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
}
}
【问题讨论】:
-
如果您从
StructDisptach中删除Class字段然后调用unsafe { ((StructDisptach*)0)->CallViaStruct(); },那么在cmp上触发NRE 很容易,但我想使用不安全的上下文是作弊;)跨度> -
哈哈!很好的主意!然而,JIT 是否会遇到麻烦来捕获这种情况似乎有点令人怀疑——尤其是考虑到类字段。即使这就是原因(并且看到了一个未优化的案例),如果我两次致电
NoOp我会收到两次支票。那么问题来了,第二个是干什么用的? -
是的,这就是为什么我说它会作弊 - 如果我知道你打赌我会告诉你的答案 :) 它可能只是 JIT 中的一个疏忽据我所知,因为我想不出任何更好的解释来解释
cmp在这里:-\ -
您的反汇编样本是来自调试版本吗?这可能是编译器/JITer 的方式,至少留下一条可以说是“属于”您的无操作函数的指令,以便您可以逐行遍历源代码。
-
原来指令是因为
ldflda而不是call。使用 ildasm/ilasm 重写方法表明情况确实如此。所以在我看来,它可以被优化掉。但是,在不平凡的情况下,下一个问题是当字段位于结构中时,ldfda是否需要cmp指令?