调试实战——PInvoke导致栈破坏

简介: .net 程序也能崩溃?本文记录了 PInvoke 导致的栈破坏的排查过程

缘起

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

  1. debug版不崩溃,release版稳定崩溃。
  2. 只有x64位的程序崩溃,32位anycpu编译出来的程序运行不会崩溃。
  3. 出问题的代码范围很小(按钮点击事件代码不多)。

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

排查

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

最后发现并不是由于升级.net版本导致的,而是程序本身的问题:
代码中通过P/Invoke调用了原生API GlobalMemoryStatus。在定义MemoryStatus结构体的时候强制按4字节定义了每一个字段。而在x64MemoryStatus结构体中的成员有些不是4字节大小,而是8字节大小!这样,传递给GlobalMemoryStatusMemoryStatus参数(32字节)比GlobalMemoryStatus预期的(56字节)小,导致GlobalMemoryStatus写了不该写的内存!:bomb::bomb::bomb:

重现

我把会出问题的代码独立出来了,完整的测试代码如下(请编译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选项

    关于Platform target的作用,具体参考《CLR via C#》,下图是从《CLR via C#》中文版第 4 版上截取的.
    Effects of /platform switch 摘自《CLR via C#》

总结

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

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

关于MDAs的介绍请点击这里

参考资料

相关文章
|
3月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
53 1
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
142 77
|
2天前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
1月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
43 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
46 9
|
1月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
38 7
|
3月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
99 5
|
3月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
116 21
|
3月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
3月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
71 0