sql >> Databáze >  >> RDS >> Database

Řešení úniku prostředků GDI

Netěsnost GDI (nebo jednoduše použití příliš mnoha objektů GDI) je jedním z nejběžnějších problémů. To nakonec způsobí problémy s vykreslováním, chyby a/nebo problémy s výkonem. Článek popisuje, jak tento problém ladíme.

V roce 2016, kdy se většina programů spouští v sandboxech, z nichž ani ten nejneschopnější vývojář nemůže poškodit systém, jsem ohromen, když čelím problému, o kterém budu hovořit v tomto článku. Upřímně řečeno, doufal jsem, že tento problém zmizel navždy společně s Win32Api. Přesto jsem tomu čelil. Předtím jsem o tom jen slyšel hororové příběhy od starších zkušenějších vývojářů.

Problém

Únik nebo použití obrovského množství objektů GDI.

Příznaky

  1. Sloupec objektů GDI na kartě Podrobnosti ve Správci úloh zobrazuje kritických 10 000 (pokud tento sloupec chybí, můžete jej přidat kliknutím pravým tlačítkem na záhlaví tabulky a výběrem Vybrat sloupce).
  2. Při vývoji v C# nebo v jiných jazycích, které jsou spouštěny pomocí CLR, dojde k následující špatně informativní chybě:
    Zpráva:V GDI+ došlo k obecné chybě.
    Zdroj:System.Drawing
    Cílový web:IntPtr GetHbitmap(System.Drawing.Color)
    Typ:System.Runtime.InteropServices.ExternalException
    K chybě nemusí dojít u určitých nastavení nebo v určitých verzích systému, ale vaše aplikace nebude schopna vykreslit jeden objekt:
  3. Během vývoje v С/С++ začaly všechny metody GDI, jako Create%SOME_GDI_OBJECT%, vracet hodnotu NULL.

Proč?

Systémy Windows neumožňují vytvořit více než 65535 objekty GDI. Toto číslo je ve skutečnosti působivé a jen stěží si dokážu představit normální scénář vyžadující tak obrovské množství objektů. Existuje omezení pro procesy – 10 000 na proces, který lze upravit (změnou HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota hodnotu v rozsahu 256 až 65535), ale společnost Microsoft nedoporučuje toto omezení zvyšovat. Pokud to přesto uděláte, jeden proces bude schopen zmrazit systém, takže nebude schopen vykreslit ani chybovou zprávu. V tomto případě lze systém oživit až po restartu.

Jak to opravit?

Pokud žijete v pohodlném a spravovaném světě CLR, je velká šance, že ve vaší aplikaci dochází k obvyklému úniku paměti. Problém je to nepříjemný, ale je to docela běžný případ. Existuje minimálně tucet skvělých nástrojů, jak to odhalit. Budete muset použít libovolný profiler, abyste viděli, zda se zvyšuje počet objektů, které zabalují prostředky GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics). Pokud ano, můžete přestat číst tento článek. Pokud nebyl detekován únik objektů wrapper, váš kód používá přímo GDI API a existuje scénář, kdy nebudou odstraněny

Co doporučují ostatní?

Oficiální pokyny společnosti Microsoft nebo jiné články na toto téma vám doporučí něco takového:

Najít vše Vytvořit %SOME_GDI_OBJECT% a zjistit, zda odpovídající DeleteObject (nebo ReleaseDC pro objekty HDC) existuje. Pokud takový DeleteObject existuje, může existovat scénář, který to nevolá.

Existuje mírně vylepšená verze této metody, která obsahuje další krok:

Stáhněte si nástroj GDIView. Může zobrazit přesný počet objektů GDI podle typu. Všimněte si, že celkový počet objektů neodpovídá hodnotě v posledním sloupci. Ale můžeme nad tím zavřít oči, pokud to pomůže zúžit pole hledání.

Projekt, na kterém pracuji, má kódovou základnu 9 milionů záznamů, přibližně stejné množství záznamů se nachází v knihovnách třetích stran, stovky volání funkce GDI, které jsou rozmístěny v desítkách souborů. Ztratil jsem spoustu času a energie, než jsem pochopil, že manuální analýza bez chyb je nemožná.

Co mohu nabídnout?

Pokud se vám tato metoda zdá příliš dlouhá a únavná, s tou předchozí jste neprošli všemi fázemi zoufalství. Můžete zkusit postupovat podle předchozích kroků, ale pokud to nepomůže, nezapomeňte na toto řešení.

Při honbě za únikem jsem si položil otázku:Kde se vytvořily unikající objekty? Nebylo možné nastavit body přerušení na všech místech, kde se volá funkce API. Kromě toho jsem si nebyl jistý, že se to neděje v .NET Framework nebo v některé z knihoven třetích stran, které používáme. Několik minut googlování mě přivedlo k nástroji API Monitor, který umožňoval protokolovat a sledovat volání všech systémových funkcí. Snadno jsem našel seznam všech funkcí, které generují GDI objekty, našel a vybral je v API Monitoru. Poté nastavím body přerušení.

Poté jsem spustil proces ladění v Visual Studio a vybrali jej ve stromu procesů. Pátý zarážkový bod fungoval okamžitě:

Uvědomil jsem si, že se v tomto torrentu utopím a že potřebuji něco jiného. Odstranil jsem body přerušení z funkcí a rozhodl jsem se zobrazit protokol. Ukázal tisíce hovorů. Bylo jasné, že je nebudu moci analyzovat ručně.

Úkolem je Nalézt volání funkcí GDI, která nezpůsobují odstranění . Protokol obsahoval vše, co jsem potřeboval:seznam volání funkcí v chronologickém pořadí, jejich vrácené hodnoty a parametry. Proto jsem potřeboval získat vrácenou hodnotu funkce Create%SOME_GDI_OBJECT% a najít volání DeleteObject s touto hodnotou jako argumentem. Vybral jsem všechny záznamy v API Monitoru, vložil je do textového souboru a dostal něco jako CSV s oddělovačem TAB. Spustil jsem VS, kde jsem zamýšlel napsat malý program pro analýzu, ale než se mohl načíst, přišel jsem na lepší nápad:exportovat data do databáze a napsat dotaz, abych našel, co potřebuji. Byla to správná volba, protože mi umožnila rychle klást otázky a získávat odpovědi.

Existuje mnoho nástrojů pro import dat z CSV do databáze, takže se tímto tématem nebudu zdržovat (mysql, mssql, sqlite).

Mám následující tabulku:

CREATE TABLE apicalls (
id int(11) DEFAULT NULL,
`Time of Day` datetime DEFAULT NULL,
Thread int(11) DEFAULT NULL,
Module varchar(50) DEFAULT NULL,
API varchar(200) DEFAULT NULL,
`Return Value` varchar(50) DEFAULT NULL,
Error varchar(100) DEFAULT NULL,
Duration varchar(50) DEFAULT NULL
)

Napsal jsem následující funkci MySQL, abych získal deskriptor odstraněného objektu z volání API:

CREATE FUNCTION getHandle(api varchar(1000))
RETURNS varchar(100) CHARSET utf8
BEGIN
DECLARE start int(11);
DECLARE result varchar(100);
SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )'
IF start = 0 THEN
SET start := INSTR(api, '(');
END IF;
SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1);
RETURN TRIM(result);
END

A nakonec jsem napsal dotaz na umístění všech aktuálních objektů:

SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi
FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates
LEFT JOIN (SELECT
d.id,
d.API,
getHandle(d.API) handle
FROM apicalls d
WHERE API LIKE 'DeleteObject%'
OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels
ON dels.handle = creates.handle
WHERE creates.API LIKE 'Create%';

(V podstatě jednoduše najde všechny hovory Delete pro všechny hovory Create).

Jak vidíte na obrázku výše, všechny hovory bez jediného Delete byly nalezeny najednou.

Takže zbývá poslední otázka:Jak zjistit, odkud jsou tyto metody volány v kontextu mého kódu? A tady mi pomohl jeden skvělý trik:

  1. Spusťte aplikaci ve VS pro ladění
  2. Najděte jej v Api Monitor a vyberte jej.
  3. Vyberte požadovanou funkci v rozhraní API a umístěte bod přerušení.
  4. Klikejte na tlačítko „Další“, dokud nebude voláno s příslušnými parametry (opravdu mi chyběly podmíněné zarážky z VS)
  5. Když přijdete na požadovaný hovor, přepněte na CS a klikněte na Přerušit vše .
  6. VS Debugger bude zastaven přímo tam, kde je vytvořen unikající objekt, a vše, co musíte udělat, je zjistit, proč nebyl odstraněn.

Poznámka:Kód je napsán pro ilustrační účely.

Shrnutí:

Popsaný algoritmus je komplikovaný a vyžaduje mnoho nástrojů, ale výsledek poskytl mnohem rychleji ve srovnání s hloupým prohledáváním obrovské kódové základny.

Zde je shrnutí všech kroků:

  1. Hledejte úniky paměti objektů GDI wrapper.
  2. Pokud existují, odstraňte je a opakujte krok 1.
  3. Pokud nedochází k únikům, hledejte volání funkcí API explicitně.
  4. Pokud jejich množství není velké, vyhledejte skript, kde objekt není odstraněn.
  5. Pokud je jejich množství velké nebo je lze jen stěží dohledat, stáhněte si API Monitor a nastavte jej pro protokolování volání funkcí GDI.
  6. Spusťte aplikaci pro ladění ve VS.
  7. Zopakujte únik (bude inicializován program, aby se skryly proplacené objekty).
  8. Připojte se k API Monitor.
  9. Zopakujte únik.
  10. Zkopírujte protokol do textového souboru, importujte jej do jakékoli databáze, kterou máte po ruce (skripty uvedené v tomto článku jsou pro MySQL, ale lze je snadno použít pro jakýkoli systém správy relačních databází).
  11. Porovnejte metody Create a Delete (skript SQL najdete v tomto článku výše) a najděte metody bez volání Delete.
  12. Nastavte bod přerušení v API Monitor pro volání požadované metody.
  13. Klikejte na Pokračovat, dokud nebude metoda volána se znovu získanými parametry.
  14. Po zavolání metody s požadovanými parametry klikněte na Break All in VS.
  15. Zjistěte, proč tento objekt není smazán.

Doufám, že tento článek bude užitečný a pomůže vám ušetřit čas.


  1. Jak chránit databázi MySQL nebo MariaDB před SQL Injection:Část druhá

  2. Jak zobrazit největší výnos z databáze Microsoft Access

  3. ROUND(číslo) Funkce v Oracle

  4. Proč pořadí řazení varchar společnosti Oracle neodpovídá chování srovnání varchar?