来看下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 ===> size
从刚才分析的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中,如下:
/* * 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则为无效物理地址,这种情况用户空间一般不会出现。
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目录下。
range_is_allowed函数对要检查的物理地址范围以4K页为单位,一页一页的调用devmem_is_allowed,如果不允许,则会进行打印提示,并返回0,表示该物理地址范围不被允许。
来看devmem_is_allowed.该函数是平台相关函数,不过arm跟powerpc的实现相差不大,以arm的实现为例。在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。
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。
所以说对于网上给出的各种利用/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的确是映射的内核虚拟地址。
#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个变量来表示内核一些基本的物理地址和虚拟地址,如下: