網上的說法也很是統一,/dev/mem是物理內存的全映像,可以用來訪問物理內存,通常使用方法是open("/dev/mem",O_RDWR|O_SYNC),接着就可以用mmap來訪問物理內存以及外設的IO資源,這就是實現用戶空間驅動的一種方法。
用戶空間驅動聽起來很是酷。但是對於/dev/mem,我認爲沒那麼簡單,有2個地方引發個人懷疑:
(1)網上資料都說/dev/mem是物理內存的全鏡像。這個概念很是含糊,/dev/mem究竟可以完畢哪些地址的虛實映射?
(2)/dev/mem看似很是強大。但是這也太危急了,黑客全然可以利用/dev/mem對kernel代碼以及IO進行一系列的非法操做,後果不可預測。難道內核開發人員們沒有意識到這點嗎?
網上資料說法都很是泛泛,僅僅對mem設備的使用進行說明,沒有對這些問題進行深究。安全
要搞清這一點,我認爲仍是從/dev/mem驅動開始下手。app
參考內核版本號:3.4.55
參考平臺:powerpc/arm
mem驅動在drivers/char/mem.c,mmap是系統調用。產生軟中斷進入內核後調用sys_mmap。終於會調用到mem驅動的mmap實現函數。
ide
來看下mem.c中的mmap實現:函數
static int mmap_mem(struct file *file, struct vm_area_struct *vma) { size_t size = vma->vm_end - vma->vm_start; if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size)) return -EINVAL; if (!private_mapping_ok(vma)) return -ENOSYS; if (!range_is_allowed(vma->vm_pgoff, size)) return -EPERM; if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size, &vma->vm_page_prot)) return -EINVAL; vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff, size, vma->vm_page_prot); vma->vm_ops = &mmap_mem_ops; /* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */ if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) { return -EAGAIN; } return 0; }vma是內核內存管理很是重要的一個結構體。
這些成員的賦值是在調用詳細驅動的mmap實現函數以前。在sys_mmap中進行的。
在mmap_mem最後調用remap_pfn_range,該函數完畢指定物理地址與用戶空間虛擬地址頁表的創建。
remap_pfn_range參數中vma->vm_pgoff即表明要映射的物理地址,並無範圍限制僅可以操做內存。
mmap系統調用的函數定義例如如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr指定要映射到的虛擬地址。寫NULL則有sys_mmap來分配該虛擬地址。
mmap參數與mem_mmap參數對應關係例如如下:
prot ===> vma->vma_page_prot
offset ===> vma->vma_pgoff
length ===> sizepost
從剛纔分析的mem_mmap流程來看,可以得出一個簡單的結論:
mem_mmap可以映射整個處理器的地址空間。而不僅僅是內存。這裏要說明的是,地址空間不等於內存空間。站在處理器角度看。地址空間指處理器總線上的所有可尋址空間。除了內存,還有外設的IO空間。以及其它總線映射過來的mem(如PCI)
個人理解。mem_mmap全然可以映射0-0xffffffff的所有物理地址(填TLB頁表完畢映射)。但前提是保證該物理地址是真實有效的,也就是處理器訪問該總線物理地址可以獲取有效數據。
因此現在看來mmap /dev/mem,僅僅要肯定咱們處理器的地址空間分佈,就可以將咱們需要的地址映射到用戶空間進行操做。
假設地址不是一個有效物理地址(處理器地址空間分佈中該地址沒用)。mmap創建該物理地址與用戶空間虛擬地址的映射。填TLB,CPU通過TLB翻譯後去訪問該不存在的物理地址訪問就有可能致使CPU掛掉。
這也就解釋了我第一個疑問,但是kernel的安全機制不會贊成用戶這麼肆無忌憚的操做。接着來看remap_pfn_range以前mmap_mem怎樣進行防禦。
首先是valid_mmap_phys_addr_range,檢查該物理地址是不是一個有效的mmap地址。假設平臺定義了ARCH_HAS_VALID_PHYS_ADDR_RANGE則會實現該函數,
arm中定義並實現了該函數,在arch/arm/mm/mmap.c中,例如如下:ui
/* * We don't use supersection mappings for mmap() on /dev/mem, which * means that we can't map the memory area above the 4G barrier into * userspace. */ int valid_mmap_phys_addr_range(unsigned long pfn, size_t size) { return !(pfn + (size >> PAGE_SHIFT) > 0x00100000); }該函數肯定mmap的範圍是否超過4G,超過4G則爲無效物理地址,這樣的狀況用戶空間通常不會出現。
而對於powerpc,平臺未定義ARCH_HAS_VALID_PHYS_ADDR_RANGE,因此valid_mmap_phys_addr_range在mem.c中定義爲空函數,返回1 表示該物理地址一直有效。
物理地址有效。不會返回-EINVAL。繼續往下走。
接下來是private_mapping_ok,對於有MMU的CPU,實現例如如下:
this
static inline int private_mapping_ok(struct vm_area_struct *vma) { return 1; }MMU的權限管理可以支持私有映射,因此該函數一直成功。
#ifdef CONFIG_STRICT_DEVMEM static inline int range_is_allowed(unsigned long pfn, unsigned long size) { u64 from = ((u64)pfn) << PAGE_SHIFT; u64 to = from + size; u64 cursor = from; while (cursor < to) { if (!devmem_is_allowed(pfn)) { printk(KERN_INFO "Program %s tried to access /dev/mem between %Lx->%Lx.\n", current->comm, from, to); return 0; } cursor += PAGE_SIZE; pfn++; } return 1; } #else static inline int range_is_allowed(unsigned long pfn, unsigned long size) { return 1; } #endif可以看出假設不打開CONFIG_STRICT_DEVMEM,range_is_allowed是返回1,表示該物理地址範圍是被贊成的。查看kconfig文件(在對應平臺文件夾下。如arch/arm/Kconfig.debug中)找到CONFIG_STRICT_DEVMEM說明例如如下
config STRICT_DEVMEM def_bool y prompt "Filter access to /dev/mem" help This option restricts access to /dev/mem. If this option is disabled, you allow userspace access to all memory, including kernel and userspace memory. Accidental memory access is likely to be disastrous. Memory access is required for experts who want to debug the kernel. If you are unsure, say Y.該選項menuconfig時在kernel hacking文件夾下。
假設打開該選項,內核就會對mem設備訪問加以檢查。檢查函數就是range_is_allowed。spa
range_is_allowed函數對要檢查的物理地址範圍以4K頁爲單位,一頁一頁的調用devmem_is_allowed。假設不一樣意,則會進行打印提示。並返回0,表示該物理地址範圍不被贊成。翻譯
來看devmem_is_allowed.該函數是平臺相關函數,只是arm跟powerpc的實現相差不大,以arm的實現爲例。debug
在arch/arm/mm/mmap.c中。
/* * devmem_is_allowed() checks to see if /dev/mem access to a certain * address is valid. The argument is a physical page number. * We mimic x86 here by disallowing access to system RAM as well as * device-exclusive MMIO regions. This effectively disable read()/write() * on /dev/mem. */ int devmem_is_allowed(unsigned long pfn) { if (iomem_is_exclusive(pfn << PAGE_SHIFT)) return 0; if (!page_is_ram(pfn)) return 1; return 0; }首先iomem_is_exclusive檢查該物理地址是否被獨佔保留,實現例如如下:
#ifdef CONFIG_STRICT_DEVMEM static int strict_iomem_checks = 1; #else static int strict_iomem_checks; #endif /* * check if an address is reserved in the iomem resource tree * returns 1 if reserved, 0 if not reserved. */ int iomem_is_exclusive(u64 addr) { struct resource *p = &iomem_resource; int err = 0; loff_t l; int size = PAGE_SIZE; if (!strict_iomem_checks) return 0; addr = addr & PAGE_MASK; read_lock(&resource_lock); for (p = p->child; p ; p = r_next(NULL, p, &l)) { /* * We can probably skip the resources without * IORESOURCE_IO attribute? */ if (p->start >= addr + size) break; if (p->end < addr) continue; if (p->flags & IORESOURCE_BUSY && p->flags & IORESOURCE_EXCLUSIVE) { err = 1; break; } } read_unlock(&resource_lock); return err; }假設打開了CONFIG_STRICT_DEVMEM,iomem_is_exclusive遍歷iomem_resource鏈表,查看要檢查的物理地址所在resource的flags,假設是bug或者exclusive。則返回1,代表該物理地址是獨佔保留的。
對於外設的IO資源,kernel中使用platform device機制來註冊平臺設備(platform_device_register)時調用insert_resource將該設備對應的io資源插入到iomem_resource鏈表中。
假設我要對某外設的IO資源進行保護。防止用戶空間訪問。可以將其resource的flags置位exclusive就能夠。
只是我查看我平臺支持包裏的所有platform device的resource。flags都沒有置位exclusive或者busy。
假設我映射的物理地址範圍是外設的IO。檢查可以經過。
對於內存的mem資源,怎樣註冊到iomem_resource鏈表中。內核代碼中我還沒找到詳細的位置,只是iomem在proc下有對應的表徵文件。可以cat /proc/iomem。
依據個人實際操做測試。內存資源也都沒有exclusive。因此假設我映射地址是內存。檢查也可以經過。
因此這裏iomem_is_exclusive檢查一般是經過的。接下來看page_is_ram。看devmem_is_range的邏輯,假設地址是ram地址。則該地址不被贊成。page_is_ram也是平臺函數,查看powerpc的實現例如如下。
int page_is_ram(unsigned long pfn) { #ifndef CONFIG_PPC64 /* XXX for now */ return pfn < max_pfn; #else unsigned long paddr = (pfn << PAGE_SHIFT); struct memblock_region *reg; for_each_memblock(memory, reg) if (paddr >= reg->base && paddr < (reg->base + reg->size)) return 1; return 0; #endif } max_pfn賦值在在do_init_bootmem中。例如如下. void __init do_init_bootmem(void) { unsigned long start, bootmap_pages; unsigned long total_pages; struct memblock_region *reg; int boot_mapsize; max_low_pfn = max_pfn = memblock_end_of_DRAM() >> PAGE_SHIFT; total_pages = (memblock_end_of_DRAM() - memstart_addr) >> PAGE_SHIFT;max_pfn表明了內核lowmem的頁個數,lowmem在內核下靜態線性映射。系統啓動之初完畢映射以後不會修改。讀寫效率高。內核代碼都是跑在lowmem。
這裏就明確了假設要映射的物理地址在lowmem範圍內,也是不一樣意被映射的。
這樣range_is_allowed就分析完了。exclusive的iomem以及lowmem範圍內的物理地址是不一樣意被映射的。
接下來phys_mem_access_prot_allowed實現爲空返回1,沒有影響。
phys_mem_access_prot肯定咱們映射頁的權限,該函數也是平臺函數,以powerpc實現爲例,例如如下:
pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn, unsigned long size, pgprot_t vma_prot) { if (ppc_md.phys_mem_access_prot) return ppc_md.phys_mem_access_prot(file, pfn, size, vma_prot); if (!page_is_ram(pfn)) vma_prot = pgprot_noncached(vma_prot); return vma_prot; }假設有平臺實現的phys_mem_access_prot,則調用之。
假設沒有。對於不是lowmem範圍內的物理地址。權限設置爲uncached。
以上的檢查完畢,最後調用remap_pfn_range完畢頁表設置。
因此假設打開CONFIG_STRICT_DEVMEM,mem驅動會對mmap要映射的物理地址進行範圍和位置的檢查而後才進行映射。檢查條件例如如下:
(1)映射範圍不能超過4G。
(2)該物理地址所在iomem不能exclusive.
(3)該物理地址不能處在lowmem中。
因此說對於網上給出的各類利用/dev/mem來操做內存以及寄存器的文章。假設操做範圍在上述3個條件內,內核必須關閉CONFIG_STRICT_DEVMEM才行。
這樣對於mem設備個人2個疑問算是攻克了。
查看mem.c時我還看到了另一個有趣的設備kmem。這個設備mmap的是哪裏的地址,網上的說法是內核虛擬地址。這個說法我不覺得然,這裏記錄下個人想法。
假設內核打開CONFIG_KMEM。則會建立kmem設備。它與mem設備主要區別在mmap的實現上。kmem的mmap實現例如如下:#ifdef CONFIG_DEVKMEM static int mmap_kmem(struct file *file, struct vm_area_struct *vma) { unsigned long pfn; /* Turn a kernel-virtual address into a physical page frame */ pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT; /* * RED-PEN: on some architectures there is more mapped memory than * available in mem_map which pfn_valid checks for. Perhaps should add a * new macro here. * * RED-PEN: vmalloc is not supported right now. */ if (!pfn_valid(pfn)) return -EIO; vma->vm_pgoff = pfn; return mmap_mem(file, vma); } #endif引發我注意的是__pa,完畢內核虛擬地址到物理地址的轉換,最後調用mmap_mem,簡單一看kmem的確是映射的內核虛擬地址。
以powerpc爲例。在arch/powerpc/include/asm/page.h,定義例如如下:
#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET)) #define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET) .... #define VIRT_PHYS_OFFSET (KERNELBASE - PHYSICAL_START)內核中定義了4個變量來表示內核一些主要的物理地址和虛擬地址,例如如下:
對於超過lowmem範圍,訪問highmem。假設使用__pa訪問,由於highmem是動態映射的,其映射關係不是線性的那麼簡單了,依據__pa獲取的物理地址與咱們想要的內核虛擬地址是不正確應的。