获取非显式字段偏移

时间:2015-06-13 11:03:11

标签: c# .net reflection clr

我有以下课程:

[StructLayout(LayoutKind.Sequential)]
class Class
{
    public int Field1;
    public byte Field2;
    public short? Field3;
    public bool Field4;
}

如何从类数据(或对象标题)的开头获取Field4的字节偏移量?

举例说明:

Class cls = new Class();
fixed(int* ptr1 = &cls.Field1) //first field
fixed(bool* ptr2 = &cls.Field4) //requested field
{
    Console.WriteLine((byte*)ptr2-(byte*)ptr1);
}

结果偏移量在这种情况下为5,因为运行时实际上将Field3移动到类型的末尾(并填充它),可能是因为它的类型是通用的。我知道有Marshal.OffsetOf,但它返回非托管偏移,而不是托管。

如何从FieldInfo实例中检索此偏移量?是否有任何.NET方法,或者我必须编写自己的方法,考虑所有异常(类型大小,填充,显式偏移等)?

2 个答案:

答案 0 :(得分:4)

使用TypedReference.MakeTypedReference周围的一些技巧,可以获得对该字段的引用,以及对象数据的开头,然后只需减去。该方法可在SharpUtils中找到。

答案 1 :(得分:1)

.NET 4.7.2中结构中字段的偏移量:

public static int GetFieldOffset(this FieldInfo fi) => GetFieldOffset(fi.FieldHandle);

public static int GetFieldOffset(RuntimeFieldHandle h) => 
                               Marshal.ReadInt32(h.Value + (4 + IntPtr.Size)) & 0xFFFFFF;

它们返回classstruct内某个字段的字节偏移量,相对于运行时某些托管实例的布局。这适用于所有StructLayout模式,并且适用于值类型和引用类型(包括泛型,包含引用或其他不可引用的类型)。偏移值相对于structclass的用户定义内容或“数据主体”的开头,从零开始,并且不包括任何标题,前缀或其他填充字节。

讨论

由于struct类型没有标头,因此可以直接通过指针算术直接使用返回的整数偏移值,如果需要,可以直接使用System.Runtime.CompilerServices.Unsafe(此处未显示)。另一方面,引用类型的对象具有标头,必须将其跳过才能引用所需的字段。该对象标头通常是单个IntPtr,这意味着需要将IntPtr.Size添加到偏移量值。还必须首先取消引用GC(“垃圾收集”)句柄,以获取对象的地址。

考虑到这些因素,我们可以在运行时通过将字段偏移量(通过上述方法获得)与 GC对象的内部合成跟踪参考 class的实例(例如Object句柄)。

以下方法(仅对class(而不是struct)类型有意义)说明了该技术。为简单起见,它使用ref-returnSystem.Runtime.CompilerServices.Unsafe库。为了简单起见,也省略了错误检查,例如断言fi.DeclaringType.IsSubclassOf(obj.GetType())

/// <summary>
///   Returns a managed reference ("interior pointer") to field 'fi' of type 'U' in
///   managed object instance 'obj'
/// </summary>
public static unsafe ref U RefFieldValue<U>(Object obj, FieldInfo fi)
{
    var pobj = Unsafe.As<Object, IntPtr>(ref obj);
    pobj += IntPtr.Size + GetFieldOffset(fi.FieldHandle);
    return ref Unsafe.AsRef<U>(pobj.ToPointer());
}

此方法将托管指针返回到垃圾回收对象实例obj的“内部”。它可用于任意读取 字段,因此该功能代替了传统的一对独立的 getter / setter < / em>函数。

用法示例

class MyClass { public byte b_bar; public String s0, s1; public int iFoo; }

第一个演示获取s1实例中引用类型的字段MyClass的整数偏移量,然后使用它来获取和设置字段值。

var fi = typeof(MyClass).GetField("s1");

// note that we can get a field offset without actually having any instance of 'MyClass'
var offs = GetFieldOffset(fi);

// i.e., later... 

var mc = new MyClass();

RefFieldValue<String>(mc, offs) = "moo-maa";          // field "setter"

// note the use of method calls as l-values (on the left-hand side of '=' assignment)

RefFieldValue<String>(mc, offs) += "!!";              // in-situ access

Console.WriteLine(mc.s1);                             // -->  moo-maa!! (in the original)

// can be used as a non-ref "getter" for by-value access
var _ = RefFieldValue<String>(mc, offs) + "%%";       // 'mc.s1' not affected

如果这看起来有些混乱,则可以通过将托管指针保留为ref local变量来显着清除它。如您所知,每当GC移动包含 对象时,都会自动调整这种类型的指针(保留内部偏移)。这意味着,即使您继续无意识地访问该字段,它也将保持有效。作为允许使用此功能的交换,CLR要求不允许ref局部变量本身逸出其堆栈帧,在这种情况下,这是由C#编译器强制执行的。

// demonstrate using 'RuntimeFieldHandle', and accessing a value-type field (int) this time
var h = typeof(MyClass).GetField(nameof(mc.iFoo)).FieldHandle; 

// later... (still using 'mc' instance created above)

// acquire managed pointer to 'mc.iFoo'
ref int i = ref RefFieldValue<int>(mc, h);      

i = 21;                                                // directly affects 'mc.iFoo'
Console.WriteLine(mc.iFoo == 21);                      // --> true

i <<= 1;                                               // operates directly on 'mc.iFoo'
Console.WriteLine(mc.iFoo == 42);                      // --> true

// any/all 'ref' uses of 'i' just affect 'mc.iFoo' directly:
Interlocked.CompareExchange(ref i, 34, 42);            // 'mc.iFoo' (and 'i' also): 42 -> 34

摘要

这些用法示例着重于将技术与class对象一起使用,但是如前所述,此处显示的GetFieldOffset方法对于struct也可以很好地工作。只要确保不要对值类型使用RefFieldValue方法,因为该代码包括针对预期对象标头的调整。对于这种简单的情况,只需将System.Runtime.CompilerServicesUnsafe.AddByteOffset用于地址算术即可。

不用说,对于某些人来说,这种技术似乎有点激进。我会注意到,它多年来一直完美地为我工作,特别是在.NET Framework 4.7.2上,并且包括32位和64位模式,调试与发行版以及我尝试过的各种JIT优化设置