程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 合並DLL

合並DLL

編輯:關於VC++

前言

你可能不希望在發布程序時附帶上一個外部的 DLL,因為可能會有些用戶在無意中把 DLL 刪除了而造成 EXE 不能正確運行,也有可能該 DLL 會被別人拿去使用,也有可能,此 DLL 會成為破解者破解你的程序的突破口。無論出於何種原因,如果你想把一個 DLL 合並到一個 EXE 中的話,本文向你介紹這種方法。

Win32 程序調用 DLL 的機制

Win32 EXE 在調用一個外部 DLL 中的函數時,首先要調用 LoadLibary 函數來載入此 DLL 到程序的進程地址空間。如果 LoadLibary 載入此 DLL 成功,將返回一個該 DLL 的句柄。這個句柄實際上就是該 DLL 在內存中的起始地址。在載入 DLL 成功後,還必須調用 GetProcAddress 函數來獲取要調用的函數的地址。然後再根據該地址來調用這個函數。

根據上述原理,我們可以把一個 DLL 作為資源文件放到 EXE 文件中,在程序運行時,分配一塊內存,然後將此資源復制到該分配的內存中,並根據該內存地址計算得到相關的導出函數地址,然後,當我們需要調用某一函數時,可以用該函數在內存中的地址來調用它。

程序實現。

首先,把要合並的 DLL 作為資源加入到項目的資源文件中,然後在程序運行時,從資源中載入該資源,以得到該 DLL 在內存中的位置:

LPVOID sRawDll; // 資源文件在內存中的地址
HRSRC hRes;
HMODULE hLibrary;
HGLOBAL hResourceLoaded;
char lib_name[MAX_PATH];
GetModuleFileName(hInstance, lib_name, MAX_PATH ); // 得到運行程序的名字
hLibrary = LoadLibrary(lib_name);         // 就象載入一個 DLL 一樣載入運行程序到內存中
if (NULL != hLibrary)
{
  // 得到指定的資源文件在內存中的位置
  hRes = FindResource(hLibrary, MAKEINTRESOURCE(IDR_DATA1), RT_RCDATA);
  if (NULL != hRes)
  {
    // 將資源文件載入內存
    hResourceLoaded = LoadResource(hLibrary, hRes);
    if (NULL != hResourceLoaded)
    {
      // 得到資源文件大小
      SizeofResource(hLibrary, hRes);
      // 鎖定資源以得到它在內存中的地址
      sRawDll = (LPVOID)LockResource(hResourceLoaded);
    }
  }
  else return 1;
  FreeLibrary(hLibrary);
}
else return 1;

然後,從資源中載入 DLL 到內存函數 LoadPbDllFromMemory 將載入 DLL 到內存中, 該函數有兩個參數,第一個參數是指向 DLL 資源在內存中的地址的指針,也就是前面代碼中的 LockResource 函數的返回值。第二個參數是一個空的指針,如果函數 LoadPbDllFromMemory 運行成功,該指針將指向重新組合後的內存中的 DLL 的起始地址。該函數還有一個功能就是如果運行成功,它將手動地用 DLL_PROCESS_ATTACH 參數調用 DLL 的入口函數 DllMain 來初始化該 DLL。除此之外,它還會手動地載入合並的 DLL 的入口表中導入的 DLL 並調整它們在內存中的相對地址。以下是該函數代碼:

DWORD LoadPbDllFromMemory(LPVOID lpRawDll, LPVOID lpImageDll)
{
  SYSTEM_INFO sSysInfo;
  PIMAGE_DOS_HEADER dosHeader;
  PIMAGE_NT_HEADERS pNTHeader;
  PIMAGE_SECTION_HEADER section;
  PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
  PIMAGE_IMPORT_BY_NAME pOrdinalName;
  PIMAGE_BASE_RELOCATION baseReloc;
  PDWORD lpLink;
  unsigned char Protection[4096];
  HINSTANCE hDll;
  WORD i;
  DWORD ImagePages,fOldProtect,j,MaxLen,HdrLen,Addr1,Addr2,Pg,Pg1,Pg2;
  char * sDllName;
  if(NULL == lpRawDll) return 1 ;
  dosHeader = (PIMAGE_DOS_HEADER)lpRawDll;
  // Is this the MZ header?
  if ((TRUE == IsBadReadPtr(dosHeader,sizeof (IMAGE_DOS_HEADER))) ||
         (IMAGE_DOS_SIGNATURE != dosHeader->e_magic))
    return 2;
  // Get the PE header.
  pNTHeader = MakePtr(PIMAGE_NT_HEADERS,dosHeader,dosHeader->e_lfanew);
  // Is this a real PE image?
  if((TRUE == IsBadReadPtr(pNTHeader,sizeof ( IMAGE_NT_HEADERS))) ||
        ( IMAGE_NT_SIGNATURE != pNTHeader->Signature))
    return 3 ;
  if(( pNTHeader->FileHeader.SizeOfOptionalHeader !=
      sizeof(pNTHeader->OptionalHeader)) ||
    (pNTHeader->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC))
    return 4;
  if (pNTHeader->FileHeader.NumberOfSections < 1) return 5;
  section = IMAGE_FIRST_SECTION( pNTHeader );
  int HeaderSize = sizeof(IMAGE_SECTION_HEADER);
  // 節頭長度
  HdrLen = (DWORD)section - (DWORD)dosHeader +
      HeaderSize * pNTHeader->FileHeader.NumberOfSections;
  // 找出最大的節的長度,此節一般是代碼所在的節(.text 節)
  MaxLen = HdrLen;
  int ii=0;
  for (i = 0;i<(DWORD)pNTHeader->FileHeader.NumberOfSections;i++)// find MaxLen
  {
    if(MaxLen < section[i].VirtualAddress + section[i].SizeOfRawData)
    {
      MaxLen = section[i].VirtualAddress + section[i].SizeOfRawData;
    }
    if(strcmp((const char *)section[i].Name,".rsrc") == 0) ii=i;
  }
  GetSystemInfo(&sSysInfo);
  ImagePages = MaxLen / sSysInfo.dwPageSize;
  if (MaxLen % sSysInfo.dwPageSize) ImagePages++;
  // 分配所需的內存
  DWORD NeededMemory = ImagePages * sSysInfo.dwPageSize;
  lpImageDll = VirtualAlloc(NULL, NeededMemory, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if (lpImageDll == NULL) return 6; // 分配內存失敗
  MoveMemory( lpImageDll, lpRawDll, HdrLen ); // 復制節頭
  DWORD OrgAddr = 0;
  DWORD NewAddr = 0;
  DWORD Size = 0;
  // 復制 .text 節數據
  for (i = 0;i<pNTHeader->FileHeader.NumberOfSections;i++)
  {
    OrgAddr = (DWORD)lpImageDll + (DWORD)section[i].VirtualAddress;
    NewAddr = (DWORD)lpRawDll + (DWORD)section[i].PointerToRawData;
    Size = (DWORD)section[i].SizeOfRawData;
    MoveMemory((void *)OrgAddr, (void *)NewAddr, Size );
  }
  // 把指針指向新的 DLL 映像
  dosHeader = (PIMAGE_DOS_HEADER) lpImageDll; // Switch to new image
  pNTHeader = (PIMAGE_NT_HEADERS) ((DWORD)dosHeader + dosHeader->e_lfanew);
  section = (PIMAGE_SECTION_HEADER) ((DWORD)pNTHeader + sizeof(IMAGE_NT_HEADERS));
  pImageBase = (PBYTE)dosHeader;
  if((ii!=0) && (IsNT()==TRUE))
  {
    section[ii].VirtualAddress = section[ii].VirtualAddress + (DWORD)lpRawDll;
    section[ii].PointerToRawData = section[ii].PointerToRawData + (DWORD)lpRawDll;
  }
  DWORD importsStartRVA;
  // Look up where the imports section is (normally in the .idata section)
  // but not necessarily so. Therefore, grab the RVA from the data dir.
  importsStartRVA = GetImgDirEntryRVA(pNTHeader,IMAGE_DIRECTORY_ENTRY_IMPORT);
  if ( !importsStartRVA )
  {
    VirtualFree(dosHeader,0, MEM_RELEASE);
    return 7;
  }
  pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) pNTHeader->
    OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  if(pImportDesc!= 0)
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ((DWORD)pImportDesc + (DWORD)dosHeader);
  else
  {
    VirtualFree(dosHeader,0, MEM_RELEASE);
    return 8;
  }
  while (1) // 處理各入口表中的 DLL
  {
    // 檢查是否遇到了空的 IMAGE_IMPORT_DESCRIPTOR
    if ((pImportDesc->TimeDateStamp==0 ) && (pImportDesc->Name==0)) break;
    // 從磁盤載入必須的 Dll,
    // 注意,載入的 DLL 是合並的 DLL 的入口表中的 DLL,
    // 不是合並到 EXE 中的 DLL
    sDllName = (char *) (pImportDesc->Name + (DWORD)pImageBase);
    hDll = GetModuleHandle(sDllName);
    if (hDll == 0 ) hDll = LoadLibrary(sDllName);
    if (hDll == 0 )
    {
      MessageBox(NULL, "Can''t find required Dll",
          "Error in LoadPbDllFromMemory()",0);
      VirtualFree(dosHeader,0, MEM_RELEASE);
      return 9;
    }
    DWORD *lpFuncNameRef = (DWORD *) (pImportDesc->OriginalFirstThunk +
                 (DWORD)dosHeader);
    DWORD *lpFuncAddr = (DWORD *) (pImportDesc->FirstThunk +
                 (DWORD)dosHeader);
    while( *lpFuncNameRef != 0)
    {
      pOrdinalName = (PIMAGE_IMPORT_BY_NAME) (*lpFuncNameRef +
             (DWORD)dosHeader);
      DWORD pIMAGE_ORDINAL_FLAG = 0x80000000;
      if (*lpFuncNameRef & pIMAGE_ORDINAL_FLAG)
        *lpFuncAddr = (DWORD) GetProcAddress(hDll,
           (const char *)(*lpFuncNameRef & 0xFFFF));
      else
        *lpFuncAddr = (DWORD) GetProcAddress(hDll,
             (const char *)pOrdinalName->Name);
      if (lpFuncAddr == 0)
      {
        VirtualFree(dosHeader,0, MEM_RELEASE);
        return 10;// Can''t GetProcAddress
      }
      lpFuncAddr++;
      lpFuncNameRef++;
    }
    pImportDesc++;
  }
  DWORD TpOffset;
  baseReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pNTHeader->
     OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
  if (baseReloc !=0)
  {
    baseReloc = (PIMAGE_BASE_RELOCATION) ((DWORD)baseReloc + (DWORD)dosHeader);
    while(baseReloc->VirtualAddress != 0)
    {
      PWORD lpTypeOffset = (PWORD) ((DWORD)baseReloc +
           sizeof(IMAGE_BASE_RELOCATION));
      while (lpTypeOffset < (PWORD)((DWORD)baseReloc +
             (DWORD)baseReloc->SizeOfBlock))
      {
        TpOffset = *lpTypeOffset & 0xF000;
        if(TpOffset == 0x3000)
        {
          lpLink = (PDWORD) ((DWORD)dosHeader +
                             baseReloc->VirtualAddress +
                           (*lpTypeOffset & 0xFFF));
          *lpLink = (DWORD)dosHeader +
                      (*lpLink) - pNTHeader->OptionalHeader.ImageBase;
        }
        else
        {
          if (TpOffset != 0)
          {
            VirtualFree(dosHeader,0, MEM_RELEASE);
            return 10;
          }
        }
        lpTypeOffset++;
      }
      baseReloc = (PIMAGE_BASE_RELOCATION)((DWORD)baseReloc +
        (DWORD)baseReloc->SizeOfBlock);
    }
  }
  // 取得原始的內存狀態
  memset(Protection,0,4096);
  for (i = 0;i<=pNTHeader->FileHeader.NumberOfSections;i++)
  {
    if (i == pNTHeader->FileHeader.NumberOfSections)
    {
      Addr1 = 0;
      Addr2 = HdrLen;
      j = 0x60000000;
    }
    else
    {
      Addr1 = section[i].VirtualAddress;
      Addr2 = section[i].SizeOfRawData;
      j = section[i].Characteristics;
    }
    Addr2 += Addr1 - 1;
    Pg1 = Addr1 / sSysInfo.dwPageSize;
    Pg2 = Addr2 / sSysInfo.dwPageSize;
    for(Pg = Pg1 ;Pg<=Pg2;Pg++)
    {
      if (j & 0x20000000) Protection[Pg] |= 1; // Execute
      if (j & 0x40000000) Protection[Pg] |= 2; // Read
      if (j & 0x80000000) Protection[Pg] |= 4; // Write
    }
  }
  // 恢復原始的內存狀態
  Addr1 = (DWORD)dosHeader;
  for (Pg = 0 ;Pg<= ImagePages;Pg++)
  {
    switch(Protection[Pg])
    {
    case 2:
      fOldProtect = PAGE_READONLY;
      break;
    case 3:
      fOldProtect = PAGE_EXECUTE_READ;
      break;
    case 6:
      fOldProtect = PAGE_READWRITE;
      break;
    default:
      // Ignore strange combinations
      fOldProtect = PAGE_EXECUTE_READWRITE;
      break;
    }
    if (fOldProtect !=PAGE_EXECUTE_READWRITE)
    {
      if (VirtualProtect((void *)Addr1,
        sSysInfo.dwPageSize,
        fOldProtect,
        &fOldProtect) == 0)
      {
        VirtualFree(dosHeader,0, MEM_RELEASE);
        return 11;
      }
    }
    Addr1 += sSysInfo.dwPageSize;
  }
  EntryPoint = (LPENTRYPOINT) ((DWORD)pNTHeader->OptionalHeader.AddressOfEntryPoint +
         (DWORD)dosHeader);
  LPVOID lpReserved = 0;
  EntryPoint((HINSTANCE)dosHeader, DLL_PROCESS_ATTACH, lpReserved);
  lpImageDll2=lpImageDll;
  return 0;
}

一但 DLL 被正確地載入到內存中,我們就可以通過自定義函數 GetProcAddressDirectly 來獲取某函數在內存中的地址,並根據該地址來調用該函數,該函數也有兩個參數,第一個參數是指向載入到內存中的 DLL 的起始地址的指針,第二個是要調用的函數的函數名。以下是 GetProcAddressDirectly 函數代碼:

DWORD GetProcAddressDirectly(PIMAGE_DOS_HEADER dosHeader, char * FuncName)
{
  PIMAGE_NT_HEADERS pNTHeader;
  PIMAGE_EXPORT_DIRECTORY pExportDir;
  PWORD lpNameOrdinals;
  LPDWORD lpFunctions;
  DWORD * lpName;
  char * lpExpFuncName;
  DWORD i;
  DWORD j;
  char * lpFuncName;
  if(dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return 0;
  pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);
  if (pNTHeader->Signature != IMAGE_NT_SIGNATURE) return 0;
  if ((pNTHeader->FileHeader.SizeOfOptionalHeader != sizeof(pNTHeader->OptionalHeader)) || 
    (pNTHeader->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC))
    return 0;
  DWORD exportsStartRVA, exportsEndRVA;
  pImageBase = (PBYTE)dosHeader;
  // Make pointers to 32 and 64 bit versions of the header. 
  pNTHeader = MakePtr( PIMAGE_NT_HEADERS, dosHeader,dosHeader->e_lfanew );
  exportsStartRVA = GetImgDirEntryRVA(pNTHeader,IMAGE_DIRECTORY_ENTRY_EXPORT);
  exportsEndRVA = exportsStartRVA + 
    GetImgDirEntrySize(pNTHeader, IMAGE_DIRECTORY_ENTRY_EXPORT);
  // Get the IMAGE_SECTION_HEADER that contains the exports. This is 
  // usually the .edata section, but doesn''t have to be. 
  PIMAGE_SECTION_HEADER header;
  header = GetEnclosingSectionHeader( exportsStartRVA, pNTHeader );
  if ( !header ) return 0;
  INT delta;
  delta = (INT)(header->VirtualAddress - header->PointerToRawData);
  pExportDir = (PIMAGE_EXPORT_DIRECTORY)GetPtrFromRVA(exportsStartRVA,
        pNTHeader, pImageBase);
  pExportDir =(PIMAGE_EXPORT_DIRECTORY) (pNTHeader->
  OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
  if (pExportDir == 0)
  {
    MessageBox(NULL,"Error in GetProcAddressDirectly()",0,0);
    return 0;
  }
  pExportDir =(PIMAGE_EXPORT_DIRECTORY) ((DWORD)pExportDir + (DWORD)dosHeader);
  lpNameOrdinals =(PWORD)((DWORD)pExportDir->AddressOfNameOrdinals + (DWORD)dosHeader);
  lpName =(LPDWORD) (pExportDir->AddressOfNames + (DWORD)dosHeader);
  lpFunctions =(LPDWORD) (pExportDir->AddressOfFunctions + (DWORD)dosHeader);
  lpFuncName = FuncName;
  if(HIWORD(lpFuncName)!=0 )
  {
    for( i = 0;i<=pExportDir->NumberOfFunctions - 1;i++)
    {
      DWORD entryPointRVA = *lpFunctions;
      // Skip over gaps in exported function 
      if ( entryPointRVA == 0 ) continue;
      for( j = 0;j<=pExportDir->NumberOfNames-1;j++)
      {
        if( lpNameOrdinals[j] == i)
        {
          lpExpFuncName = (char *) (lpName[j] +
              (DWORD)dosHeader);
          if(strcmp((char *)lpExpFuncName,(char *)FuncName)==0)
            return (DWORD) (lpFunctions[i] +
                (DWORD)dosHeader);
        }
      }
    }
  }
  else
  {
    for (i = 0 ;i<=pExportDir->NumberOfFunctions - 1;i++)
    {
      if (lpFuncName == (char *)(pExportDir->Base + i))
      {
        if (lpFunctions[i]) return (unsigned long) (lpFunctions[i] +
              dosHeader);
      }
    }
  }
  return 0;
}

在調用完函數後,不要忘記用 UnloadPbDllFromMemory 來從內存中移去 DLL 以釋放分配的內存,該函數還會用 DLL_PROCESS_DETACH 參數調用 DLL 的入口函數 DllMain 來從調用進程的地址空間卸載該 DLL。以下是 UnloadPbDllFromMemory 函數代碼:

DWORD UnloadPbDllFromMemory(PIMAGE_DOS_HEADER dosHeader)
{
  PIMAGE_NT_HEADERS pNTHeader;
  pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + (DWORD)dosHeader->e_lfanew);
  EntryPoint = (LPENTRYPOINT)(pNTHeader->OptionalHeader.AddressOfEntryPoint +
           (DWORD)dosHeader);
  EntryPoint((HINSTANCE)dosHeader, DLL_PROCESS_DETACH, 0);
  return VirtualFree(dosHeader, 0, MEM_RELEASE);
}

關於示例代碼的說明

在本文附帶的示例代碼中,合並了一個名為 hardware.dll 的動態連接庫,該動態連接庫是一個獲取系統硬件信息的庫文件,其中包括了以下函數:

getmac     取得網卡 MAC
  VolumeNumber   取得硬盤卷標
  changeres      改變屏幕分辯率
  IsDiskInDrive    檢查軟驅中是否插有盤
  DPGetDefaultPrinter 取得默認的打印機名
  DPSetDefaultPrinter 設置默認的打印機
  getserial      取得硬盤的出廠序列號
  getmetric      取得顯示分辯率
  PrintStringDirect  直接向打印機發送一個串
  vfpbeep       讓 PC 喇叭發聲
  getcpuid       取得 CPU ID
  getbios       取得主板 BIOS ID

在示例代碼中,只調用了其中一個函數 getbios 來獲取主板 BIOS ID, 這裡說明一下,該函數實際上好象只能檢測 AWARD 主板的 BIOS, 也就是說它是讀取的是系統內存 0x000fex71 處的值。因為其它牌子的主板 BIOS 的位置稍有不同,但在程序中沒有進行這方面的處理,所以在讀其它牌子的主板 BIOS 時可能會有些問題(讀出的內容可能不正確)。關於此 DLL 的內容和實現,也許我會在另一篇文章中論及。

局限

在我進行測試時,發現對於有些含有資源的 DLL,在 9x 平台下可能會有問題。

題外話

另外,其它一些本文未提及的非主要的函數,請自行參見源代碼中的注釋。

再,本文涉及 PE 文件格式方面的知識,它們已經超出了本文的范圍,具體信息可參見 MSDM 中的:

Peering Inside the PE: A Tour of the Win32 Portable Executable File Format 一文和

Microsoft Portable Executable and Common Object File Format Specification 一文

特別感謝盧春明(Aming)在我編寫本文時所作的一些技術方面的建議和指導

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved