离奇现象

大家在C#代码中遇到这样的问题吗:一个局部变量,上一秒还是非null的,下一秒就变成null了,中间只调用了一个非托管函数。

我前几天就遇到了这样的问题,问题代码长这样:

private static PropVariant GetProperty(Window window, PropertyKey key)
{
var hwnd = new WindowInteropHelper(window).EnsureHandle();
Win32.Shell32.SHGetPropertyStoreForWindow(hwnd, out IPropertyStore ps);
ps.GetValue(ref key, out PropVariant value);
return value;
}

ps是一个COM类型的对象,本来是非null的,调用完它的GetValue方法后,就变成null了,离奇地是,没有任何报错,调用返回值是正常的,value的结果也是正确的。

在这里,IPropertyStore是一个Com类型,PropVariant类型的原型是这样的,具体可看官方文档:PROPVARIANT 结构 (propidl.h)

typedef struct tagPROPVARIANT {
union {
typedef struct {
VARTYPE vt;
PROPVAR_PAD1 wReserved1;
PROPVAR_PAD2 wReserved2;
PROPVAR_PAD3 wReserved3;
union {
//....此处省略
};
} tag_inner_PROPVARIANT, PROPVARIANT, *LPPROPVARIANT;
DECIMAL decVal;
};
} PROPVARIANT, *LPPROPVARIANT;

然后我是这样封装的

[StructLayout(LayoutKind.Sequential)]
internal record struct PropVariant(ushort vt, IntPtr pointer);

暗藏玄机

由于我对平台调用的机制不是特别熟悉,所以一开始想到的是,是不是因为我封装的结构体不对,导致平台调用的内部发生了什么异常。但是仔细一想感觉也不太可能,非托管代码里面不管发生了什么,应该都不会直接影响到我托管代码里的变量的值吧。

但是我觉得大概率还是PropVariant封装的有问题,所以我就尝试了一下把PropVariant的大小申明为128字节,再试了一下,果然就没问题了。

[StructLayout(LayoutKind.Sequential, Size = 128)]
internal record struct PropVariant(ushort vt, IntPtr pointer);

这个封装在64位应用里面默认大小是16字节,我改成显式申明为128字节之后就没问题了,那么说明平台调用对PropVariant的操作实际上是大于16字节的,因为访问越界把ps变量的值给写掉了。不过问题是,这个越界是怎么越到ps这个变量上去的。

因为搞不懂到底非托管代码是怎么写到我的ps变量的,于是我就不断尝试变量申明的方式,试图避免非托管代码去改写我的ps变量。终于在尝试到下面这种写法的时候,让我发现了一点端倪:

    IPropertyStore ps;
var hwnd = new WindowInteropHelper(window).EnsureHandle();
Win32.Shell32.SHGetPropertyStoreForWindow(hwnd, out ps);
ps.GetValue(ref key, out PropVariant value);
return value;

现在,被改写的变量从ps变成了hwnd,那么很容易可以联想到,局部变量是按照某种顺序排列在一起,当操作PropVariant越界的时候,自然就写到了它后面的变量。现在这种写法,大概率是改变了局部变量的排序,所以被改写的变量从ps变成了hwnd。

于是,我拿出了我几个月前刚学的的阅读IL代码的技能。GetValue这一行的IL代码是这样的:

    IL_0016: ldloc.0      // ps
IL_0017: ldarga.s key
IL_0019: ldloca.s 'value'
IL_001b: callvirt instance int32 H3C.Windows.Core.IPropertyStore::GetValue(valuetype H3C.Windows.Core.PropertyKey&, valuetype H3C.Windows.Core.PropVariant&)
IL_0020: pop

ldloca.s指令把'value'这个变量的地址放入到计算堆栈中,然后调用了GetValue方法,于是平台调用在往'value'这个变量的地址写数据的时候,就越界到其他变量去了。

那'value'变量是存在哪里的呢,我搜索了一下,搜到了一个叫局部变量表(Record Frame)的东西,它在IL代码里长这样:

    .locals init (
[0] class H3C.Windows.Core.IPropertyStore ps,
[1] native int hwnd,
[2] valuetype H3C.Windows.Core.PropVariant 'value',
[3] valuetype H3C.Windows.Core.PropVariant V_3
)

然后,在我的原始代码里,它长这样:

    .locals init (
[0] native int hwnd,
[1] class H3C.Windows.Core.IPropertyStore ps,
[2] valuetype H3C.Windows.Core.PropVariant 'value',
[3] valuetype H3C.Windows.Core.PropVariant V_3
)

可以看到,hwnd和ps交换了位置。那么这个局部变量表应该就是以0~3这样的顺序入栈的,所以当写'value'变量越界的时候,就写到了它后面的hwnd或者ps的数据。

罪魁祸首

看到这里,几乎就可以肯定问题是PropVariant封装的大小不对了。PropVariant的原型还挺复杂的,套了几层的union。我为了图方便,让ai给我写了个例子,所以这个封装方式其实是ai帮我写的(小小ai,速来背锅),在调用IPropertyStore.SetValue方法的时候倒是没发现什么异常,没想到在这里给我埋了一个坑。

于是我用Win32Cs这个包生成了一个PropVariant的封装,首先确认了一下Win32Cs给我生成的封装调用是正常的,然后调用Marshal.SizeOf<PROPVARIANT>();看了一下它封装的大小,发现实际上是24字节。

这时候死去的C语言记忆突然开始攻击我:我们都知道,union类型的大小是它占用内存最大的成员的大小(还有考虑内存对齐)。让我们再来看看PropVariant的原型:

typedef struct tagPROPVARIANT {
union {
typedef struct {
VARTYPE vt;
PROPVAR_PAD1 wReserved1;
PROPVAR_PAD2 wReserved2;
PROPVAR_PAD3 wReserved3;
union {
//....此处省略
};
} tag_inner_PROPVARIANT, PROPVARIANT, *LPPROPVARIANT;
DECIMAL decVal;
};
} PROPVARIANT, *LPPROPVARIANT;

中间有个union的成员我省略掉了,为什么省略掉,因为太多了......于是我让ai帮我封装,它给我把union直接封装成了IntPtr,我一想也挺合理的,拿到IntPtr,想要什么类型的值再自己转换嘛,完全没有注意到这一长串的成员里面有一些长度超过了8个字节的结构体(关键也是我完全忘了union类型的大小了)。

这个union里面的某些成员的类型,大小是16个字节的,例如这个结构体类型,它是一个uint加一个指针:

[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Windows.CsWin32", "0.3.183+73e6125f79.RR")]
internal partial struct CALPWSTR
{
internal uint cElems; internal unsafe winmdroot.Foundation.PWSTR* pElems;
}

所以这个union的大小是16个字节,然后加上前面的8个字节,tag_inner_PROPVARIANT这个结构体的大小是24字节。外层的union中,DECIMAL在文档中说明是和tag_inner_PROPVARIANT具有相同的大小,所以,整个结构体的大小是24字节。

最后,我的代码就改成这样了:

[StructLayout(LayoutKind.Sequential, Size = 24)]
internal record struct PropVariant(ushort vt, IntPtr pointer);

没错,只要显式申明封装大小是24字节就可以了,因为那个成员巨多的union里面,我只用到了string和bool,所以就不做复杂的封装了。

记一次C#平台调用中因非托管union类型导致的内存访问越界的更多相关文章

  1. 解决MWPhotoBrowser中的SDWebImage加载大图导致的内存警告问题

    下面两种现象,用同一种方法解决 1.解决MWPhotoBrowser中的SDWebImage加载大图导致的内存警告问题 2.突然有一天首页访问图片很慢,至少隔20多秒所有图片才会出来.(解析:app使 ...

  2. 记一次 .NET 某打印服务 非托管内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的程序出现了内存泄漏,能不能帮他看一下,这个问题还是比较经典的,加上好久没上非托管方面的东西了,这篇就和大家分享一下,话不多说,上 WinD ...

  3. C# 互操作性入门系列(三):平台调用中的数据封送处理

    好文章搬用工模式启动ing ..... { 文章中已经包含了原文链接 就不再次粘贴了 言明 改文章是一个系列,但只收录了2篇,原因是 够用了 } --------------------------- ...

  4. [转]C# 互操作性入门系列(三):平台调用中的数据封送处理

    参考网址:https://www.cnblogs.com/FongLuo/p/4512738.html C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列( ...

  5. C# 中托管内存与非托管内存之间的转换

    c#有自己的内存回收机制,所以在c#中我们可以只new,不用关心怎样delete,c#使用gc来清理内存,这部分内存就是managed memory,大部分时候我们工作于c#环境中,都是在使用托管内存 ...

  6. (转)C#调用非托管Win 32 DLL

    转载学习收藏,原文地址http://www.cnblogs.com/mywebname/articles/2291876.html 背景 在项目过程中,有时候你需要调用非C#编写的DLL文件,尤其在使 ...

  7. 托管DLL和非托管DLL的区别

    首先解释一下,托管DLL和非托管DLL的区别.狭义解释讲,托管DLL就在Dotnet环境生成的DLL文件.非托管DLL不是在Dotnet环 境生成的DLL文件. 托管DLL文件,可以在Dotnet环境 ...

  8. C#的三大难点之二:托管与非托管

    相关文章: C#的三大难点之前传:什么时候应该使用C#?​C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 ...

  9. C#内存管理之托管堆与非托管堆( reprint )

    在 .NET Framework 中,内存中的资源(即所有二进制信息的集合)分为“托管资源”和“非托管资源”.托管资源必须接受 .NET Framework 的 CLR (通用语言运行时)的管理(诸如 ...

  10. C# 托管和非托管混合编程

    在非托管模块中实现你比较重要的算法,然后通过 CLR 的平台互操作,来使托管代码调用它,这样程序仍然能够正常工作,但对非托管的本地代码进行反编译,就很困难.   最直接的实现托管与非托管编程的方法就是 ...

随机推荐

  1. .Net 组件库先混淆签名,再打包成.nupkg包

    目前,我们项目组打算做增量升级的功能,这涉及到dll库增量改变,只有修改过代码的dll才需要有文件的变化,否则文件和对应的版本是不会改变的. 我们之前的打包的项目有一个小缺陷: 在整个应用打包输出的时 ...

  2. Qt图像处理技术七:轮廓提取

    Qt图像处理技术七:轮廓提取 效果图 原理 图像先二值化让rgb数值相同,只有(0,0,0)或者(255,255,255) 取每个点的周围8个点,如果周围8个点与该点rgb值相同,则需要将该点描黑为( ...

  3. 使用Spring AOP 和自定义注解统一API返回值格式

    摘要:统一接口返回值格式后,可以提高项目组前后端的产出比,降低沟通成本.因此,在借鉴前人处理方法的基础上,通过分析资料,探索建立了一套使用Spring AOP和自定义注解无侵入式地统一返回数据格式的方 ...

  4. Spring AOP 面向切面编程之AOP是什么

    前言   软件工程有一个基本原则叫做"关注点分离"(Concern Separation),通俗的理解就是不同的问题交给不同的部分去解决,每部分专注于解决自己的问题.这年头互联网也 ...

  5. Django Web应用开发实战附录A

    Django面试题 1.Python解释器有哪些类型,有什么特点? CPython:由C语言开发,而且使用范围最广泛IPython:基于CPython的一个交互式计时器PyPy:提高执行效率,采用JI ...

  6. vllm

    !声明:本文部分框架及理论来自于 [大猿搬砖简记] 的公众号文章,但为了方便本人学习,进行了整理,同时在这个清晰的框架内添加了一些总结性质的内容,如需看原文请在其公众号中搜索:图解大模型计算加速系列. ...

  7. [abc306h/ex] Balance Scale

    Ex - Balance Scale 考虑只有>和<的情况,相当于给每条边定向,当且仅当成环时不合法,那么方案数就是\(DAG\)的方案数 对于=,就是将两个点合并 然后对于一般的求\(n ...

  8. 八、make编译输出重定向

    4.编译输出重定向 ​ 将 make 命令的标准输出(stdout)和标准错误输出(stderr)重定向到文件,以便于查看编译日志,快速分析定位问题. 1.重定向到同一个文件 语法: make > ...

  9. C# winform 打开设计时,也会执行编写的代码,

    if (System.Diagnostics.Process.GetCurrentProcess().ProcessName == "devenv")//判断是否为设计时 { re ...

  10. Jquery获取div的宽度与高度

    https://blog.csdn.net/qq2468103252/article/details/82835563 宽度$('div').width(); 区块的本身宽度$('div').oute ...