OutOfMemory und GDI + manchmal gar nicht OutOfMemory

Published on May 31, 2018

OutOfMemory und GDI + manchmal gar nicht OutOfMemory

    Während des letzten Projekts bei der Arbeit waren mein Kollege und ich mit der Tatsache konfrontiert, dass einige Methoden und Konstruktoren in System.Drawing an ganz gewöhnlichen Orten von OutOfMemory fallen und wenn viel freier Speicher vorhanden ist.



    Das Wesentliche des Problems


    Nehmen Sie zum Beispiel diesen C # -Code:

    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);
            }
        }
    }
    

    Bei der Ausführung der letzten Zeile ist die Ausnahme OutOfMemoryException garantiert, unabhängig davon, wie viel freier Speicher verfügbar ist. Wenn Sie außerdem 3.367667E-16f und -3.367667E-16f durch 0 ersetzen, was sehr nahe an der Wahrheit liegt, wird alles gut funktionieren - die Füllung wird erstellt. Meines Erachtens sieht dieses Verhalten merkwürdig aus. Mal sehen, warum das passiert und wie man damit umgeht.

    Wir ermitteln die Ursachen der Krankheit


    Beginnen wir damit, herauszufinden, was im LinearGradientBrush-Konstruktor passiert. Um dies zu tun, können Sie betrachten referencesource.microsoft.com . Es wird folgendes geben:

    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); 
    }
    

    Es ist leicht einzusehen, dass das Wichtigste dabei ist, die GDI + -Methode von GdipCreateLineBrush aufzurufen. Es ist also notwendig zu beobachten, was in ihm passiert. Hierfür verwenden wir IDA + HexRays. Wir laden gdiplus.dll in IDA. Wenn Sie die zu debuggende Version der Bibliothek ermitteln müssen, können Sie Process Explorer von SysInternals aus verwenden. Darüber hinaus können Probleme mit den Rechten für den Ordner auftreten, in dem sich gdiplus.dll befindet. Sie werden gelöst, indem Sie den Besitzer dieses Ordners ändern.

    Öffnen Sie also gdiplus.dll in IDA. Warten Sie auf die Dateiverarbeitung. Wählen Sie anschließend im Menü: Ansicht → Subviews öffnen → Exportieren, um alle Funktionen zu öffnen, die aus dieser Bibliothek exportiert werden, und suchen Sie dort nach GdipCreateLineBrush.

    Dank Laden der Zeichen, HexRays-Leistung und Dokumentationkönnen Sie den Code der Methode problemlos vom Assembler in den lesbaren Code in C ++ übersetzen:

    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;
    }
    

    Der Code dieser Methode ist absolut klar. Sein Kern liegt in den Zeilen:

    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 prüft, ob die Eingabeparameter gültig sind, und gibt, falls nicht vorhanden, einen InvalidParameter zurück. Andernfalls wird GpLineGradient erstellt und auf Gültigkeit geprüft. Wenn die Validierung fehlschlägt, wird OutOfMemory zurückgegeben. Anscheinend ist dies unser Fall, was bedeutet, dass wir verstehen müssen, was im GpLineGradient-Konstruktor passiert:

    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;
    }
    

    Hier werden die Variablen initialisiert, die dann in LinearGradientRectFromPoints und SetLineGradient gefüllt werden. Ich wage zu vermuten, dass rect ein Füllrechteck ist, das auf Punkt1 und Punkt2 basiert. Um dies sicherzustellen, können Sie sich LinearGradientRectFromPoints anschauen:

    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;
    }
    

    Wie erwartet ist rect ein Rechteck aus Punkten1 und Punkt2.

    Nun zurück zu unserem Hauptproblem und sehen, was in SetLineGradient passiert:

    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;
    }

    In SetLineGradient werden nur Felder initialisiert. Also müssen wir tiefer gehen:

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

    Und endlich:

    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;
      //...
    }
    

    In der InferAffineMatrix-Methode geschieht genau das, was uns interessiert. Hier wird der rechte Bereich geprüft - das ursprüngliche Rechteck der Punkte. Wenn der Wert weniger als 0,00000011920929 beträgt, gibt die InferAffineMatrix einen InvalidParameter zurück. 0.00000011920929 ist eine Maschine für Float (FLT_EPSILON). Sie können sehen, wie interessant Microsoft den Bereich eines Rechtecks ​​betrachtet:

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

    Von der Fläche in die rechte untere Ecke ziehen Sie die Fläche nach oben links ab und dann die Fläche oberhalb des Rechtecks ​​und links vom Rechteck. Warum dies geschieht, verstehe ich nicht; Ich hoffe, ich werde eines Tages diese geheime Methode kennenlernen.

    Was haben wir also:

    • InnerAffineMatrix gibt einen InvalidParameter zurück.
    • CalcLinearGradientXForm wirft dieses Ergebnis höher aus;
    • In SetLineGradient folgt die Ausführung dem Zweig if, und die Methode gibt außerdem einen InvalidParameter zurück.
    • Der GpLineGradient-Konstruktor verliert die Informationen zum InvalidParameter und gibt das nicht initialisierte GpLineGradient-Objekt an das Ende zurück. Dies ist sehr schlecht!
    • GdipCreateLineBrush überprüft in CheckValid (Zeile 26) das GpLineGradient-Objekt mit leeren Feldern bis zum Ende und gibt regelmäßig false zurück.
    • Danach ändert sich der Status in OutOfMemory, wodurch bei der Ausgabe der GDI + -Methode .NET angezeigt wird.

    Es stellt sich heraus, dass Microsoft aus irgendeinem Grund den Rückgabestatus einiger Methoden ignoriert, aufgrund dieser falschen Annahmen macht und das Verständnis der Arbeit der Bibliothek für andere Programmierer komplizierter macht. Aber es war immerhin notwendig, den Status höher vom GpLineGradient-Konstruktor weiterzuleiten und in GdipCreateLineBrush den Rückgabewert auf OK zu überprüfen und den Konstruktorstatus anderweitig zurückzugeben. Für GDI + -Benutzer würde eine Fehlermeldung, die in der Bibliothek aufgetreten ist, logischer aussehen.

    Die Möglichkeit, sehr kleine Zahlen durch Null zu ersetzen, d.h. Vertikal gefüllt, wird aufgrund der Magie, die Microsoft in der LinearGradientRectFromPoints-Methode in Zeilen 35 bis 45 ausführt, fehlerfrei ausgeführt:

    Magie
    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;
    }
    

    Wie behandeln?


    Wie kann ich diesen Absturz im .NET-Code vermeiden? Die einfachste und naheliegendste Option besteht darin, die Fläche des Rechtecks ​​der Punkte1 und Punkt2 mit FLT_EPSILON zu vergleichen und keinen Farbverlauf zu erstellen, wenn die Fläche kleiner ist. Bei dieser Option verlieren wir jedoch die Informationen über den Gradienten, und es wird ein nicht ausgefüllter Bereich gezeichnet, was nicht gut ist. Ich sehe eine akzeptablere Option, wenn der Winkel der Farbverlaufsfüllung markiert ist und wenn sich herausstellt, dass die Füllung nahezu horizontal oder vertikal ist, dann setzen wir dieselben Parameter für die Punkte:

    Meine C # -Lösung
    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;
    }
    

    Und wie geht es den Konkurrenten?


    Lassen Sie uns herausfinden, was in Wine passiert. Schauen Sie sich dazu den Quellcode von Wine , Zeile 306 an:

    Wines 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;
    }
    

    Hier ist die einzige Validierungsprüfung von Parametern:

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

    Wahrscheinlich wurde aus Gründen der Kompatibilität mit Windows Folgendes geschrieben:

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

    Und der Rest ist nichts Interessantes - die Zuordnung von Speicher und das Ausfüllen der Felder. Aus dem Quellcode wird offensichtlich, dass in Wine die Erstellung einer problematischen Farbverlaufsfüllung fehlerfrei erfolgen sollte. Und wirklich - wenn Sie das folgende Programm unter Windows ausführen (ich lief unter Windows10x64)

    Programm testen
    #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;
    }

    Das in der Windows-Konsole wird sein:
    OutOfMemory
    Ok
    und in Ubuntu mit Wein:
    Ok
    ok
    Es stellt sich heraus, dass ich entweder etwas falsch mache oder dass Wine in dieser Angelegenheit logischer ist als Windows.

    Fazit


    Ich hoffe wirklich, dass ich etwas nicht verstanden habe und das Verhalten von GDI + logisch ist. Es ist nicht ganz klar, warum Microsoft genau das getan hat. Ich habe viel in ihre anderen Produkte gegraben, und es gibt auch Dinge, die in einer anständigen Gesellschaft die Code Review nicht bestanden hätten.