导读:本期聚焦于韦伯创作的《C#怎么读取内存中的数据?使用指针读取非托管内存的完整指南》,敬请观看详情。在C#里读取内存数据并不只有托管对象一种方式。当数据来自非托管DLL返回的指针、本机堆分配或跨进程地址空间时,直接使用指针和内存访问API往往更直接。本文围绕C#读取内存的常见场景展开,先说明托管堆与非托管内存的边界,以及GC移动引用带来的读取陷阱;随后演示Marshal类的ReadInt32、ReadIntPtr、Copy等方法如何在不开启unsafe的情况下安全读取线性内存;接着介绍项目如何启用unsafe编译选项,并用byte*、int*等指针完成结构体解析和批量读取;最后补充使用kernel32的ReadProcessMemory读取其他进程内存的调用方式,以及x86与x64平台下指针长度、内存对齐和异常处理等注意事项。读者可以根据实际互操作需求选择合适的方案,避免只依赖托管数组导致不必要的复制。

C#运行时的托管内存由垃圾回收器统一管理,但这并不意味着所有数据都躺在托管堆上。与C/C++动态库交互时,经常会拿到IntPtr类型的原始地址,它可能指向malloc分配的内存、COM对象内部缓冲区,或者操作系统返回的虚拟内存页。如果直接把这个指针当作托管数组使用,既无法通过GC访问,也难以利用索引语法。理解托管与非托管内存的边界,是正确读取这类数据的前提。

C#怎么读取内存中的数据?使用指针读取非托管内存的完整指南

本文会从非托管内存的基本读写开始,逐步介绍Marshal类、unsafe指针以及跨进程读取接口。无论你是做硬件SDK接入、自动化工具开发,还是排查原生库返回的数据异常,这些方法都能派上用场。

一、先弄清楚托管内存和非托管内存的区别

托管代码创建的对象一般由CLR分配在托管堆上,垃圾回收器会负责整理和移动这些对象。对于普通C#数组来说,我们不需要关心它的实际地址,因为GC会在必要时调整它的位置。但非托管内存完全不受CLR管理,它通常来自Marshal.AllocHGlobal、VirtualAlloc、C库的malloc,或者由非托管DLL返回的原始IntPtr。这些内存不会自动释放,也不会被GC移动,因此可以直接通过地址读取。

很多读取异常正是因为没有区分这两种内存。例如把一个非托管指针错误地当成托管数组的引用去访问,或者反过来在非托管回调中保存了托管对象的地址,却没有固定该对象。针对托管对象,如果确实需要拿到固定地址,可以使用fixed语句或GCHandle.Alloc的GCHandleType.Pinned,但这不属于常规读取非托管内存的场景。下面先看一段最基础的非托管内存写入和读回代码。

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        IntPtr buffer = Marshal.AllocHGlobal(100);
        try
        {
            Marshal.WriteInt32(buffer, 0, 42);
            Marshal.WriteByte(buffer, 4, 0x7F);

            int intValue = Marshal.ReadInt32(buffer, 0);
            byte byteValue = Marshal.ReadByte(buffer, 4);

            Console.WriteLine($"{intValue}, {byteValue}");
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }
}

这段代码没有任何unsafe指针,却完成了对非托管内存的写入和读取。Marshal.WriteInt32会把数据写到IntPtr指定的偏移处,Marshal.ReadInt32再把它读回来。偏移量从0开始,与C语言中的指针地址偏移一致。这种方式适合访问数量不多、类型明确的内存区域。

二、用Marshal类读取非托管内存中的结构体

如果非托管内存里存放的是结构体或者结构体数组,Marshal.PtrToStructure和Marshal.SizeOf是更直接的工具。它们可以在IntPtr和托管对象之间做转换,同时处理布局、对齐和字节序等细节。需要注意的是,目标结构体必须用StructLayout标注,通常是LayoutKind.Sequential,以保证字段顺序与C/C++结构一致。

以下示例模拟一个非托管DLL返回两个Point结构体的场景。通过计算每个元素的大小,再用指针偏移依次读取,可以避免一次性复制整个缓冲区。

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
struct Point
{
    public int X;
    public int Y;
}

class Program
{
    static void Main()
    {
        IntPtr ptr = nativeGetPoints();
        try
        {
            for (int i = 0; i < 2; i++)
            {
                IntPtr p = IntPtr.Add(ptr, i * Marshal.SizeOf<Point>());
                Point point = Marshal.PtrToStructure<Point>(p);
                Console.WriteLine(point.X + "," + point.Y);
            }
        }
        finally
        {
            nativeFree(ptr);
        }
    }

    [DllImport("native.dll")]
    static extern IntPtr nativeGetPoints();

    [DllImport("native.dll")]
    static extern void nativeFree(IntPtr ptr);
}

这里Marshal.SizeOf<Point>()返回该结构在非托管内存中的实际大小,IntPtr.Add负责做指针偏移。对于包含数组、字符串或指针字段的复杂结构,还需要配合MarshalAs特性明确长度和类型,否则读取结果可能不完整或引发访问违例。

三、启用unsafe并用指针直接读取

有些高性能场景下,逐次调用Marshal.ReadInt32会产生额外的平台调用开销。此时可以开启C#的unsafe上下文,直接把IntPtr转换为原生指针。需要在项目文件中把<AllowUnsafeBlocks>true</AllowUnsafeBlocks>加入PropertyGroup,然后在代码中使用unsafe关键字。

原生指针的优势在于可以通过+、*等操作符进行地址计算和取值,代码风格更接近C语言。下面这段程序分配16字节非托管内存,用int*写入两个整数,再用byte*写入一个字节,最后从同一段内存中读取。

using System;
using System.Runtime.InteropServices;

class Program
{
    unsafe static void Main()
    {
        IntPtr buffer = Marshal.AllocHGlobal(16);
        try
        {
            int* p = (int*)buffer;
            *p = 100;
            *(p + 1) = 200;
            byte* pb = (byte*)(p + 2);
            *pb = 0xAA;

            int first = *p;
            int second = *(p + 1);
            byte third = *pb;

            Console.WriteLine(first + "-" + second + "-" + third);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }
}

指针运算完全绕过了Marshal的封装,因此在频繁读取大量数据时更加高效。但它也把内存安全的责任交给了开发者:越界、野指针、错误对齐都可能直接导致进程崩溃。建议只在明确知道内存布局并且确实需要性能优化时使用。

四、跨进程读取内存:ReadProcessMemory

除了读取本进程的非托管内存,有时还需要读取另一个进程的地址空间,例如调试器、性能分析工具或自动化程序。Windows提供了ReadProcessMemory这一原生API,C#可以通过DllImport调用它。使用前需要打开目标进程并获得足够的权限,通常PROCESS_VM_READ就足够。

ReadProcessMemory的缓冲区是字节数组,这意味着你可以先按字节读取,再根据偏移手动解析整数、浮点数或结构体。下面的类封装了字节读取过程,读取失败时抛出Win32Exception。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

class ProcessMemoryReader
{
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool ReadProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        byte[] lpBuffer,
        int dwSize,
        out int lpNumberOfBytesRead);

    public static byte[] ReadBytes(Process process, IntPtr address, int size)
    {
        byte[] buffer = new byte[size];
        int bytesRead;
        bool ok = ReadProcessMemory(
            process.Handle,
            address,
            buffer,
            size,
            out bytesRead);
        if (!ok)
        {
            throw new System.ComponentModel.Win32Exception(
                Marshal.GetLastWin32Error());
        }
        return buffer;
    }
}

跨进程读取很容易触发权限和稳定性问题。目标进程的地址必须在当前会话中可访问,受保护的系统进程或存在反调试机制的程序会拒绝读取。实践中应当仅用于自己开发或授权的程序,并做好异常处理和超时控制。

五、固定托管对象与内存对齐的注意点

有时候非托管代码希望直接使用托管数组,例如把byte[]传给音频采集SDK。由于GC可能移动对象,必须使用fixed语句暂时固定它。下面代码把managed数组固定后,通过指针填充数据,退出fixed块后GC恢复管理。

using System;
using System.Runtime.InteropServices;

class Program
{
    unsafe static void Main()
    {
        byte[] managed = new byte[64];
        fixed (byte* p = managed)
        {
            for (int i = 0; i < managed.Length; i++)
            {
                p[i] = (byte)(i * 2);
            }
        }
        Console.WriteLine(managed[10]);
    }
}

内存对齐是另一个容易忽略的问题。在64位平台上,指针长度是8字节,IntPtr的大小也会随之变化。如果结构体包含long、double或指针字段,C#默认会按照自然对齐方式排列,但C/C++库可能使用#pragma pack改变对齐规则。这时候需要给StructLayout指定Pack参数,否则Marshal.PtrToStructure读到的字段位置会偏移错误。

至于性能,连续读取大块数据时,可以优先考虑Span<T>和MemoryMarshal。它们既能提供类似数组的索引访问,又可以避免复制和额外的平台调用。不过创建指向非托管内存的Span通常仍需要unsafe上下文,所以本质上是对指针操作的进一步封装。

六、选择合适的读取方案

如果只是偶尔读取几个整数或字节,Marshal.ReadInt32、Marshal.ReadByte最省事,不需要项目开启unsafe。如果需要读取结构体或结构体数组,Marshal.PtrToStructure配合StructLayout是标准做法。当数据量大、访问频繁,或者需要灵活做地址运算时,unsafe指针能提供最高性能,但要求开发者对内存布局有准确把控。

跨进程场景则必须依赖ReadProcessMemory或更高层的内存读取组件。这类操作已经超出了普通非托管内存读取的范畴,涉及操作系统安全模型,不能简单用Marshal替代。无论选择哪种方案,都要牢记释放非托管内存、固定托管对象、校验偏移和大小,避免悬空指针和访问越界。

C#内存读取unsafe指针非托管内存修改时间:2026-09-28 07:30:26

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0928/62895.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。