如何在保留装箱对象的前提下修改值
有人问如何在保留装箱对象的前提下修改值?
场景:
1 2 3 4 | object obj = 100; Console.WriteLine( "original object value: " + obj.ToString()); // when debug, make obj's ID: 1# //TODO: modify obj value here (to 1000, for example), but preserve obj object Console.WriteLine( "modified object value: " + obj.ToString()); // make sure obj's ID: 1# |
分析:
显然这里直接obj = 1000是不行的,那样之后得到的是对1000装箱的对象,而不是对100的装箱对象了,那么如何修改呢?
首先,这里列出本文涉及的一些.NET和CLR的准备知识——装箱的对象的分配和存储、对象的托管内存地址获取、对象唯一性确定、托管内存数据读写。如果你不是很熟悉,没关系,经过本篇的实践,加上MSDN的解释,你很快就可以理解。
1、对象的分配和存储。这里设计的仅仅是部分,细节可以参考CLR via。对象分配在托管堆上,由几个部分组成,第一部分是存储的是对象类型的TypeHandle,其后内容随类型不同而不同;对于装箱对象,其后紧跟的内存存储的是装箱的值(就是我们要找到然后去修改的东东了)。
2、对象的托管内存地址获取。通过System.Runtime.InteropServices.GCHandle类和其上的静态方法获取。
3、对象唯一性确定。这个方法有两种,第一种,需要依赖VS IDE Debug环境,在IDE的debug下,可以对任何对象设置对象标识(object ID),通过对象标识,就可以知道对象的往生来去了。另一种办法则是利用第二条知识,使用GCHandle的IsAllocated来判断。
4、通过上面得到了托管地址,如何修改托管地址处保存的内容呢?使用System.Runtime.InteropServices.Marshal.StructureToPtr或者System.Runtime.InteropServices.Marshal.WriteXXX系列方法即可。
基于以上内容,我们可以可以做到在保留装箱对象的前提下修改值了,显然首先需要的是装箱对象的引用,然后调用System.Runtime.InteropServices.GCHandle.Aloc(object)得到托管地址,该托管地址指向的内容就是装箱的对象;由于装箱对象的第一部分是TypeHandle,所以需要将指针向后偏移IntPtr.Size得到数据存储地址,然后通过Marshal.StructureToPtr写入新的内容即可。代码片段如下
1 2 3 4 5 6 7 | if (!_memData.IsAllocated) { _memData = GCHandle.Alloc(_boxedObject); } IntPtr pMemData = GCHandle.ToIntPtr(_memData); IntPtr pBox = new IntPtr((Marshal.ReadIntPtr(pMemData).ToInt64() + IntPtr.Size)); Marshal.StructureToPtr(value, pBox, false ); |
结果:
讨论:
显然这里写入数据时候是需要很小心的,因为如果装箱的数据占用内存小,而写入的数据比它大的话,就会触发AccessViolationException,甚至导致溢出,形成安全漏洞。
额外话题:
如果传入的就是一个引用类型的实例,会是什么结果呢?
还等什么呢,赶快自己动手试试喽。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | using System; using System.Runtime.InteropServices; namespace BoxedObjectWriter { class Program { static void Main( string [] args) { object test = 100; Console.WriteLine( "original value=" + test.ToString() + ", hash=" + test.GetHashCode()); BoxedObject b = new BoxedObject(test); b.Value = 1000; Console.WriteLine( "after edit value=" + test.ToString() + ", hash=" + test.GetHashCode()); Console.ReadLine(); } } public class BoxedObject : IDisposable { private object _boxedObject; private GCHandle _memData; public BoxedObject( object boxObject) { if (boxObject == null ) { throw new ArgumentNullException(); } _boxedObject = boxObject; } ~BoxedObject() { ( this as IDisposable).Dispose(); } public object Value { get { return _boxedObject; } set { if (value == null ) { throw new ArgumentNullException(); } if (value.GetType() != _boxedObject.GetType()) { throw new NotSupportedException( string .Format( "Can not set [{0}] value to [{1}] object" , value.GetType().Name, _boxedObject.GetType().Name)); } if (!_memData.IsAllocated) { _memData = GCHandle.Alloc(_boxedObject); } IntPtr pMemData = GCHandle.ToIntPtr(_memData); IntPtr pBox = new IntPtr((Marshal.ReadIntPtr(pMemData).ToInt64() + IntPtr.Size)); Marshal.StructureToPtr(value, pBox, false ); } } IDisposable Members } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理