首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

PE Loader 進階:分離內存分配與映射邏輯提升復用性

     編輯:關於C語言

動機:為什麼要拆分分配與映射?

在上一版 LdrLayoutPe 中,VirtualAlloc 直接封裝在函數內部。這導致兩大局限:

  1. 無法在內核模式使用:驅動程序無法調用 VirtualAlloc,需改用 MmAllocateIndependentPages 或 ExAllocatePoolWithTag + MDL 映射。
  2. 測試與彈性受限:單元測試時難以注入 Mock 內存塊;無法預先鎖定大頁內存、共享內存或已映射的映像區域。

重構後的 LdrLayoutPe 將內存管理職責上移至調用方,實現「純邏輯映射」,大幅提升復用性。

重構後的接口契約

/* 將文件內容按虛擬地址信息映射到預分配的地址空間
 * 參數:
 *   pImage        - 源PE文件緩衝區
 *   FileSize      - 文件大小
 *   pRealImageBase- 調用方預分配的目標基址 (已提交、可寫)
 *   SizeOfImage   - 目標區域大小 (需 >= OptionalHeader.SizeOfImage)
 * 返回: TRUE/FALSE 表示映射成敗
 */
BOOL LdrLayoutPe(char* pImage, DWORD FileSize, char* pRealImageBase, DWORD SizeOfImage)
{
    PIMAGE_FILE_HEADER pFileHeader;
    PIMAGE_SECTION_HEADER pSectionHeader;
    char* pSrc;
    char* pDst;
    DWORD CopySize;
    DWORD i;

    // 1. 校驗源文件合法性
    if(!LdrValidateImage(pImage, FileSize))
        return FALSE;

    // 2. 解析頭部結構
    pFileHeader = _GetFileHeaderFromPe(pImage);
    pSectionHeader = _GetSectionHeaderFromPe(pFileHeader);

    // 3. 拷貝文件頭 (含 Section Table)
    pDst = pRealImageBase;
    pSrc = pImage;
    CopySize = (char*)pSectionHeader - pImage + pFileHeader->NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
    
    // 邊界檢查:防止頭部拷貝溢出目標緩衝區
    if(pDst + CopySize >= pRealImageBase + SizeOfImage)
        return FALSE;
    RtlCopyMemory(pDst, pSrc, CopySize);

    // 4. 逐節區拷貝數據,帶嚴格邊界保護
    for(i = 0; i < pFileHeader->NumberOfSections; i++){
        if(!pSectionHeader[i].PointerToRawData)
            continue; // 無文件數據的節區 (如 .bss) 跳過拷貝,內存已由 VirtualAlloc 零初始化

        pDst = pRealImageBase + pSectionHeader[i].VirtualAddress;
        pSrc = pImage + pSectionHeader[i].PointerToRawData;
        CopySize = pSectionHeader[i].SizeOfRawData;

        // 關鍵防禦:確保拷貝不超出 SizeOfImage 邊界
        if(pDst + CopySize >= pRealImageBase + SizeOfImage)
            return FALSE;
        RtlCopyMemory(pDst, pSrc, CopySize);
    }
    return TRUE;
}

設計亮點解析

1. 依賴倒置:調用方控制內存生命週期

場景調用方實現策略
用戶態普通加載VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
指定基址加載 (需重定位)VirtualAlloc(pPreferredBase, ...) 失敗則 fallback
大頁內存優化VirtualAlloc(..., MEM_LARGE_PAGES) 需鎖定頁面權限
內核驅動加載MmAllocateIndependentPages + MmMapLockedPagesSpecifyCache
共享內存映射CreateFileMapping + MapViewOfFile 實現進程間映像共享
單元測試 Mock預分配堆緩衝區 malloc(SizeOfImage) 注入測試

2. 契約式防禦性編程

  • 前置條件檢查:LdrValidateImage 確保輸入非畸形 PE。
  • 緩衝區邊界守衛:兩處 pDst + CopySize >= pRealImageBase + SizeOfImage 徹底杜緩衝區溢出,這在處理惡意構造的 PE (如異常大的 VirtualAddress) 時至關重要。
  • 純邏輯無副作用:函數不分配、不釋放、不修改全局狀態,天然線程安全、可重入。

3. 隱含約定與擴展點

  • 權限預設:調用方需保證目標內存至少 PAGE_READWRITE;映射完成後,調用方應根據節區屬性調用 VirtualProtect 降權 (RX/RW/RO)。
  • 零初始化依賴:.bss 類節區 (PointerToRawData == 0) 依賴 VirtualAlloc 的零頁特性;若調用方使用 malloc 需自行 memset。
  • 重定位就緒:映射後的內存映像基址為 pRealImageBase,若與 ImageBase 不同,調用方可立即調用 LdrProcessRelocs(pRealImageBase, delta) 完成重定位。

典型調用示例 (用戶態)

BOOL LoadPeImage(char* pFileBuffer, DWORD dwFileSize, HMODULE* phMod)
{
    // 1. 解析 SizeOfImage (需先輕量解析 OptionalHeader)
    DWORD sizeOfImage = GetPeSizeOfImage(pFileBuffer);
    if(!sizeOfImage) return FALSE;

    // 2. 分配目標內存 (此處演示系統自動選址)
    LPVOID pMem = VirtualAlloc(NULL, sizeOfImage, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    if(!pMem) return FALSE;

    // 3. 純邏輯映射
    if(!LdrLayoutPe(pFileBuffer, dwFileSize, (char*)pMem, sizeOfImage)){
        VirtualFree(pMem, 0, MEM_RELEASE);
        return FALSE;
    }

    // 4. 後續處理: 重定位 -> IAT綁定 -> TLS回調 -> EntryPoint調用
    // ...

    *phMod = (HMODULE)pMem;
    return TRUE;
}

總結

通過「內存分配外置化」,LdrLayoutPe 從一個耦合 Win32 API 的工具函數蛻變為平台無關的 PE 映射核心。這不僅滿足驅動層、EFI、裸機、跨平台模擬器等異構環境的需求,更貫徹了單一職責原則與依賴倒置原則,是系統級組件庫設計的典範範例。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved