人们总说 C# 没有指针?但,这是真的?答案是否定的!我个体喜欢贴近 “指针” 或者说是 “地址标识符” 编程,这会令人感到不尽的美妙,让人感到不至离被驾驭死物(计算机)太过遥远,C# 之中有哪些指针呢?从语法层面只有原生指针,但是托管指针就真的不存在了?答案是否定的,我们天天都在接触它只不过它有了一个新的叫法,叫做 “O类型引用” 但我们似乎不可以获取它引用的地址,这似乎是一件稍稍令人闹心的糟糕事情,但真的不能获取?
我从多年以前就在片段性思考这个问题,纵然是在 C/C++ .NET 语言中,我们也仅仅只是可以获取到 “托管指针” 的二级指针(写作 O^* ),“托管指针(写作 O^)” 是无法被强制转换成任意指针的,虽然它具有很多的优点,规避了 “盲/恶意托管指针” 的出现,但是也对很多人造成了困扰,虽然我们可以利用 “托管指针” 的二级指针,将其强制转换成任意类型的 “托管指针” 的二级指针,但这并不适用于 C# 语言,同样是玩指针的编程语言,让人感到一些不舒服的怪异与奇怪。
C# 语言中具有一个被 VS-IDE 隐藏的一些关键字,它们是一种比较另类的 “托管指针”,似乎它们并没有什么太大的作用?但显然不是,利用它们以指针的方式操作足够安全,同时效率也足够的高效,但是它们被编译器限制实在太严重了,各种的不被允许,让人用起来足够的糟心。
C# 语言中一个托管指针的二级指针在受支持的语法中,是由 ref 关键字代表的【ByRef 传址类型】其真实类型为 “O^&” 几乎等价于 “O^*” ,但又不同,我们无法改变 “二级指针” 的 “O^” 指针类型,而 “O^*” 却允许我们改变 “O^” 的类型,但就 C# 语言本身似乎无法做到这一切,难道真的要为此编写一个 C/C++ .NET 的 library?令人困惑又好奇。
People people = new People();
__refvalue(__makeref(people), People).Age = 26;
__refvalue(__makeref(people), People).Name = "诗青璇";
Console.WriteLine(people);
C# 语法中这个比较另类的 “托管指针” 叫做 “typedef System::TypedReference O^*” 似乎很好理解,“类型引用” 的二级指针,但它却只是指向 “真正的托管指针” 的 “一种较怪异的二级指针”,但是我们似乎没有办法可以获取到里面 “托管指针” 指向的 “真正地址”,但是可以确定一件事情就是说,它的内部标注了指向 “托管对象” 的真正的 “托管指针”,我们能不能想办法提取它内部维护的真正的地址?答案是可行的。
我们利用 ilspy 反译编 “System::TypedReference” 类型结构的 C# 代码,看清楚了定义的结构成员字段,上面有两个让我们感到兴奋有趣的字段,“private IntPtr Value”、“private IntPtr Type” !我们继续的观察 “TypedReference” 的结构发现这是一个没有定义 “托管类型字段” 很单纯的 “结构体” 类型,这让我们持续的兴奋的不能自已,这意味着我们具有好几种有效的办法可以从 “TypedReference” 结构中 “轻而易举” 的提取到 “Value” 字段的值(“Value” 保存指向托管对象的地址【真正的托管指针】)。
我们可以通过 “反射侵入到这个结构的内部,提取 Value 的值” 或者 “通过多维指针的方式来提取 “Value” 的值” 或者有人要问 dotnet 只要类型定义时没有显示的说明类的字段,分配的结构体的字段内存对齐不是错序的吗?【为防止反汇编的内存分析的一种保护机制】的确是这样的,但这里有一个前提就是说 “类型结构” 在没有参杂托管类型或者不够大的情况下几乎是按照C/C++结构体的字段内存分布顺序导出的(即:StructLayout(LayoutKind::kSequential))同时可以确定一点就是说结构的第一个字段内存分布位置是固定不变的。
People people = new People();
TypedReference reference = __makeref(people);
__refvalue(reference, People).Age = 21;
__refvalue(reference, People).Name = "李梦情";
Console.WriteLine(".NET PTR=0x{0}, .NET OBJECT=({1})", ((long)*(IntPtr**)&reference).ToString("X2"), people);
为什么要 conv_u ref_ptr To long ? 这个问题很有意思,我们都知道现在的 CPU 几乎都是 IA64(x64) 的,CPU 支持是 64bit 二进制的 “巨长地址” 寻址(俗称:QWORD PTR)一个字节占8位二进制,64/8=8 个字节;那么我们想一想在 C# 语言中那种基本数值数据类型,能足够的表示它们?long、ulong、decimal 三种,但 decimal 这个类型一般很少会用到,同时 “decimal” 太长太大了,它是一个 128bit 的数值类型(16字节),而 long 是 64bit ,那么想想这么长/大的数据类型用以表示远远超出 CPU 允许的最大地址范围,它真的适合吗?我... ...很好奇!
public struct SysInt
{
#if WRPSYSINT
public IntPtr val;
#elif WIN64
public long val;
#else
public int val;
#endif
}public unsafe static void Main(string[] args)
{
People[] peoples = new People[100];
TypedReference reference = __makeref(peoples[0]);SysInt first = *(SysInt*)&reference;
for (int i = 0; i < peoples.Length; i++)
{
*(SysInt*)&reference = first;
__refvalue(reference, People).Name = Path.GetRandomFileName();first.val += 8;
}
for (int i = 0; i < peoples.Length; i++)
{
People people = peoples[i];
Console.WriteLine(people.Name);
}
Console.ReadKey(false);
}
我们都知道 CPU 执行的代码都是存放在 “物理内存” 或者 “虚拟内存” 之中,就目前的内存单条带宽最大只能 64bit,所以我们为了提高内存的效能,就会人为增加一根或多跟新的内存到硬件上,那么可以组成双通 “128bit” 的内存带宽或多通【但 1+1 并不一定等于 2,真正的提升并不是很大】,而 CPU 执行代码需要不停的从内存中读出具体的操作代码指令,同时 CPU 在 “寻址高位地址” 时效率并不如 “寻址低位地址”,一个很明显的影响是,原来 CPU 寻址 “短地址” 时只需要从内存读入 “2个字节” 的指令,而寻址 “具长地址” 时需要从内存读入 “8个字节” 的指令,当然这不仅仅是 “CPU” 寻址效率变低的问题,内存芯片颗粒本身也会受到内存占用率增大的影响,内存利用率达到一定数量时整个系统及CPU的工作效率会逐渐的下跌,注意一点:内存的利用量并没有达到 “操作系统” 设定的内存阀值,移动内存到虚拟内存【一般为70%,但40%的时候就已经显现了整体的性能下跌】
上面提到了关于为什么 “decimal” 数值类型,并不适合作为 “地址标识符(指针)” 的一些原因,另一方面目前的 CPU 本身并不支持寻址 “decimal” 所表示的 128bit 数值范围的 “超超长地址”,同时它被视作地址的话在应用上会经历一些算术强制转换,同时这个数值的计算是由 dotnet 在 “软件层面” 实现的【效率不是很高】,注意一点:CPU 没有提供任何用以计算 128bit 的值的操作指令序列。
private static SafeGetPointer<TValue> CreateGetPointer()
{
DynamicMethod dm = new DynamicMethod(string.Empty,
typeof(void), new[]
{
typeof(TValue).MakeByRefType(),
typeof(long),
}, typeof(TValue).Module, true);
ILGenerator il = dm.GetILGenerator();il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldobj, typeof(TValue));
il.Emit(OpCodes.Stobj, typeof(TValue));il.Emit(OpCodes.Ret);
return (SafeGetPointer<TValue>)dm.CreateDelegate(typeof(SafeGetPointer<TValue>));
}
我们知道 C# 语言在语法层面没有提供任何可用于通过显示的 “托管指针” 获取或改写内存的办法,所以我们通过一些 .NET 底层的方法在 C# 语言中完成这件事情(定义一些小装置 small gadgets),上面是一个用( long 类型表示的 “托管指针”)获取指向的 “托管对象” ,它等价于:expression ->【Object^& p = *((Object^*)long_ptr);】;它相当于在 C/C++ 语言或 C# native-code 中用以获取指针指向的数据( * )运算符。
private static SafeSetPointer<TValue> CreateSetPointer()
{
DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),
new Type[]
{
typeof(long),
typeof(TValue),
}, typeof(TValue).Module, true);
ILGenerator il = dm.GetILGenerator();il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stobj, typeof(TValue));il.Emit(OpCodes.Ret);
return (SafeSetPointer<TValue>)dm.CreateDelegate(typeof(SafeSetPointer<TValue>));
}
上述代码等价于:expression ->【*((Object^*)long_ptr) = in_objval;】;它相当于在 C/C++ 语言或 C# native-code 中用以设置指针指向的数据( * )运算符,下面演示利用二级的 “托管指针” 快速的遍历一个 “托管数组” 的所有元素。
public unsafe static void Main(string[] args)
{
People[] peoples = new People[] // 演示快速的元素迭代
{
new People { Age = 24, Name = "张爱玲" },
new People { Age = 21, Name = "水云梦" },
new People { Age = 19, Name = "南宫小蝶" },
new People { Age = 23, Name = "苏璃" },
new People { Age = 25, Name = "依曦泪" },
new People { Age = 20, Name = "姜红颜" },
new People { Age = 22, Name = "欧阳小倩" },
new People { Age = 23, Name = "柳倩凝" },
};
SafePointer<People> safepointer = SafePointer<People>.GetSafePointer();
int chksz = SafePointer<People>.GetChunkSize();
long far = SafePointer<People>.GetReferencePointer(ref peoples[0]);
for (int i = 0; i < peoples.Length; i++)
{
People n = safepointer.GetValue(far);
far = far + chksz; // 位移指针位置
Console.WriteLine(n);
}
Console.ReadKey(false);
}
上述代码演示了一种遍历方面的应用,但是你要说它有多大的含义其实并没有什么,操作 “托管指针” 只有在需要的时候才会起到很关键的作用,但是大多数情况下我们并不需要直接的进行操作,当然这个 demo 代码证明了一件事情就是说对于引用类型的数组地址只占目标平台默认的大小,例如:IA32(x86)为4个字节,IA64(x64)为8个字节。
下面的代码演示一种利用 “指针” 的方式快速的栈的PUSH与POP操作,它利用本方式可以绕过,托管元素不能被获取原生指针进行操作的一些问题,当然其实还有一种办法是通过 “Marshal::UnsafeAddrOfPinnedArrayElement” 的方式,它返回一个被包装的 native-sysint 的托管形式的指针。
public unsafe static void Main(string[] args)
{
Stack<People> stack = new Stack<People>(100); // 演示快速的入栈处理
for (int i = 0; i < 100; i++)
{
stack.Push(null);
}
Console.ReadKey(false);
}
下面提供本文所示代码运行所需的代码,其就实现本身并不复杂;但是指针或者说地址标识符,它仅仅只是标识某个东西的一串地址,那么用什么类型进行标识其实都是可以的,它只需要能够代表它就可以了,并不是说指针就一定是需要有(*)之类的符号显示标注的才叫做指针,指针是一种概念,一种思想。
namespace SimpleClr
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;public unsafe static class program
{
public class People
{
public int Age;
public string Name;public override string ToString()
{
return $"Name={Name}, Age={Age}";
}
}public class SafePointer<TValue>
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private static readonly IDictionary<Type, object> pointers = new Dictionary<Type, object>();protected delegate void SafeGetPointer<TOut>(out TOut out_, long far);
protected delegate void SafeSetPointer<TIn>(long far, TIn in_);private readonly SafeGetPointer<TValue> getp;
private readonly SafeSetPointer<TValue> setp;public TValue this[long far]
{
get
{
return this.GetValue(far);
}
set
{
this.SetValue(far, value);
}
}public virtual TValue GetValue(long far)
{
TValue value;
this.getp(out value, far);
return value;
}public virtual void SetValue(long far, TValue value)
{
this.setp(far, value);
}protected SafePointer(SafeGetPointer<TValue> getp, SafeSetPointer<TValue> setp)
{
if (getp == null)
{
throw new ArgumentNullException("getp");
}
if (setp == null)
{
throw new ArgumentNullException("setp");
}
this.setp = setp;
this.getp = getp;
}public object Tag
{
get;
set;
}public static int GetChunkSize()
{
TValue[] s = new TValue[2];
TypedReference r1 = __makeref(s[0]);
TypedReference r2 = __makeref(s[1]);
long p1 = (long)*(IntPtr**)&r1;
long p2 = (long)*(IntPtr**)&r2;
long sz = p2 - p1;
return unchecked((int)sz);
}public static long GetReferencePointer(ref TValue value)
{
return GetReferencePointer(__makeref(value));
}public static long GetReferencePointer(TypedReference reference)
{
return (long)*(IntPtr**)&reference;
}public static SafePointer<TValue> GetSafePointer()
{
lock (pointers)
{
object o;
if (pointers.TryGetValue(typeof(TValue), out o))
{
return (SafePointer<TValue>)o;
}
SafePointer<TValue> pointer = new SafePointer<TValue>(CreateGetPointer(), CreateSetPointer());
pointers.Add(typeof(TValue), pointer);
return pointer;
}
}private static SafeGetPointer<TValue> CreateGetPointer()
{
DynamicMethod dm = new DynamicMethod(string.Empty,
typeof(void), new[]
{
typeof(TValue).MakeByRefType(),
typeof(long),
}, typeof(TValue).Module, true);
ILGenerator il = dm.GetILGenerator();il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldobj, typeof(TValue));
il.Emit(OpCodes.Stobj, typeof(TValue));il.Emit(OpCodes.Ret);
return (SafeGetPointer<TValue>)dm.CreateDelegate(typeof(SafeGetPointer<TValue>));
}private static SafeSetPointer<TValue> CreateSetPointer()
{
DynamicMethod dm = new DynamicMethod(string.Empty, typeof(void),
new Type[]
{
typeof(long),
typeof(TValue),
}, typeof(TValue).Module, true);
ILGenerator il = dm.GetILGenerator();il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stobj, typeof(TValue));il.Emit(OpCodes.Ret);
return (SafeSetPointer<TValue>)dm.CreateDelegate(typeof(SafeSetPointer<TValue>));
}
}public class Stack<T>
{
private readonly T[] containers = null;
private readonly long rbp = 0; // 指向栈顶部(基低指标指针)
private readonly long rsp = 0; // 指向栈的底部(堆叠指标指针)
private readonly int chksz = 0;
private long far = 0; // 指向当前元素(远元指标指针)
private GCHandle handle;
private readonly SafePointer<T> safep = SafePointer<T>.GetSafePointer();public Stack(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException("capacity");
}
this.containers = new T[capacity];
this.chksz = SafePointer<T>.GetChunkSize();
TypedReference reference = __makeref(this.containers[0]);
this.far = (long)*(IntPtr**)&reference;
this.rbp = this.far;
this.rsp = this.rbp + (this.chksz * capacity);
}~Stack()
{
GCHandle handle = this.handle;
if (handle.IsAllocated)
{
handle.Free();
}
GC.SuppressFinalize(this);
}public virtual bool Pinned()
{
GCHandle handle = this.handle;
if (!handle.IsAllocated)
{
handle = GCHandle.Alloc(this.containers);
this.handle = handle;
}
return handle.IsAllocated;
}public virtual int Chunk
{
get
{
return this.chksz;
}
}public virtual int Count
{
get
{
long n = this.rsp - this.rbp;
if (n <= 0)
{
return 0;
}
return unchecked((int)(n / this.chksz));
}
}public virtual int Position
{
get
{
long n = this.far - this.rbp;
if (n <= 0)
{
return 0;
}
return unchecked((int)(n / this.chksz));
}
}public virtual T Pop()
{
if (this.far <= this.rbp)
{
throw new InvalidOperationException("The current stack does not have any elements and is not allowed pop");
}
this.far -= this.chksz;
return this.safep[this.far];
}public virtual T Peek()
{
if (this.far <= this.rbp)
{
throw new InvalidOperationException("The current stack does not have any elements and is not allowed peek");
}
return this.safep[this.far - this.chksz];
}public virtual void Push(T item)
{
if (this.far >= this.rsp)
{
throw new InvalidOperationException("The current stack has no space available for push");
}
this.safep[this.far] = item;
this.far += this.chksz;
}
}public unsafe static void Main(string[] args)
{
Stack<People> stack = new Stack<People>(100); // 演示快速的入栈处理
for (int i = 0; i < 100; i++)
{
stack.Push(null);
}
Console.ReadKey(false);
}
}
}