WINDOWS 2K Dll 載入過程

來源:互聯網
上載者:User

WINDOWS 2K Dll 載入過程
jefong by 2005/03/30
這片文章是我在閱讀完MSJ September 1999 Under the Hood後的總結。
在windows中exe可執行程式運行時都會調用一些DLL,例如KERNEL32.DLL和USER32.DLL等系統的dll。但是dll是怎麼被載入的呢?通常,大家都知道在編寫dll時會有一個DLLMain的入口函數,但是實際上這個函數並不是調用dll時最先的工作。首先dll需要被載入,然後要進行初始化分配,再之後才進入DLLMain。還有可能你的一個dll中還會調用另一各dll。那麼dll到底是怎樣載入和初始化的呢,我們來參考一下Platform SDK中的“Dynamic-Link Library Entry-Point Function”。
你的函數正在執行一個初始化任務,例如設定TLS,建立同步對象或開啟一個檔案。那麼你在函數中一定不要調用LoadLibrary函數,因為dll載入命令會建立一個依賴迴圈。這點會導致在系統執行dll的初始化代碼前就已經調用了dll的函數。例如,你不能在入口函數中調用FreeLibrary函數,因為這樣會使系統在已經結束了dll後還調用dll中的操作,引起嚴重錯誤。
初始化任務時調用Win32函數也會引起錯誤,例如調用User,Shell和COM函數可能會引起儲存無效的錯誤,因為dll中一些函數會調用LoadLibrary來載入別的系統組件。
  當你在你的DllMain函數中讀一個註冊表索引值,這樣做會被限制,因為在正常情況下ADVAPI32.DLL在你執行DllMain代碼時還沒被初始化,所以你調用的讀註冊表的函數會失敗。
  在文檔中初始化部分使用LoadLibrary函數是嚴格限制的,但是存在特殊的情況,在WindowsNT中USER32.DLL是忽略上面的限制的。這樣一來好像與上面所說的相背了,在USER32.DLL的初始化部分出現了調用LoadLibrary載入dll的部分,但是沒有出現問題。這是因為AppInit_Dlls的原因,AppInit_Dlls可以為任一個進程調用一個dll列表。所以,如果你的USER32.dll調用出現問題,那一定是AppInit_Dlls沒有工作。
  接下來,我們來看看dll的載入和初始化是怎樣完成的。作業系統有一個載入器,載入一個模組通常有兩個步驟:1.把exe或dll映象到記憶體中,這時,載入器會檢查模組的匯入地址表(IAT),看模組是否依賴於附加的dll。如果dll還沒有被載入到進程中,那麼載入器就把dll映象到記憶體。直到所有的未載入的模組都被映象到記憶體。2.初始化所有的dll。在windows NT中,系統調用exe和dll入口函數的程式會先調用LdrpRunInitializeRoutines函數,也就是說當你調用LoadLibrary時會調用LdrpRunInitializeRoutines,當調用LdrpRunInitializeRoutines時會首先檢查已經映射到記憶體的dll是否已經被初始化。我們來看下面的代碼(Matt的LdrpRunInitializeRoutines虛擬碼):
//=============================================================================
// Matt Pietrek, September 1999 Microsoft Systems Journal
// 中文注釋部分為jefong翻譯
//
// Pseudocode for LdrpRunInitializeRoutines in NTDLL.DLL (NT 4, SP3)
//
// 當LdrpRunInitializeRoutines 在一個進程中第一次被調用時(這個進程的隱式連結模組已經被初始化),bImplicitLoad 參數是非零。當使用LoadLibrary調用dll時,bImplicitLoad 參數是零;
//=============================================================================

#include <ntexapi.h>    // For HardError defines near the end

// Global symbols (name is accurate, and comes from NTDLL.DBG)
//  _NtdllBaseTag
//  _ShowSnaps
//  _SaveSp
//  _CurSp
//  _LdrpInLdrInit
//  _LdrpFatalHardErrorCount
//  _LdrpImageHasTls

NTSTATUS
LdrpRunInitializeRoutines( DWORD bImplicitLoad )
{
    // 第一部分,得到可能需要初始化的模組的數目。一些模組可能已經被初始化過了
    unsigned nRoutinesToRun = _LdrpClearLoadInProgress();

    if ( nRoutinesToRun )
    {
        // 如果有需要初始化的模組,為它們分配一個隊列,用來裝載各模組資訊。
        pInitNodeArray = _RtlAllocateHeap(GetProcessHeap(),
                                            _NtdllBaseTag + 0x60000,
                                            nRoutinesToRun * 4 );
                           
        if ( 0 == pInitNodeArray )    // Make sure allocation worked
            return STATUS_NO_MEMORY;
    }
    else
        pInitNodeArray = 0;

    //第二部分;
    //進程環境塊(Peb),包含一個指向新載入模組的連結清單的指標。
    pCurrNode = *(pCurrentPeb->ModuleLoaderInfoHead);
    ModuleLoaderInfoHead = pCurrentPeb->ModuleLoaderInfoHead;
       
    if ( _ShowSnaps )
    {
        _DbgPrint( "LDR: Real INIT LIST/n" );
    }

    nModulesInitedSoFar = 0;

    if ( pCurrNode != ModuleLoaderInfoHead ) //判斷是否有新載入的模組
    {
       
        while ( pCurrNode != ModuleLoaderInfoHead ) //遍曆所有新載入的模組
        {
            ModuleLoaderInfo  pModuleLoaderInfo;
           
            //
            //一個ModuleLoaderInfo結構節點的大小為0X10位元組
            pModuleLoaderInfo = &NextNode - 0x10;
           
            localVar3C = pModuleLoaderInfo;        

            //
            // 如果模組已經被初始化,就忽略
            // X_LOADER_SAW_MODULE = 0x40 已被初始化
            if ( !(pModuleLoaderInfo->Flags35 & X_LOADER_SAW_MODULE) )
            {
                //
                // 模組沒有被初始化,判斷是否具有入口函數
                //
                if ( pModuleLoaderInfo->EntryPoint )
                {
                    //
                    // 具有初始化函數,添加到模組列表中,等待進行初始化
                    pInitNodeArray[nModulesInitedSoFar] =pModuleLoaderInfo;

                    // 如果ShowSnaps為非零,那麼列印出模組的路徑和入口函數的地址
      // 例如:
                    // C:/WINNT/system32/KERNEL32.dll init routine 77f01000
                    if ( _ShowSnaps )
                    {
                        _DbgPrint(  "%wZ init routine %x/n",
                                    &pModuleLoaderInfo->24,
                                    pModuleLoaderInfo->EntryPoint );
                    }

                    nModulesInitedSoFar++;
                }
            }

            // 設定模組的X_LOADER_SAW_MODULE標誌。說明這個模組還沒有被初始化。
            pModuleLoaderInfo->Flags35 &= X_LOADER_SAW_MODULE;

            // 處理下一個模組節點
            pCurrNode = pCurrNode->pNext
        }
    }
    else
    {
        pModuleLoaderInfo = localVar3C;     // May not be initialized???
    }
   
    if ( 0 == pInitNodeArray )
        return STATUS_SUCCESS;

    // ************************* MSJ Layout! *****************
    // If you're going to split this code across pages, this is a great
    // spot to split the code.  Just be sure to remove this comment
    // ************************* MSJ Layout! *****************
   
    //
    // pInitNodeArray指標包含一個模組指標隊列,這些模組還沒有 DLL_PROCESS_ATTACH
    // 第三部分,調用初始化部分
    try     // Wrap all this in a try block, in case the init routine faults
    {
        nModulesInitedSoFar = 0;  // Start at array element 0

        //
        // 遍曆模組隊列
        //
        while ( nModulesInitedSoFar < nRoutinesToRun )
        {
            // 獲得模組指標
            pModuleLoaderInfo = pInitNodeArray[ nModulesInitedSoFar ];

            // This doesn't seem to do anything...
            localVar3C = pModuleLoaderInfo;
           
            nModulesInitedSoFar++;
               
            // 儲存初始化程式入口指標
            pfnInitRoutine = pModuleLoaderInfo->EntryPoint;
           
            fBreakOnDllLoad = 0;    // Default is to not break on load

            // 調試用
            // If this process is a debuggee, check to see if the loader
            // should break into a debugger before calling the initialization.
            //
            // DebuggerPresent (offset 2 in PEB) is what IsDebuggerPresent()
            // returns. IsDebuggerPresent is an NT only API.
            //
            if ( pCurrentPeb->DebuggerPresent || pCurrentPeb->1 )
            {
                LONG retCode;

                //             
                // Query the "HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/
                // Windows NT/CurrentVersion/Image File Execution Options"
                // registry key.  If a a subkey entry with the name of
                // the executable exists, check for the BreakOnDllLoad value.
                //
                retCode =
                    _LdrQueryImageFileExecutionOptions(
                                pModuleLoaderInfo->pwszDllName,
                                "BreakOnDllLoad",pInitNodeArray
                                REG_DWORD,
                                &fBreakOnDllLoad,
                                sizeof(DWORD),
                                0 );

                // If reg value not found (usually the case), then don't
                // break on this DLL init
                if ( retCode <= STATUS_SUCCESS )
                    fBreakOnDllLoad = 0;pInitNodeArray
            }
           
            if ( fBreakOnDllLoad )
            {          
                if ( _ShowSnaps )
                {
                    // Inform the debug output stream of the module name
                    // and the init routine address before actually breaking
                    // into the debugger

                    _DbgPrint(  "LDR: %wZ loaded.",
                                &pModuleLoaderInfo->pModuleLoaderInfo );
                   
                    _DbgPrint(  "- About to call init routine at %lx/n",
                                pfnInitRoutine )
                }
               
                // Break into the debugger                             
                _DbgBreakPoint();   // An INT 3, followed by a RET
            }
            else if ( _ShowSnaps && pfnInitRoutine )
            {
                // Inform the debug output stream of the module name
                // and the init routine address before calling it              
                _DbgPrint(  "LDR: %wZ loaded.",
                            pModuleLoaderInfo->pModuleLoaderInfo );

                _DbgPrint("- Calling init routine at %lx/n", pfnInitRoutine);
            }
                   
            if ( pfnInitRoutine )
            {
                // 設定DLL_PROCESS_ATTACH標誌
                //
                // (Shouldn't this come *after* the actual call?)
                //
                // X_LOADER_CALLED_PROCESS_ATTACH = 0x8            
                pModuleLoaderInfo->Flags36 |= X_LOADER_CALLED_PROCESS_ATTACH;

                //
                // If there's Thread Local Storage (TLS) for this module,
                // call the TLS init functions.  *** NOTE *** This only
                // occurs during the first time this code is called (when
                // implicitly loaded DLLs are initialized).  Dynamically
                // loaded DLLs shouldn't use TLS declared vars, as per the
                // SDK documentation
                // 如果模組需要分配TLS,調用TLS初始化函數
  // 注意只有在第一次調時(bImplicitLoad!=0)才會分配TLS,就是隱式dll載入時
  // 當動態載入時(bImplicitLoad==0)就不需要聲明TLS變數
                if ( pModuleLoaderInfo->bHasTLS && bImplicitLoad )
                {
                    _LdrpCallTlsInitializers(   pModuleLoaderInfo->hModDLL,
                                                DLL_PROCESS_ATTACH );
                }
               

                hModDLL = pModuleLoaderInfo->hModDLL

                MOV     ESI,ESP // Save off the ESP register into ESI
  
  // 設定入口函數指標               
                MOV     EDI,DWORD PTR [pfnInitRoutine]                    

                // In C++ code, the following ASM would look like:
                //
                // initRetValue =
                // pfnInitRoutine(hInstDLL,DLL_PROCESS_ATTACH,bImplicitLoad);
                //

                PUSH    DWORD PTR [bImplicitLoad]
               
                PUSH    DLL_PROCESS_ATTACH
               
                PUSH    DWORD PTR [hModDLL]
               
                CALL    EDI     // 調用入口函數
               
                MOV     BYTE PTR [initRetValue],AL  // 儲存入口函數傳回值

                MOV     DWORD PTR [_SaveSp],ESI // Save stack values after the
                MOV     DWORD PTR [_CurSp],ESP  // entry point code returns

                MOV     ESP,ESI     // Restore ESP to value before the call

                //
                // 檢查調用前後的ESP值是否一至
  //
                if ( _CurSP != _SavSP )
                {
                    hardErrorParam = pModuleLoaderInfo->FullDllPath;

                    hardErrorRetCode =
                        _NtRaiseHardError(
                            STATUS_BAD_DLL_ENTRYPOINT | 0x10000000,
                            1,  // Number of parameters
                            1,  // UnicodeStringParametersMask,
                            &hardErrorParam,
                            OptionYesNo,    // Let user decide
                            &hardErrorResponse );
                                           
                    if ( _LdrpInLdrInit )
                        _LdrpFatalHardErrorCount++;

                    if (    (hardErrorRetCode >= STATUS_SUCCESS)
                        &&  (ResponseYes == hardErrorResponse) )
                    {
                        return STATUS_DLL_INIT_FAILED;
                    }
                }

                //
                // 入口函數返回0,錯誤
                //
                if ( 0 == initRetValue )
                {
                    DWORD hardErrorParam2;
                    DWORD hardErrorResponse2;
                                       
                    hardErrorParam2 = pModuleLoaderInfo->FullDllPath;
                   
                    _NtRaiseHardError(  STATUS_DLL_INIT_FAILED,
                                        1,  // Number of parameters
                                        1,  // UnicodeStringParametersMask
                                        &hardErrorParam2,
                                        OptionOk,   // OK is only response
                                        &hardErrorResponse2 );
                                                           
                    if ( _LdrpInLdrInit )
                        _LdrpFatalHardErrorCount++;

                    return STATUS_DLL_INIT_FAILED;
                }
            }
        }

        //
        // 如果EXE已經擁有了TLS,那麼調用TLS初始化函數,也是在進程第一次初始化dll時
        //     
        if ( _LdrpImageHasTls && bImplicitLoad )
        {
            _LdrpCallTlsInitializers(   pCurrentPeb->ProcessImageBase,
                                        DLL_PROCESS_ATTACH );
        }
    }
    __finally
    {
        //
        // 第四部分;
        // 清除分配的記憶體
        _RtlFreeHeap( GetProcessHeap(), 0, pInitNodeArray );
    }

    return STATUS_SUCCESS;
}  

這個函數分為四個主要部分:
一:第一部分調用_LdrpClearLoadInProgress函數,這個NTDLL函數返回已經被映象到記憶體的dll的個數。例如,你的進程調用exm.dll,而exm.dll又調用exm1.dll和exm2.dll,那麼_LdrpClearLoadInProgress會返回3。得到dll個數後,調用_RtlAllocateHeap,它會返回一個記憶體的隊列指標。偽碼中的隊列指標為pInitNodeArray。隊列中的每個節點指標都指向一個新載入的dll的結構資訊。
二:第二部分的代碼通過進程內部的資料結構獲得一個新載入dll的連結清單。並且檢查dll是否有入口指標,如果有,就把模組資訊指標加入pInitNodeArray中。偽碼中的模組資訊指標為pModuleLoaderInfo。但是有的dll是資源檔,並不具有入口函數。所以pInitNodeArray中節點比_LdrpClearLoadInProgress返回的數目要少。
三:第三部分的代碼枚舉了pInitNodeArray中的對象,並且調用了入口函數。因為這部分的初始化代碼有可能出現錯誤,所以使用了_try異常撲獲功能。這就是為什麼在DllMain中出現錯誤後不會使整個進程終止。
另外,在調用入口函數時還會對TLS進行初始化,當用 __declspec來聲明TLS變數時,連結器包含的資料可以進行觸發。在調用dll的入口函數時,LdrpRunInitializeRoutines函數會檢查是否需要初始化一個TLS,如果需要,就調用_LdrpCallTlsInitializers。
在最後的虛擬碼部分使用組合語言來進行dll的入口函數調用。主要的命令時CALL EDI;EDI中就是入口函數的指標。當此命令返回後,dll的初始化工作就完成了。對於C++寫的dll,DllMain已經執行完成了它的DLL_PROCESS_ATTACH代碼。注意一下入口函數的第三個參數pvReserved,當exe或dll隱式調用dll時這個參數是非零,當使用LoadLibrary調用時是零。在入口函數調用以後,載入器會檢查調用入口函數前和後的ESP的值,如果不同,dll的初始化函數就會報錯。檢查完ESP後,還會檢查入口函數的傳回值,如果是零,說明初始化的時候出現了什麼問題。並且系統會報錯並停止調用dll。在第三部分的最後,在初始化完成後,如果exe進程已經擁有了TLS,並且隱式調用的dll已經被初始化,那麼會調用_LdrpCallTlsInitializers。
四:第四部分代碼是清理代碼,象_RtlAllocateHeap 分配的pInitNodeArray的記憶體需要被釋放。釋放代碼出現在_finally塊中,調用了_RtlFreeHeap 。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.