【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
通过以上代码可以实现,访问内存的异常触发和捕获处理。
因此剩下的问题就是如何实现数据到内存的地址对应关系管理了。
最后
原理性的东西已经展示出来,功能性的东西就各自开发了。
此异常处理的原理还可以应用在代码保护,内存保护,反外挂,代码虚拟机等等地方,虽然看似简单,但是其作用真的有非常多的想象空间,实际就看每个人的经验和创作力了。