如何验证一个地址可否使用—— MmIsAddressValid函数分析
又是一篇内核函数分析的博文,我个人觉得Windows的内核是最好的老师,当你想实现一个功能之前可以看看Windows内核是怎么做的,说不定就有灵感呢:)
首先看下官方的注释说明:
/*++ Routine Description: For a given virtual address this function returns TRUE if no page fault will occur for a read operation on the address, FALSE otherwise. Note that after this routine was called, if appropriate locks are not held, a non-faulting address could fault. Arguments: VirtualAddress - Supplies the virtual address to check. Return Value: TRUE if no page fault would be generated reading the virtual address, FALSE otherwise. Environment: Kernel mode. --*/
WDK文档中给出的功能描述是这样的:The MmIsAddressValid routine checks whether a page fault will occur for a read or write operation at a given virtual address.根据描述来看这个函数的功能只是去检查读写操作会不会触发一个页错误,但是作为一个常用函数,我们常常用这个函数来检查地址合不合法,这次就在源码里看下具体的流程,主要目的是搞清楚这个函数是怎么判断一个函数会不会触发页错误的。
1 BOOLEAN 2 MiIsAddressValid ( 3 IN PVOID VirtualAddress, 4 IN LOGICAL UseForceIfPossible 5 ) 6 { 7 PMMPTE PointerPte; 8 9 10 // 11 // If the address is not canonical then return FALSE as the caller (which 12 // may be the kernel debugger) is not expecting to get an unimplemented 13 // address bit fault. 14 // 15 16 if (MI_RESERVED_BITS_CANONICAL(VirtualAddress) == FALSE) { 17 return FALSE; 18 } 19 20 21 22 23 PointerPte = MiGetPdeAddress (VirtualAddress); 24 if (PointerPte->u.Hard.Valid == 0) { 25 return FALSE; 26 } 27 28 if (MI_PDE_MAPS_LARGE_PAGE (PointerPte)) { 29 return TRUE; 30 } 31 32 PointerPte = MiGetPteAddress (VirtualAddress); 33 if (PointerPte->u.Hard.Valid == 0) { 34 return FALSE; 35 } 36 37 // 38 // Make sure we're not treating a page directory as a page table here for 39 // the case where the page directory is mapping a large page. This is 40 // because the large page bit is valid in PDE formats, but reserved in 41 // PTE formats and will cause a trap. A virtual address like c0200000 (on 42 // x86) triggers this case. 43 // 44 45 if (MI_PDE_MAPS_LARGE_PAGE (PointerPte)) { 46 return FALSE; 47 } 48 49 return TRUE; 50 }
代码出人意外的简单,很明显,这是利用了分页机制去查询。先查下页目录项是否为空,然后再看一下页表项是否为空。至于28、29行应该是判断是不是直接使用PDE作为一级表吧,但是现在应该没有这么用的吧。
if (MI_PDE_MAPS_LARGE_PAGE (PointerPte)) { return TRUE; }
如上就是一个判断。
而MiGetPdeAddress和MiGetPteAddress 其实是两个宏,这个宏我们也可以拿来用。
#define MiGetPdeAddress(va) \ ((PMMPTE)(((((ULONG_PTR)(va) & VIRTUAL_ADDRESS_MASK) >> PDI_SHIFT) << PTE_SHIFT) + PDE_BASE))
#define MiGetPteAddress(va) \ ((PMMPTE)(((((ULONG_PTR)(va) & VIRTUAL_ADDRESS_MASK) >> PTI_SHIFT) << PTE_SHIFT) + PTE_BASE))
#define VIRTUAL_ADDRESS_BITS 48 #define VIRTUAL_ADDRESS_MASK ((((ULONG_PTR)1) << VIRTUAL_ADDRESS_BITS) - 1)
注意,每个进程都有自己的进程页表和页目录但是内核在分配一个进程的地址空间时会把PD给复制一份,以便于访问。
由以上的分析可以看出并没有所谓的验证地址是否可读写的功能,我们有时候会把它和ProbeForRead(),ProbeForWrite()这两个函数相混淆。这两个函数才是来验证地址是否可读写的函数,但是仅限于用户地址空间的地址。从WDK中可以看到如下的描述:The ProbeForWrite routine checks that a user-mode buffer actually resides in the user-mode portion of the address space, is writable, and is correctly aligned.我们来看下这个函数的实现。
1 VOID 2 ProbeForWrite ( 3 __inout_bcount(Length) PVOID Address, 4 __in SIZE_T Length, 5 __in ULONG Alignment 6 ) 7 8 /*++ 9 10 Routine Description: 11 12 This function probes a structure for write accessibility and ensures 13 correct alignment of the structure. If the structure is not accessible 14 or has incorrect alignment, then an exception is raised. 15 16 Arguments: 17 18 Address - Supplies a pointer to the structure to be probed. 19 20 Length - Supplies the length of the structure. 21 22 Alignment - Supplies the required alignment of the structure expressed 23 as the number of bytes in the primitive datatype (e.g., 1 for char, 24 2 for short, 4 for long, and 8 for quad). 25 26 Return Value: 27 28 None. 29 30 --*/ 31 32 { 33 34 ULONG_PTR EndAddress; 35 ULONG_PTR StartAddress; 36 37 #define PageSize PAGE_SIZE 38 39 // 40 // If the structure has zero length, then do not probe the structure for 41 // write accessibility or alignment. 42 // 43 44 if (Length != 0) { 45 46 // 47 // If the structure is not properly aligned, then raise a data 48 // misalignment exception. 49 // 50 51 ASSERT((Alignment == 1) || (Alignment == 2) || 52 (Alignment == 4) || (Alignment == 8) || 53 (Alignment == 16)); 54 55 StartAddress = (ULONG_PTR)Address; 56 if ((StartAddress & (Alignment - 1)) == 0) { 57 58 // 59 // Compute the ending address of the structure and probe for 60 // write accessibility. 61 // 62 63 EndAddress = StartAddress + Length - 1; 64 if ((StartAddress <= EndAddress) && 65 (EndAddress < MM_USER_PROBE_ADDRESS)) { 66 67 // 68 // N.B. Only the contents of the buffer may be probed. 69 // Therefore the starting byte is probed for the 70 // first page, and then the first byte in the page 71 // for each succeeding page. 72 // 73 // If this is a Wow64 process, then the native page is 4K, which 74 // could be smaller than the native page size/ 75 // 76 77 EndAddress = (EndAddress & ~(PageSize - 1)) + PageSize; 78 do { 79 *(volatile CHAR *)StartAddress = *(volatile CHAR *)StartAddress; 80 StartAddress = (StartAddress & ~(PageSize - 1)) + PageSize; 81 } while (StartAddress != EndAddress); 82 83 return; 84 85 } else { 86 ExRaiseAccessViolation(); 87 } 88 89 } else { 90 ExRaiseDatatypeMisalignment(); 91 } 92 } 93 94 return; 95 }
1 VOID 2 ProbeForRead( 3 __in_bcount(Length) VOID *Address, 4 __in SIZE_T Length, 5 __in ULONG Alignment 6 ) 7 8 /*++ 9 10 Routine Description: 11 12 This function probes a structure for read accessibility and ensures 13 correct alignment of the structure. If the structure is not accessible 14 or has incorrect alignment, then an exception is raised. 15 16 Arguments: 17 18 Address - Supplies a pointer to the structure to be probed. 19 20 Length - Supplies the length of the structure. 21 22 Alignment - Supplies the required alignment of the structure expressed 23 as the number of bytes in the primitive datatype (e.g., 1 for char, 24 2 for short, 4 for long, and 8 for quad). 25 26 Return Value: 27 28 None. 29 30 --*/ 31 32 { 33 34 PAGED_CODE(); 35 36 ASSERT((Alignment == 1) || (Alignment == 2) || 37 (Alignment == 4) || (Alignment == 8) || 38 (Alignment == 16)); 39 40 if (Length != 0) { 41 if (((ULONG_PTR)Address & (Alignment - 1)) != 0) { 42 ExRaiseDatatypeMisalignment(); 43 44 } else if ((((ULONG_PTR)Address + Length) > (ULONG_PTR)MM_USER_PROBE_ADDRESS) || 45 (((ULONG_PTR)Address + Length) < (ULONG_PTR)Address)) { 46 47 *(volatile UCHAR * const)MM_USER_PROBE_ADDRESS = 0; 48 } 49 } 50 }
以分页来管理内存,以页为单位,如果一个内存页的第一个字节可写,那么整个内存页就可写,所以就验证页的一个字节就可以了。(xxx & ~(PageSize - 1)) + PageSize就是以页为单位移动。这里也可以看到字节对齐Alignment仅仅是起到了一个验证的作用,而读写验证也只是一个简单的指针操作*(volatile CHAR *)StartAddress = *(volatile CHAR *)StartAddress;