缘起

最近项目中遇到一个诡异的问题,程序在升级到.net4.6.1后,执行某个功能时会崩溃,提示访问只读内存区。大概规律如下:

  1. debug版不崩溃,release版稳定崩溃。

  2. 只有x64位的程序崩溃,32位anycpu编译出来的程序运行不会崩溃。

  3. 出问题的代码范围很小(按钮点击事件代码不多)。

根据以上信息,各位小伙伴有什么思路吗?

排查

由于release版可以稳定重现,而且范围不大,故通过二分法(每次注释掉一半代码,看看是否崩溃,如果崩溃,接着注释掉一半代码,如果不崩溃说明崩溃跟注释掉的那段代码有关...)很快定位到了导致问题的代码。

最后发现并不是由于升级.net版本导致的,而是程序本身的问题:

代码中通过P/Invoke调用了原生 API GlobalMemoryStatus()。在定义MemoryStatus结构体的时候强制按4字节定义了每一个字段。而在x64MemoryStatus结构体中的成员有些不是4字节大小,而是8字节大小!这样,传递给GlobalMemoryStatus()MemoryStatus参数(32字节)比GlobalMemoryStatus()预期的(56字节)小,导致GlobalMemoryStatus写了不该写的内存!????????????

重现

我把有问题的代码独立出来了,完整的测试代码如下(请编译x64版本):

using System;using System.Runtime.InteropServices;namespace ConsoleApplication1{  class Program  {    [StructLayout(LayoutKind.Sequential)]    public struct MemoryStatus    {      [MarshalAs(UnmanagedType.U4)]      public uint dwLength;      [MarshalAs(UnmanagedType.U4)]      public uint dwMemoryLoad;      [MarshalAs(UnmanagedType.U4)]      public uint dwTotalPhys;      [MarshalAs(UnmanagedType.U4)]      public uint dwAvailPhys;      [MarshalAs(UnmanagedType.U4)]      public uint dwTotalPageFile;      [MarshalAs(UnmanagedType.U4)]      public uint dwAvailPageFile;      [MarshalAs(UnmanagedType.U4)]      public uint dwTotalVirtual;      [MarshalAs(UnmanagedType.U4)]      public uint dwAvailVirtual;    }

    [DllImport("kernel32.dll")]    public static extern void GlobalMemoryStatus(ref MemoryStatus memoryStatus);    class CMyClass    {      public int n1 = 0;    }    struct CMyStruct    {      public CMyClass data;    }    static void Main(string[] args)    {      CMyStruct myObj = new CMyStruct(); myObj.data = new CMyClass();      MemoryStatus memoryStatus = new MemoryStatus();      // this line will corrupt the stack if we run in x64.      // because memoryStatus is defined on the stack.      GlobalMemoryStatus(ref memoryStatus);      // myObj.data is corrupted      System.Console.WriteLine("{0}", myObj.data);    }  }}

修复

只需要定义MemoryStatus的时候,注意字段的大小即可。正确的MemoryStatus定义如下:

public struct MemoryStatus{  [MarshalAs(UnmanagedType.U4)]  public uint dwLength;  [MarshalAs(UnmanagedType.U4)]  public uint dwMemoryLoad;  // 以下字段 4 bytes on 32-bit Windows, 8 bytes on 64-bit Windows.  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwTotalPhys;  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwAvailPhys;  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwTotalPageFile;  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwAvailPageFile;  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwTotalVirtual;  [MarshalAs(UnmanagedType.SysUInt)]  public IntPtr dwAvailVirtual;}

思考

  • 为什么debug版不崩溃?而release版会崩溃?

    我在测试机器上调查的原因是debug版本运行的时候,关键内存恰巧没被破坏(太“幸运”或者太不幸了),而在release版本中暴露了问题。可能在其它机器上debug版本也会崩溃或者发生其它诡异的问题。

    说明:测试代码与项目中的实际代码不一样,有可能现象不一样,但问题的本质是一样的。

  • 为什么运行Any CPU编译出来的程序不崩溃?

    Platform targetAny CPU的时候,在工程属性,Build下的Prefer 32-bit的选项默认是勾选的,编译的程序会作为 32 位进程运行,所以不会崩溃。如果取消勾选,则编译出来的程序会作为 64 位应用程序运行,会崩溃。

build settings


关于Platform target的作用,具体参考《CLR via C#》,下图是从《CLR via C#》中文版第 4 版上截取的。

/platform option 截自《CLR via C#》

总结

.net程序中,令人头疼的内存破坏问题很难出现了,这极大的提高了程序的稳定性。如果出现堆破坏,很有可能跟P/Invoke或者unsafe代码相关,可以重点排查相关代码。

启用托管调试助手(Managed Debugging Assistants, 下文简称MDAs) 有时候会对调试问题有极大的帮助,虽然我这次调试没有借助MDAs,但我第一个想到的就是MDAs

关于MDAs的介绍请参考参考资料第一条

参考资料

  • Managed Debugging Assistants[1]

  • GlobalMemoryStatus[2]

  • 《CLR via C#》[3]

References

[1]  Managed Debugging Assistants:
https://docs.microsoft.com/en-us/dotnet/framework/debug-trace-profile/diagnosing-errors-with-managed-debugging-assistants

[2]  GlobalMemoryStatus:
https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-globalmemorystatus?redirectedfrom=MSDN

[3] 《CLR via C#》:

https://book.douban.com/subject/4924165/

写留言

[原]调试PInvoke导致的内存破坏相关推荐

  1. [原]调试TerminateThread导致的死锁

    这是继上一篇 [原]调试DLL卸载时的死锁 后的又一篇使用windbg调试死锁的文章.希望能对大家有所帮助. 前言 之前项目里的一个升级程序偶尔会死锁,查看dump后发现是死在了ShellExecut ...

  2. [原]调试实战——程序CPU占用率飙升,你知道如何快速定位吗?

    前言 如果我们自己的程序的CPU Usage(CPU占用率)飙升,并且居高不下,很有可能陷入了死循环.你知道怎么快速定位并解决吗?今天跟大家分享几种定位方法,希望对你有所帮助. 如何判断是否有死循环? ...

  3. linux中非法内存,Linux下数组非法访问导致内存破坏 —— 引发segmentation fault的原因...

    2012-02-05 wcdj 1, 调试时必需的栈知识 2, 数组非法访问导致内存破坏 调试时必需的栈知识 栈(stack)是程序存放数据的内存区域之一,其特征是LIFO(Last In First ...

  4. C++内存写越界导致堆内存被破坏致使new失败的问题定位总结

    问题描述: C++内存写越界导致堆内存被破坏致使new失败的问题定位总结. 报错信息:test_CRGraph: malloc.c:2379: sysmalloc: Assertion `(old_t ...

  5. VC使用CRT调试功能来检测内存泄漏

    信息来源:csdn      C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:"最大的长处也可能成为最大的弱点",那么 C/C++ 应用程序正好印证 ...

  6. VC++ 6.0 中如何使用 CRT 调试功能来检测内存泄漏[转]

    /C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:"最大的长处也可能成为最大的弱点",那么 C/C++ 应用程序正好印证了这句话.在 C/C++ 应用程 ...

  7. Linux内核的l2tp实现,Linux Kernel gdth实现内核内存破坏漏洞

    Linux Kernel gdth实现内核内存破坏漏洞 发布日期:2010-11-04 更新日期:2010-11-16 受影响系统: Linux kernel 2.6.x 描述: ---------- ...

  8. SQL Server 内存泄露(memory leak)——游标导致的内存问题

    原文:SQL Server 内存泄露(memory leak)--游标导致的内存问题 转自:http://blogs.msdn.com/b/apgcdsd/archive/2011/07/01/sql ...

  9. 使用CRT调试功能来检测内存泄漏

    C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:"最大的长处也可能成为最大的弱点",那么 C/C++ 应用程序正好印证了这句话.在 C/C++ 应用 ...

最新文章

  1. AI+IOT战火升级:未来人工智能抢人大战
  2. 深入理解JAVA虚拟机 虚拟机性能监控和故障处理工具
  3. 在CentOS7上部署.net core 控制台应用程序部署为后台服务
  4. Win11手机应用大改!全新界面来袭
  5. 从生物神经网络到人工神经网络
  6. NodeJS开源系统Mili简介
  7. (40)System Verilog线程停止(disable fork)
  8. JDBC09 CLOB文本大对象
  9. 自适应巡航跟车距离怎么调_2020款奔驰GLS450改装原厂配件 ACC自适应巡航系统 香氛香薰负离子...
  10. 圈圈教你玩usb第一版件软件使用说明
  11. jmeter安装包双击没反应_Jmeter下载安装及使用
  12. MATLAB--求一个矩阵中所有元素的平均值
  13. 计算机无法写入U盘,电脑无法拷贝U盘文件怎么办|解除U盘写保护设置的方法
  14. 3D NAND“大连造”
  15. 小武与论文的bug -CUDA -CUDANN -YOLOV3
  16. coreseek 词库 导入搜狗词库
  17. 【Windows】关于Windows Powershell找不到打不开修复方法
  18. 北航计算机考博经验,最新的北航考博经验
  19. 人脸识别论文:Partial FC: Training 10 Million Identities on a Single Machine
  20. creo草绘工程图线条粗细设置

热门文章

  1. [原]NYOJ-开灯问题-77
  2. 开通博客园,新的网络生活的开始
  3. 如何将iPhone应用程序从应用程序库移动到主屏幕
  4. 以当天日期时间,打包目录
  5. Vim的新一代补全插件:coc.nvim
  6. 递归与非递归法实现链表相加 CC150 V5 2.5题 java版
  7. 局域网速度变慢的故障分析
  8. 更改windows2003远程最大连接数
  9. t-sql里Linked server跨数据库查询
  10. Repeater分页