【Windows核心程式設計】如何知道程式執行中當前操作的記憶體地址範圍,自己實現一個檔案對映類
大部分人窮極一生都止步於自己的“陷阱”裡,所以古人才有了破而後立的感悟!
問題來源
此問題源於對檔案對映FileMapping的改造需求。我們知道FileMapping的便利性,但可能在某個很小的開發範圍內,會發現FileMapping的侷限性!那就是隻能對核心支援的檔案物件進行對映,而核心檔案物件意味著檔案系統驅動,因而導致正常情況下只能對Windows支援的FAT/NTFS等檔案系統中的檔案進行對映。假如我有一個檔案在遠端伺服器,我不想通過檔案系統驅動的方式(網路共享也在其中)進行載入,也不想下載到本地磁碟,而是想直接將遠端檔案對映到本地記憶體中,那現有的FileMapping就無法完成了,或者需要開發驅動才能完成。
所以,有沒有辦法在使用者態環境下,改造或實現一個FileMapping,讓其能夠解決該問題?
問題解析
從FileMapping原理上看,操作對映有以下步驟:
1)建立時,告訴程序哪個檔案有能力被對映到記憶體(CreateFileMapping),
2)在訪問記憶體之前,我們還需要告知程序檔案中哪段內容會被對映到記憶體(MapViewOfFile),再把相應的記憶體空間保留起來,留到需要時使用,且檔案資料並未載入到該記憶體中。
3)在訪問記憶體時,由核心自動對映檔案資料到相應的記憶體。
4)對映細節:訪問記憶體涉及到讀和寫,在讀之前需要先將檔案資料載入,在寫之後需要將記憶體資料儲存到檔案
5)釋放
綜上,若要實現一個FileMapping,我們需要在記憶體中劃分保留空間,呼叫 VirtualAlloc 即可,接著最重要的是需要知道程序當前訪問的記憶體地址,以及如何在程序讀記憶體前和寫記憶體後進行相應的“對映”操作。至於檔案資料,我們可以放一邊,因為當前需求是獲取網路資料,當然也可以是任意其他方式讀取資料(比如從串列埠裝置中讀取資料)。
尋求方案
日常開發中,我們知道在除錯時,偵錯程式是可以知道被除錯程序當前執行的程式碼地址,以及各種變數地址和內容的,當然我們沒必要知道這麼詳細,而且實際中不太可能開發一個偵錯程式去實現該功能,我們有更好的方法。通過開源的核心原始碼以及相關資訊,我們知道程式在訪問檔案對映記憶體時,是通過觸發一個異常STATUS_ACCESS_VIOLATION,讓核心知道,然後再去自動載入檔案資料到記憶體,之後再讓程式重新執行訪問記憶體,最後程序才正常繼續往後面執行。
所以,我們同樣需要讓知道如何觸發並捕獲訪問記憶體異常的。
解決方法
1)如何觸發記憶體訪問異常?
我們知道訪問空指標或者無效指標,程式就會出現異常錯誤,不處理異常就會導致程式崩潰。
無效指標是因為對應的記憶體地址沒有申請,而通過VirtualAlloc申請記憶體後,就可以正常訪問了,如下:
pBaseAddress:=VirtualAlloc(nil,1024, MEM_COMMIT, PAGE_READWRITE);
學習Windows的記憶體管理機制後,就知道記憶體屬性PAGE_READWRITE代表該記憶體可以被讀寫,這裡我們將記憶體設定為PAGE_NOACCESS,後面程式對該記憶體空間進行訪問時,就會觸發記憶體訪問異常STATUS_ACCESS_VIOLATION(因為沒有可訪問屬性)
也可以在後續使用VirtualProtect對記憶體屬性進行設定,如:
VirtualProtect(pBaseAddress, 1024, PAGE_NOACCESS, oldProtect);
2)如何捕獲異常
進一步學習Windows異常機制,可以知道通過AddVectoredExceptionHandler(和RemoveVectoredExceptionHandler)新增異常處理函式。就能夠對STATUS_ACCESS_VIOLATION異常進行處理。
function VectoredHandler(var ExceptionInfo: EXCEPTION_POINTERS): LONG; stdcall; var oldProtect: DWORD; pAccessAddr: ULONG_PTR; pTemp: PByte; begin Result := EXCEPTION_CONTINUE_EXECUTION; if ExceptionInfo.ExceptionRecord.ExceptionCode = STATUS_ACCESS_VIOLATION then begin pAccessAddr := UIntPtr(ExceptionInfo.ExceptionRecord.ExceptionInformation[1]); pAccessAddr := (pAccessAddr div 4096) *4096; if pAccessAddr=UIntPtr(Pointer(pBaseAddress)) then begin //EFlags::TF(bit 8, 即第9位) [Trap flag] 將該位設定為1以允許單步除錯模式,清零則禁用該模式。即執行下一條指令後自動觸發單步除錯異常 ExceptionInfo.ContextRecord.EFlags := ExceptionInfo.ContextRecord.EFlags or $100; VirtualProtect(pBaseAddress, 1024, PAGE_READWRITE, oldProtect); pTemp:=pBaseAddress; Inc(pTemp, 5); pTemp^ := 5; Form1.Memo1.Lines.Add('ChangeAccess: PAGE_READWRITE'); Exit; end; end else if ExceptionInfo.ExceptionRecord.ExceptionCode = STATUS_SINGLE_STEP then begin VirtualProtect(pBaseAddress, 1024, PAGE_NOACCESS, oldProtect); Form1.Memo1.Lines.Add('ChangeAccess: PAGE_NOACCESS'); Exit; end; Result := EXCEPTION_CONTINUE_SEARCH; end;
var pAddVectoredExceptionHandler: TFnAddVectoredExceptionHandler; pRemoveVectoredExceptionHandler: TFnRemoveVectoredExceptionHandler; var pTemp: PByte; oldProtect: DWORD; nTmp: Byte; begin pVEHHandler := pAddVectoredExceptionHandler(1, @VectoredHandler); //安裝VEH try pTemp := pBaseAddress; Inc(pTemp, 5); pTemp^ := 2; Memo1.Lines.Add('WillAccess: AfterWrite'); Memo1.Lines.Add('WillAccess: BeforeRead'); nTmp := pTemp^; if nTmp<>0 then ShowMessage('pTemp^='+IntToStr(nTmp)) else ShowMessage('0000'); Memo1.Lines.Add('Free'); VirtualFree(pBaseAddress, 1024, MEM_FREE); finally pRemoveVectoredExceptionHandler(pVEHHandler); end; end;
呼叫後輸出資訊:
WillAccess: BeforeWrite STATUS_ACCESS_VIOLATION: ExceptionAddress: 00611288 AccessMode: Write AccessAddress: 050E0005 ChangeAccess: PAGE_READWRITE ChangeAccess: PAGE_NOACCESS WillAccess: AfterWrite WillAccess: BeforeRead STATUS_ACCESS_VIOLATION: ExceptionAddress: 006112C0 AccessMode: Read AccessAddress: 050E0005 ChangeAccess: PAGE_READWRITE ChangeAccess: PAGE_NOACCESS WillAccess: AfterRead
通過以上程式碼可以實現,訪問記憶體的異常觸發和捕獲處理。
因此剩下的問題就是如何實現資料到記憶體的地址對應關係管理了。
最後
原理性的東西已經展示出來,功能性的東西就各自開發了。
此異常處理的原理還可以應用在程式碼保護,記憶體保護,反外掛,程式碼虛擬機器等等地方,雖然看似簡單,但是其作用真的有非常多的想象空間,實際就看每個人的經驗和創作力了。