报错信息(编辑时间2024年9月28号)
由于在MS.Win32.UnsafeNativeMethods.RegisterClassEx产生了报错信息,但是一直向外部抛出错误但始终没有被捕捉成功,直到报错被UI线程捕获,但是仍然没有进行处理,所有造成WPF的应用闪退。
解析报错信息
1.从异常初始位置查看原因
我们可以找到MS.Win32.UnsafeNativeMethods.RegisterClassEx的这个函数,显然时来自于win32的微软的类库,通常来说win32时用于关联Windows系统窗体的类库,通常会在.net的GUI框架中会大量使用,例如WPF和WinForm中。所以我们先定位报错的函数
[DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassEx", SetLastError = true)]
[SecurityCritical]
[SuppressUnmanagedCodeSecurity]
internal static extern ushort IntRegisterClassEx(NativeMethods.WNDCLASSEX_D wc_d);
[SecurityCritical]
internal static ushort RegisterClassEx(NativeMethods.WNDCLASSEX_D wc_d)
{
ushort num = IntRegisterClassEx(wc_d);
if (num == 0)
{
throw new Win32Exception();
}
return num;
}
如上述代码,我们基本定位了我们问题的所在位置,当num的结果为0时,抛出一个win32的错误信息,这个与我们的报错中来自win32的报错一致。所以我们基本可以确认,报错来自于此处的函数。
2.查看报错对应函数的用法
当我们已经根据报错信息定位到此函数后,并且发现函数来源于win32的库,则代表,这个时微软官方的库,所以我们前往微软官方文档网站去查询对应函数的信息。
从微软官方的函数解析文档我们可以发现,这个函数的作用在于根据输入的参数注册一个窗体,并返回一个唯一标识所注册类的类原子。我们前面说过由于num的参数为0,代表注册失败才会报的错误,所以我们需要知道为什么我们创建一个唯一标识符的类原子会出现失败的情况。此时我们发现文档中著名函数的返回值是一个ATOM的参数,与代码中的ushort的返回值明显不一样,所以ATOM可能代表一个微软的自定义的类型,所以我们使用win32 ATOM的关键字去搜索
3.查看为什么会出现报错
此时我们在Bing上找到了微软关于ATOM的解释。
如官方文档所示,ATOM是一个由系统定义的表,他里面存储的是多个字符串的信息。
其中我们通过官方文档对ATOM的描述我们可以知道,当我们的原子表没有更多的空间时,并且传入的字符串不在所属空间时,则会调用失败。并且官方文档中还特别声明了ATOM的创建通常来自于RegisterClass的函数,与我们报错信息的函数一致。所以我们综上所述,此次报错时由于我们在注册窗体时,需要对ATOM的表创建一个新的唯一标识符节点,但是表被占用满了,所以创建失败,抛出Win32的错误到外部。但是一直不能被捕获,直到最外层被UI线程捕获后,但是没有进行处理,造成软件崩溃。
解决报错
1.尝试寻找可以捕获的地方
由于是来自微软库的报错,通常这种报错的原因非常难定位,所以我们通常会在报错的堆栈调用中进行try catch进行对应报错捕获,在根据报错信息去查询我们可能引起此问题的语句再哪里。其次使用创建转储文件的形式放在windbg中去查看报错调用的堆栈信息,检测是否还有弹窗没有显示的有用信息。
但是非常可惜的是,由于设备在客户现场,并且在公司的设备无法短时间内复现报错,并且公司要求软件问题需要在3天内解决。所以我们不能使用windbg去抓取dump来分析堆栈。其次由于此次报错是来自于win32的报错,在WPF中,由非常多库直接或间接使用了win32的库,在经历过数个小时的定位决定放弃捕获的方式。
2.监控ATOM表
由于时间和报错库的原因,所以我们只能通过去监控ATOM表的节点几时增加的形式去逐步排查我们的代码。
此时我在GitHub找到了一个专门用来监控ATOM表的软件。ATOM Table Monitor。
https://github.com/JordiCorbilla/atom-table-monitor
在这里时我们就需要调低监控刷新频率,然后让软件处于运行状态观察,在上面状态下运行时可能会增减ATOM的节点。
然后梳理软件的运行逻辑。例如在我的一个上位机检测软件中,分几大模块,初始化数据,拍照,运算,数据处理。然后再模块中再去深度的梳理我们可能发现的问题。尤为注意的一点,代码看着没有问题不代表实际没有问题。尤其是软件中逻辑复杂,涉及到大量多线程的状态下,及其有可能是某一个库会经过复杂的转换从而去重复调用一些内容。
3.寻找代码异常的地方
此时我们对软件的大模块分析可以基本确认问题发生在哪个区域,例如假设我的问题发生在数据计算的大模块中,那么我需要将模块的代码进行逐步的拆分,然后检测是哪一个部分的问题。
例如,假设我下述代码中不知道哪一个函数由有问题,但是全部运行时就会出现问题,所以我们可以使用我们在数据结构最常见的二分查找法。由于代码都是自上而下顺序执行的,并且代码数量固定,所以使用二分查找法可以极大的减少时间花费。
//例如在下述代码中,我只执行方法ABC观察是否会复现问题
//假设未复现时,我们可以确定时方法DEF出现问题,此时我们需要对方法DEF再次进行二分查找
static void Main(string[] args)
{
MethonA();
MethonB();
MethonC();
return;
MethonD();
MethonE();
MethonF();
}
当我们执行到需要有返回结果的函数时,我们可以给他一个合理的自定义结果,使得软件可以持续的运行下去。通常情况下,我们都是选取首次运行结果,并将其保持的全局变量中,然后将其赋值到新的对象中,以确保软件不会因为数值问题出现影响
4.成功锁定代码异常位置
public double GetArea()
{
double area = 0;
switch (ShapeType)
{
case ShapeTypes.Rectangle:
area = RoiSize.Width * RoiSize.Height;
break;
case ShapeTypes.Segment:
case ShapeTypes.Polygon:
case ShapeTypes.Path:
#region 此处是问题所在位置
var geometry = System.Windows.Media.Geometry.Parse(PathData);
#endregion
area = geometry.GetArea();
break;
case ShapeTypes.Circle:
case ShapeTypes.Ellipse:
area = Math.PI * (RoiSize.Width / 2) * (RoiSize.Height / 2);
break;
}
return area;
}
此函数的意思是,当我们得到一个来自于数据中的几何图形,然后获取其的面积的方法。当几何图像的类型为Path(即是来自于WPF的<Path.Data>的数据)时,将数据转换为Geometry的图形对象,并且获取图形对象的面积。
但是我在仔细审查其中代码时我发现,这个对象关联的类和方法并没有使用HwndWrapper的对象,但是从我们上述报错中,我们发现创建ATOM的节点是从这个类中进入的。后面我继续查找了大概5个小时的时间仍然没有任何收获。并且我发现在其他地方使用此函数却不会造成ATOM节点增加。但是由于时间的原因。所以我决定通过传入的Path.Data的数据源自己重写一个获取对应数据几何面积的方法。
5.重写对应的方法
我们可以通过查看他的函数的执行流程。
此时我们通过他的方法函数,我们可以发现,他是将输入的字符串进行解析,然后根据每个字母后面代表的点数据来绘制一个几何图形。但是由于获取面积的函数无法被反编译,所以我们只能根据他解析的字符串对应的数据进行重写方法
internal void ParseToGeometryContext(StreamGeometryContext context, string pathString, int startIndex)
{
_formatProvider = CultureInfo.InvariantCulture;
_context = context;
_pathString = pathString;
_pathLength = pathString.Length;
_curIndex = startIndex;
_secondLastPoint = new Point(0.0, 0.0);
_lastPoint = new Point(0.0, 0.0);
_lastStart = new Point(0.0, 0.0);
_figureStarted = false;
bool flag = true;
char c = ' ';
while (ReadToken())
{
char token = _token;
if (flag)
{