Warum enthält der X86 für die folgende C# -Methode die Anweisung cmp
?Warum überprüfen C# -Struct-Instanzmethoden, die Instanzmethoden in einem Struct-Feld aufrufen, zuerst ecx?
Hier ist ein vollständigeres Programm, das mit verschiedenen (Release-) Dekompilierungen als Kommentare zusammengestellt werden kann. Ich erwartete, dass die X86 für CallViaStruct
sowohl in ClassDispatch
und StructDispatch
Typen identisch sein, aber die Version in StructDispatch
(extrahiert oben) enthält eine cmp
Anweisung, während die andere nicht.
Es scheint die cmp
Anweisung ist ein Idiom wird verwendet, um sicherzustellen, dass eine Variable nicht null ist; Dereferenzierung eines Registers mit dem Wert 0 löst eine av
aus, die in eine NullReferenceException
umgewandelt wird. Aber in StructDisptach.CallViaStruct
kann ich mir keinen Weg vorstellen für ecx
als null, da es auf eine Struktur zeigt.
UPDATE: Die Antwort, die ich suche bin zu werden Code annehmen, umfassen, die eine NRE verursacht durch StructDisptach.CallViaStruct
geworfen werden, indem es mit cmp
Anweisung dereferenziert ein genullt ecx
Register. Beachten Sie, dass dies mit den beiden Methoden CallViaClass
durch Setzen von m_class = null
und ClassDisptach.CallViaStruct
problemlos möglich ist, da es keine cmp
Anweisung gibt.
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();
}
}
}
UPDATE: Stellt sich heraus, es möglich ist, callvirt
auf einer nicht-virtuelle Funktion zu verwenden, das eine Nebenwirkung von null hat den diesen Zeiger zu überprüfen. Während dies für die CallViaClass
Callsite der Fall ist (weshalb wir den Null-Check dort sehen) StructDispatch.CallViaStruct
verwendet eine call
Anweisung.
.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
UPDATE: Es gab einen Vorschlag, dass die cmp
für den Fall Trapping werden könnte, wo ein null
diese Zeiger nicht an der Aufrufstelle gefangen wurden. Wenn das der Fall wäre, würde ich erwarten, dass die cmp
einmal am Anfang der Methode auftritt. Allerdings scheint es, einmal für jeden Anruf zu 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
}
}
Das Auslösen eines NRE auf diesem 'cmp' ist einfach, wenn Sie das Feld' Class' aus 'StructDisptach' entfernen und dann' unsafe {((StructDisptach *) 0) aufrufen -> CallViaStruct(); } ', aber ich schätze, dass die Verwendung eines unsicheren Kontextes betrügt;) –
HaHA! Großartige Idee! Es scheint jedoch ein wenig zweifelhaft zu sein, dass das JIT die Probleme für diesen Fall auffangen würde - insbesondere angesichts des Klassenfeldes. Und selbst wenn das der Grund ist (und einen unoptimierten Fall sah), wenn ich zwei Anrufe an "NoOp" stelle, bekomme ich den Scheck zweimal. Also wäre die Frage, wofür ist die zweite? –
Ja, deshalb sage ich, dass es betrügen würde - wenn ich die Antwort wüsste, würde ich es dir sagen :) Es könnte doch nur ein Versehen in der JIT sein, denn ich weiß es nicht jede bessere Erklärung für dieses 'cmp' ist hier: - \ –