Sdílet prostřednictvím


Osvědčené postupy pro knihovnu Dynamic-Link

**Aktualizovaný:**

  • 17. května 2006

důležitá rozhraní API

Vytváření knihoven DLL představuje řadu výzev pro vývojáře. Knihovny DLL nemají systémově vynucenou správu verzí. Pokud v systému existuje více verzí knihovny DLL, snadné přepsání s nedostatkem schématu správy verzí vytváří závislosti a konflikty rozhraní API. Složitost vývojového prostředí, implementace zavaděče a závislostí knihovny DLL vytvořily nestabilitu v pořadí načítání a chování aplikace. A konečně, mnoho aplikací spoléhá na knihovny DLL a má komplexní sady závislostí, které musí být dodrženy, aby aplikace fungovaly správně. Tento dokument obsahuje pokyny pro vývojáře knihoven DLL, které pomáhají vytvářet robustnější, přenosné a rozšiřitelné knihovny DLL.

Nesprávná synchronizace v DllMain může způsobit zablokování aplikace nebo přístup k datům nebo kódu v neinicializované knihovně DLL. Volání určitých funkcí z DllMain způsobuje takové problémy.

, co se stane, když se knihovna načte

Obecné osvědčené postupy

DllMain je volána, zatímco je držena zámkem zavaděče. Proto jsou na funkce, které lze volat v DllMain, uvalena významná omezení. DllMain je navržen tak, aby prováděl minimální inicializační úlohy pomocí malé podmnožiny rozhraní API systému Microsoft® Windows®. V DllMain nelze volat žádnou funkci, která se pokusí přímo nebo nepřímo získat zámek zavaděče. V opačném případě hrozí nebezpečí, že se vaše aplikace zablokuje nebo selže. Chyba v DllMain implementace může ohrozit celý proces a všechny jeho vlákna.

Ideální DllMain by byl jen prázdný zástupný kód. Vzhledem ke složitosti mnoha aplikací je to obecně příliš omezující. Dobrým pravidlem pro DllMain je odložit co nejvíce inicializace. Opožděná inicializace zvyšuje odolnost aplikace, protože tato inicializace se neprovádí, když je držen zámek zavaděče. Líná inicializace vám také umožňuje bezpečně používat ještě více rozhraní API Windows.

Některé úlohy inicializace nelze odložit. Například knihovna DLL, která závisí na konfiguračním souboru, by se neměla načíst, pokud je soubor poškozený nebo obsahuje nesmysly. U tohoto typu inicializace by se knihovna DLL měla pokusit o akci a rychle selhat místo plýtvání zdroji dokončením jiné práce.

Nikdy byste neměli provádět následující úlohy z dllMain:

  • Volání LoadLibrary nebo LoadLibraryEx (buď přímo nebo nepřímo). To může způsobit zablokování nebo pád systému.
  • Volání GetStringTypeA, GetStringTypeEx, nebo GetStringTypeW (přímo nebo nepřímo). To může způsobit zablokování nebo zhroucení.
  • Synchronizujte s jinými vlákny. To může způsobit zablokování.
  • Získejte objekt synchronizace vlastněný kódem, který čeká na získání zámku zavaděče. To může způsobit zablokování.
  • Inicializujte vlákna COM pomocí CoInitializeEx. Za určitých podmínek může tato funkce volat LoadLibraryEx.
  • Volejte funkce registru.
  • Volání CreateProcess. Vytvoření procesu může načíst jinou knihovnu DLL.
  • Volání ExitThread. Ukončení vlákna během odpojení knihovny DLL může způsobit opětovnou aktivaci zámku zavaděče, což může vést k zablokování nebo pádu.
  • Volání CreateThread. Vytvoření vlákna může fungovat, pokud se nesynchronizujete s jinými vlákny, ale je to rizikové.
  • Volání ShGetFolderPathW. Volání rozhraní API pro shell nebo známé složky může vést k synchronizaci vláken, což může způsobit zablokování.
  • Vytvoření pojmenovaného kanálu nebo jiného pojmenovaného objektu (pouze Windows 2000) V systému Windows 2000 jsou pojmenované objekty poskytovány knihovnou DLL terminálové služby. Pokud tato knihovna DLL není inicializována, volání knihovny DLL může způsobit chybové ukončení procesu.
  • Použijte funkci správy paměti z C Run-Time dynamického (CRT). Pokud není knihovna DLL CRT inicializována, může volání těchto funkcí způsobit chybové ukončení procesu.
  • Volání funkcí v User32.dll nebo Gdi32.dll. Některé funkce načítají jinou knihovnu DLL, která nemusí být inicializována.
  • Použijte spravovaný kód.

Následující úlohy jsou bezpečné provádět v DllMain:

  • Inicializace statických datových struktur a členů v době kompilace
  • Vytvoření a inicializace synchronizačních objektů
  • Přidělení paměti a inicializace dynamických datových struktur (zabránění výše uvedeným funkcím)
  • Nastavení místního úložiště vlákna (TLS)
  • Otevření, čtení z a zápis do souborů.
  • Volání funkcí v Kernel32.dll (s výjimkou funkcí uvedených výše)
  • Nastavte globální ukazatele na hodnotu NULL a odložte inicializaci dynamických členů. V systému Microsoft Windows Vista™ můžete pomocí jednorázových inicializačních funkcí zajistit, aby se blok kódu spustil pouze jednou v prostředí s více vlákny.

Zablokování způsobené inverzí pořadí uzamčení

Při implementaci kódu, který používá více synchronizačních objektů, jako jsou zámky, je důležité respektovat pořadí uzamčení. Pokud je nutné získat více než jeden zámek najednou, musíte definovat explicitní prioritu, která se nazývá hierarchie zámků nebo pořadí zámků. Pokud je například zámek A získán před zámkem B někde v kódu a zámek B se získá před zámkem C jinde v kódu, pak pořadí uzamčení je A, B, C a toto pořadí by se mělo dodržovat v celém kódu. Inverze pořadí uzamčení nastane v případě, že není dodrženo pořadí uzamčení – například pokud je zámek B získán před uzamčením A. Inverze pořadí uzamčení může způsobit zablokování, které je obtížné ladit. Aby se těmto problémům zabránilo, musí všechna vlákna získat zámky ve stejném pořadí.

Je důležité poznamenat, že zavaděč volá DllMain s již získaným zámkem zavaděče, takže zámek zavaděče by měl mít nejvyšší prioritu v hierarchii uzamčení. Všimněte si také, že kód musí získat pouze zámky, které vyžaduje pro správnou synchronizaci; nemusí získat každý jeden zámek definovaný v hierarchii. Pokud například část kódu vyžaduje pouze zámky A a C pro správnou synchronizaci, měl by kód získat zámek A předtím, než získá zámek C; není nutné, aby kód získal také zámek B. Kód knihovny DLL navíc nemůže explicitně získat zámek zavaděče. Pokud kód musí volat rozhraní API, jako je například GetModuleFileName, které může nepřímo získat zámek zavaděče, a zároveň musí získat i privátní zámek, měl by nejprve zavolat GetModuleFileName, a teprve potom získat zámek P, aby bylo dodrženo pořadí načítání.

Obrázek 2 je příklad znázorňující inverzi pořadí uzamčení. Zvažte knihovnu DLL, jejíž hlavní vlákno obsahuje dllMain. Zavaděč knihovny získá zámek zavaděče L a potom zavolá do DllMain. Hlavní vlákno vytvoří synchronizační objekty A, B a G pro serializaci přístupu ke svým datovým strukturám a pak se pokusí získat zámek G. Pracovní vlákno, které již úspěšně získal zámek G, pak volá funkci, jako je GetModuleHandle, která se pokusí získat zámek zavaděče L. Pracovní vlákno je proto blokováno na L a hlavní vlákno je blokováno na G, což vede k zablokování.

slepá ulička způsobená inverzí pořadí zámků

Aby se zabránilo zablokování způsobenému inverzí pořadí zámků, musí se všechna vlákna vždy pokoušet získat synchronizační objekty v definovaném pořadí načítání.

Osvědčené postupy pro synchronizaci

V rámci inicializace zvažte knihovnu DLL, která vytváří pracovní vlákna. Při vyčištění knihovny DLL je nutné se synchronizovat se všemi pracovními vlákny, aby se zajistilo, že datové struktury jsou v konzistentním stavu, a poté ukončit pracovní vlákna. Dnes neexistuje žádný jednoduchý způsob, jak zcela vyřešit problém čisté synchronizace a vypnutí knihoven DLL v prostředí s více vlákny. Tato část popisuje aktuální osvědčené postupy pro synchronizaci vláken během vypnutí knihovny DLL.

Synchronizace vláken v DllMain během ukončení procesu

  • V době, kdy se dllMain volá při ukončení procesu, všechny vlákna procesu byly vynuceně vyčištěny a existuje šance, že adresní prostor je nekonzistentní. Synchronizace není v tomto případě nutná. Jinými slovy, ideální obslužná rutina DLL_PROCESS_DETACH je prázdná.
  • Systém Windows Vista zajišťuje, aby základní datové struktury (proměnné prostředí, aktuální adresář, halda procesu atd.) byly v konzistentním stavu. Jiné datové struktury ale můžou být poškozené, takže čištění paměti není bezpečné.
  • Trvalý stav, který je potřeba uložit, musí být vyprázdněný do trvalého úložiště.

Synchronizace vláken v DllMain pro DLL_THREAD_DETACH během uvolnění knihovny DLL

  • Při uvolnění knihovny DLL se adresní prostor nevyhodí. Proto se očekává, že knihovna DLL provede čisté vypnutí. To zahrnuje synchronizaci vláken, otevřené popisovače, trvalý stav a přidělené prostředky.
  • Synchronizace vláken je složitá, protože čekání na ukončení vláken v DllMain může způsobit zablokování. Knihovna DLL A například drží zámek zavaděče. Signalizuje výstup vlákna T a čeká na ukončení vlákna. Vlákno T se ukončí a zavaděč se pokusí získat zámek zavaděče, aby mohl volat funkci DllMain knihovny DLL A s parametrem DLL_THREAD_DETACH. To způsobí zablokování. Minimalizace rizika zablokování:
    • Knihovna DLL A obdrží zprávu DLL_THREAD_DETACH ve svém DllMain a nastaví událost pro vlákno T, signalizující mu, aby se ukončilo.
    • Vlákno T dokončí svou aktuální úlohu, uvede se do konzistentního stavu, signalizuje knihovnu DLL A a čeká bez omezení. Mějte na paměti, že rutiny kontroly konzistence by měly dodržovat stejná omezení jako DllMain, aby se zabránilo zablokování.
    • Knihovna DLL A ukončí T s vědomím, že je v konzistentním stavu.

Pokud je knihovna DLL uvolněna po vytvoření všech jejích vláken, ale před jejich zahájením provádění, mohou se vlákna zhroutit. Pokud knihovna DLL vytvořila vlákna v DllMain v rámci inicializace, některá vlákna pravděpodobně nedokončila inicializaci a jejich DLL_THREAD_ATTACH zpráva stále čeká na doručení do knihovny DLL. V takovém případě, pokud je knihovna DLL uvolněna, začne ukončovat vlákna. Některá vlákna však můžou být zablokovaná za zavaděčovým zámkem. Jejich DLL_THREAD_ATTACH zprávy se zpracovávají po zrušení mapování knihovny DLL, což způsobuje chybové ukončení procesu.

Doporučení

Následující doporučené pokyny:

  • Pomocí nástroje Application Verifier zachyťte nejběžnější chyby v DllMain.
  • Pokud používáte privátní zámek uvnitř DllMain, definujte hierarchii uzamčení a používejte ji konzistentně. Zámek nakladače musí být na dně této hierarchie.
  • Ověřte, že žádná volání nezávisí na jiné knihovně DLL, která ještě nebyla plně načtena.
  • Provádět jednoduchá inicializace staticky v době kompilace, nikoli v DllMain.
  • Odložit všechna volání v DllMain, která mohou počkat do později.
  • Odložit inicializační úlohy, které můžou čekat až později. Některé chybové stavy je nutné včas rozpoznat, aby aplikace mohly řádně zpracovávat chyby. Ale mezi touto ranou detekcí a ztrátou robustnosti, které mohou být výsledkem, existují kompromisy. Odložení inicializace je často nejlepší.