OutOfMemory和GDI +有时根本不是OutOfMemory

在执行工作中的最后一个项目时,我和我的同事面临这样一个事实,即System.Drawing中的某些方法和构造函数在完全普通的地方都来自OutOfMemory,而当仍然有非常非常多的可用内存时。



问题的实质


例如,使用以下C#代码:

using System.Drawing; using System.Drawing.Drawing2D; namespace TempProject { static class Program { static void Main() { var point1 = new PointF(-3.367667E-16f, 0f); var point2 = new PointF(3.367667E-16f, 100f); var brush = new LinearGradientBrush(point1, point2, Color.White, Color.Black); } } } 

当执行最后一行时,无论有多少可用内存,都将确保引发OutOfMemoryException。 此外,如果将0替换为3.367667E-16f和-3.367667E-16f,这非常接近真实情况,那么一切都会正常进行-将会创建填充。 我认为这种行为看起来很奇怪。 让我们看看为什么会发生这种情况以及如何处理。

找出疾病的原因


首先,我们了解LinearGradientBrush构造函数中发生的情况。 为此,您可以查看referencesource.microsoft.com 。 将有以下内容:

 public LinearGradientBrush(PointF point1, PointF point2, Color color1, Color color2) { IntPtr brush = IntPtr.Zero; int status = SafeNativeMethods.Gdip.GdipCreateLineBrush( new GPPOINTF(point1), new GPPOINTF(point2), color1.ToArgb(), color2.ToArgb(), (int)WrapMode.Tile, out brush ); if (status != SafeNativeMethods.Gdip.Ok) throw SafeNativeMethods.Gdip.StatusException(status); SetNativeBrushInternal(brush); } 

很容易注意到,这里最重要的是GDI + GdipCreateLineBrush方法的调用。 因此,您需要观察其中发生的事情。 为此,请使用IDA + HexRays。 下载到IDA gdiplus.dll。 如果需要确定要调试的库版本,则可以使用SysInternals中的Process Explorer。 此外,您可能会遇到gdiplus.dll所在文件夹的权限问题。 通过更改此文件夹的所有者可以解决它们。

因此,在IDA中打开gdiplus.dll。 让我们等待文件被处理。 之后,在菜单中选择:视图→打开子视图→导出以打开从该库导出的所有功能,然后在此处找到GdipCreateLineBrush。

由于字符的加载,HexRays和文档的强大功能,您可以轻松地将方法代码从汇编程序转换为可读的C ++代码:

GdipCreateLineBrush
 GpStatus __userpurge GdipCreateLineBrush@<eax>(int a1@<edi>, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode, GpRectGradient **result) { GpStatus status; // esi MAPDST GpGradientBrush *v8; // eax GpRectGradient *v9; // eax int v12; // [esp+4h] [ebp-Ch] int vColor1; // [esp+8h] [ebp-8h] int vColor2; // [esp+Ch] [ebp-4h] FPUStateSaver::FPUStateSaver(&v12, 1); EnterCriticalSection(&GdiplusStartupCriticalSection::critSec); if ( Globals::LibraryInitRefCount > 0 ) { LeaveCriticalSection(&GdiplusStartupCriticalSection::critSec); if ( result && point1 && point2 && wrapMode != 4 ) { vColor1 = color1; vColor2 = color2; v8 = operator new(a1); status = 0; if ( v8 ) v9 = GpLineGradient::GpLineGradient(v8, point1, point2, &vColor1, &vColor2, wrapMode); else v9 = 0; *result = v9; if ( !CheckValid<GpHatch>(result) ) status = OutOfMemory; } else { status = InvalidParameter; } } else { LeaveCriticalSection(&GdiplusStartupCriticalSection::critSec); status = GdiplusNotInitialized; } __asm { fclex } return status; } 

此方法的代码非常清楚。 其实质在于以下几行:

 if ( result && point1 && point2 && wrapMode != 4 ) { vColor1 = color1; vColor2 = color2; v8 = operator new(a1); status = 0; if ( v8 ) v9 = GpLineGradient::GpLineGradient(v8, point1, point2, &vColor1, &vColor2, wrapMode); else v9 = 0; *result = v9; if ( !CheckValid<GpHatch>(result) ) status = OutOfMemory } else { status = InvalidParameter; } 

GdiPlus检查输入参数是否正确,如果不正确,则返回InvalidParameter。 否则,将创建一个GpLineGradient并检查其有效性。 如果验证失败,则返回OutOfMemory。 显然,这是我们的情况,因此,我们需要弄清楚GpLineGradient构造函数内部发生了什么:

GpLineGradient :: GpLineGradient
 GpRectGradient *__thiscall GpLineGradient::GpLineGradient(GpGradientBrush *this, GpPointF *point1, GpPointF *point2, int color1, int color2, int wrapMode) { GpGradientBrush *v6; // esi float height; // ST2C_4 double v8; // st7 float width; // ST2C_4 float angle; // ST2C_4 GpRectF rect; // [esp+1Ch] [ebp-10h] v6 = this; GpGradientBrush::GpGradientBrush(this); GpRectGradient::DefaultBrush(v6); rect.Height = 0.0; rect.Width = 0.0; rect.Y = 0.0; rect.X = 0.0; *v6 = &GpLineGradient::`vftable; if ( LinearGradientRectFromPoints(point1, point2, &rect) ) { *(v6 + 1) = 1279869254; } else { height = point2->Y - point1->Y; v8 = height; width = point2->X - point1->X; angle = atan2(v8, width) * 180.0 / 3.141592653589793; GpLineGradient::SetLineGradient(v6, point1, point2, &rect, color1, color2, angle, 0, wrapMode); } return v6; } 

在此初始化变量,然后将其填充到LinearGradientRectFromPoints和SetLineGradient中。 我敢于假设rect是基于point1和point2的填充矩形,要看到这一点,可以看一下LinearGradientRectFromPoints:

LinearGradientRectFromPoints
 GpStatus __fastcall LinearGradientRectFromPoints(GpPointF *p1, GpPointF *p2, GpRectF *result) { double vP1X; // st7 float vLeft; // ST1C_4 MAPDST double vP1Y; // st7 float vTop; // ST1C_4 MAPDST float vWidth; // ST18_4 MAPDST double vWidth3; // st7 float vHeight; // ST18_4 MAPDST float vP2X; // [esp+18h] [ebp-8h] float vP2Y; // [esp+1Ch] [ebp-4h] if ( IsClosePointF(p1, p2) ) return InvalidParameter; vP2X = p2->X; vP1X = p1->X; if ( vP2X <= vP1X ) vP1X = vP2X; vLeft = vP1X; result->X = vLeft; vP2Y = p2->Y; vP1Y = p1->Y; if ( vP2Y <= vP1Y ) vP1Y = vP2Y; vTop = vP1Y; result->Y = vTop; vWidth = p1->X - p2->X; vWidth = fabs(vWidth); vWidth3 = vWidth; result->Width = vWidth; vHeight = p1->Y - p2->Y; vHeight = fabs(vHeight); result->Height = vHeight; vWidth = vWidth3; if ( IsCloseReal(p1->X, p2->X) ) { result->X = vLeft - 0.5 * vHeight; result->Width = vHeight; vWidth = vHeight; } if ( IsCloseReal(p1->Y, p2->Y) ) { result->Y = vTop - vWidth * 0.5; result->Height = vWidth; } return 0; } 

如预期的那样,rect是点point1和point2的矩形。

现在让我们回到主要问题,看看SetLineGradient内部发生了什么:

SetLinegradient
 GpStatus __thiscall GpLineGradient::SetLineGradient(DpGradientBrush *this, GpPointF *p1, GpPointF *p2, GpRectF *rect, int color1, int color2, float angle, int zero, int wrapMode) { _DWORD *v10; // edi float *v11; // edi GpStatus v12; // esi _DWORD *v14; // edi this->wrapMode = wrapMode; v10 = &this->dword40; this->Color1 = *color1; this->Color2 = *color2; this->Color11 = *color1; this->Color21 = *color2; this->dwordB0 = 0; this->float98 = 1.0; this->dwordA4 = 1; this->dwordA0 = 1; this->float94 = 1.0; this->dwordAC = 0; if ( CalcLinearGradientXform(zero, rect, angle, &this->gap4[16]) ) { *this->gap4 = 1279869254; *v10 = 0; v14 = v10 + 1; *v14 = 0; ++v14; *v14 = 0; v14[1] = 0; *&this[1].gap4[12] = 0; *&this[1].gap4[16] = 0; *&this[1].gap4[20] = 0; *&this[1].gap4[24] = 0; *&this->gap44[28] = 0; v12 = InvalidParameter; } else { *this->gap4 = 1970422321; *v10 = LODWORD(rect->X); v11 = (v10 + 1); *v11 = rect->Y; ++v11; *v11 = rect->Width; v11[1] = rect->Height; *&this->gap44[28] = zero; v12 = 0; *&this[1].gap4[12] = *p1; *&this[1].gap4[20] = *p2; } return v12; } 

同样在SetLineGradient中,仅发生字段初始化。 因此,我们需要更深入地研究:

 int __fastcall CalcLinearGradientXform(int zero, GpRectF *rect, float angle, int a4) { //... //... //... return GpMatrix::InferAffineMatrix(a4, points, rect) != OK ? InvalidParameter : OK; } 

最后:

 GpStatus __thiscall GpMatrix::InferAffineMatrix(int this, GpPointF *points, GpRectF *rect) { //... double height; // st6 double y; // st5 double width; // st4 double x; // st3 double bottom; // st2 float right; // ST3C_4 float rectArea; // ST3C_4 //... x = rect->X; y = rect->Y; width = rect->Width; height = rect->Height; right = x + width; bottom = height + y; rectArea = bottom * right - x * y - (y * width + x * height); rectArea = fabs(rectArea); if ( rectArea < 0.00000011920929 ) return InvalidParameter; //... } 

InferAffineMatrix方法正是我们所感兴趣的。 在这里检查rect的区域-点的原始矩形,如果小于0.00000011920929,则InferAffineMatrix返回InvalidParameter。 0.00000011920929是浮子的机器epsilon (FLT_EPSILON)。 您可以看到Microsoft对矩形区域的关注程度:

 rectArea = bottom * right - x * y - (y * width + x * height); 

从右下方的区域减去左上方的区域,然后减去矩形上方和矩形左侧的区域。 我为什么不知道为什么要这样做? 我希望有一天我会知道这种秘密方法。

所以我们有:

  • InnerAffineMatrix返回InvalidParameter;
  • CalcLinearGradientXForm将该结果更高;
  • 在SetLineGradient中,执行将遵循if分支,并且该方法还将返回InvalidParameter;
  • GpLineGradient构造函数将丢失有关InvalidParameter的信息,并返回未初始化到最后的GpLineGradient对象-这非常糟糕!
  • GdipCreateLineBrush将在CheckValid(第26行)中检查GpLineGradient对象是否对未完全填充的字段有效,并且自然会返回false。
  • 此后,状态将更改为OutOfMemory,它将在GDI +方法的退出处获取.NET。

事实证明,Microsoft由于某种原因会忽略某些方法的返回状态,因此会做出错误的假设,并使其他程序员对库工作的理解变得复杂。 但是您要做的就是从GpLineGradient构造函数中将状态调高一些,并在GdipCreateLineBrush中的OK中检查返回值,否则返回构造器状态。 然后,对于GDI +用户,库中发生的错误消息看起来更加合乎逻辑。

用零替换非常小的数字的选项,即 使用垂直填充,由于Microsoft在第35至45行的LinearGradientRectFromPoints方法中的出色表现,它可以正常运行:

魔力
 if ( IsCloseReal(p1->X, p2->X) ) { result->X = vLeft - 0.5 * vHeight; result->Width = vHeight; vWidth = vHeight; } if ( IsCloseReal(p1->Y, p2->Y) ) { result->Y = vTop - vWidth * 0.5; result->Height = vWidth; } 

怎么治疗?


如何避免.NET代码崩溃? 最简单,最明显的选择是将点1和点2的矩形面积与FLT_EPSILON进行比较,如果面积较小则不创建渐变。 但是使用此选项,我们将丢失有关渐变的信息,并且将绘制未上漆的区域,这是不好的。 在检查渐变填充的角度时,我看到了一个更可接受的选项,如果事实证明填充接近水平或垂直,则为这些点设置相同的参数:

我在C#中的解决方案
 static LinearGradientBrush CreateBrushSafely(PointF p1, PointF p2) { if(IsShouldNormalizePoints(p1, p2)) { if(!NormalizePoints(ref p1, ref p2)) return null; } var brush = new LinearGradientBrush(p1, p2, Color.White, Color.Black); return brush; } static bool IsShouldNormalizePoints(PointF p1, PointF p2) { float width = Math.Abs(p1.X - p2.X); float height = Math.Abs(p1.Y - p2.Y); return width * height < FLT_EPSILON && !(IsCloseFloat(p1.X, p2.X) || IsCloseFloat(p1.Y, p2.Y)); } static bool IsCloseFloat(float v1, float v2) { var t = v2 == 0.0f ? 1.0f : v2; return Math.Abs((v1 - v2) / t) < FLT_EPSILON; } static bool NormalizePoints(ref PointF p1, ref PointF p2) { const double twoDegrees = 0.03490658503988659153847381536977d; float width = Math.Abs(p1.X - p2.X); float height = Math.Abs(p1.Y - p2.Y); var angle = Math.Atan2(height, width); if (Math.Abs(angle) < twoDegrees) { p1.Y = p2.Y; return true; } if (Math.Abs(angle - Math.PI / 2) < twoDegrees) { p1.X = p2.X; return true; } return false; } 

竞争对手的表现如何?


让我们找出Wine中发生了什么。 为此,请查看Wine源代码 ,第306行:

GdipCreateLineBrush葡萄酒
 /****************************************************************************** * GdipCreateLineBrush [GDIPLUS.@] */ GpStatus WINGDIPAPI GdipCreateLineBrush(GDIPCONST GpPointF* startpoint, GDIPCONST GpPointF* endpoint, ARGB startcolor, ARGB endcolor, GpWrapMode wrap, GpLineGradient **line) { TRACE("(%s, %s, %x, %x, %d, %p)\n", debugstr_pointf(startpoint), debugstr_pointf(endpoint), startcolor, endcolor, wrap, line); if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter; if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory; *line = heap_alloc_zero(sizeof(GpLineGradient)); if(!*line) return OutOfMemory; (*line)->brush.bt = BrushTypeLinearGradient; (*line)->startpoint.X = startpoint->X; (*line)->startpoint.Y = startpoint->Y; (*line)->endpoint.X = endpoint->X; (*line)->endpoint.Y = endpoint->Y; (*line)->startcolor = startcolor; (*line)->endcolor = endcolor; (*line)->wrap = wrap; (*line)->gamma = FALSE; (*line)->rect.X = (startpoint->X < endpoint->X ? startpoint->X: endpoint->X); (*line)->rect.Y = (startpoint->Y < endpoint->Y ? startpoint->Y: endpoint->Y); (*line)->rect.Width = fabs(startpoint->X - endpoint->X); (*line)->rect.Height = fabs(startpoint->Y - endpoint->Y); if ((*line)->rect.Width == 0) { (*line)->rect.X -= (*line)->rect.Height / 2.0f; (*line)->rect.Width = (*line)->rect.Height; } else if ((*line)->rect.Height == 0) { (*line)->rect.Y -= (*line)->rect.Width / 2.0f; (*line)->rect.Height = (*line)->rect.Width; } (*line)->blendcount = 1; (*line)->blendfac = heap_alloc_zero(sizeof(REAL)); (*line)->blendpos = heap_alloc_zero(sizeof(REAL)); if (!(*line)->blendfac || !(*line)->blendpos) { heap_free((*line)->blendfac); heap_free((*line)->blendpos); heap_free(*line); *line = NULL; return OutOfMemory; } (*line)->blendfac[0] = 1.0f; (*line)->blendpos[0] = 1.0f; (*line)->pblendcolor = NULL; (*line)->pblendpos = NULL; (*line)->pblendcount = 0; linegradient_init_transform(*line); TRACE("<-- %p\n", *line); return Ok; } 

这是参数的唯一验证:

 if(!line || !startpoint || !endpoint || wrap == WrapModeClamp) return InvalidParameter; 

为了与Windows兼容,最有可能编写了以下内容:

 if (startpoint->X == endpoint->X && startpoint->Y == endpoint->Y) return OutOfMemory; 

其余的没什么有趣的-内存分配和填充字段。 从源代码可以明显看出,在Wine中创建问题梯度填充应该没有错误。 确实-如果您在Windows上运行以下程序(我在Windows10x64上运行)

测试程序
 #include <Windows.h> #include "stdafx.h" #include <gdiplus.h> #include <iostream> #pragma comment(lib,"gdiplus.lib") void CreateBrush(float x1, float x2) { Gdiplus::LinearGradientBrush linGrBrush( Gdiplus::PointF(x1, -0.5f), Gdiplus::PointF(x2, 10.5f), Gdiplus::Color(255, 0, 0, 0), Gdiplus::Color(255, 255, 255, 255)); const int status = linGrBrush.GetLastStatus(); const char* result; if (status == 3) { result = "OutOfMemory"; } else { result = "Ok"; } std::cout << result << "\n"; } int main() { Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); Gdiplus::Graphics myGraphics(GetDC(0)); CreateBrush(-3.367667E-16f, 3.367667E-16f); CreateBrush(0, 0); return 0; } 

在Windows控制台中将是:
内存不足
好啦
在Ubuntu中使用Wine:
好啦
好啦
事实证明,要么我做错了事,要么Wine在这方面比Windows更合乎逻辑。

结论


我真的希望我不了解某些内容,并且GDI +的行为是合乎逻辑的。 是的,完全不清楚为什么微软会这么做。 我花了很多时间研究他们的其他产品,而且在一个体面的社会中,有些事情还没有通过《代码审查》。

Source: https://habr.com/ru/post/zh-CN412851/


All Articles