/dev/mem

2024-06-03 15:38
文章标签 mem dev

本文主要是介绍/dev/mem,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这几天研究了下/dev/mem,发现功能很神奇,通过mmap可以将物理地址映射到用户空间的虚拟地址上,在用户空间完成对设备寄存器的操作,于是上网搜了一些/dev/mem的资料。网上的说法也很统一,/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驱动开始下手。

参考内核版本:3.4.55 
参考平台:powerpc/arm

mem驱动在drivers/char/mem.c,mmap是系统调用,产生软中断进入内核后调用sys_mmap,最终会调用到mem驱动的mmap实现函数。

来看下mem.c中的mmap实现:

[cpp]  view plain copy
  1. static int mmap_mem(struct file *file, struct vm_area_struct *vma)  
  2. {  
  3.     size_t size = vma->vm_end - vma->vm_start;  
  4.   
  5.     if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))  
  6.         return -EINVAL;  
  7.   
  8.     if (!private_mapping_ok(vma))  
  9.         return -ENOSYS;  
  10.   
  11.     if (!range_is_allowed(vma->vm_pgoff, size))  
  12.         return -EPERM;  
  13.   
  14.     if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,  
  15.                         &vma->vm_page_prot))  
  16.         return -EINVAL;  
  17.   
  18.     vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,  
  19.                          size,  
  20.                          vma->vm_page_prot);  
  21.   
  22.     vma->vm_ops = &mmap_mem_ops;  
  23.   
  24.     /* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */  
  25.     if (remap_pfn_range(vma,  
  26.                 vma->vm_start,  
  27.                 vma->vm_pgoff,  
  28.                 size,  
  29.                 vma->vm_page_prot)) {  
  30.         return -EAGAIN;  
  31.     }  
  32.     return 0;  
  33. }  
vma是内核内存管理很重要的一个结构体,
其结构成员中start end代表要映射到的用户空间虚拟地址范围,用户空间的动态映射是以PAGE_SIZE也就是4K为一页,
vma_pgoff是要映射的物理地址,vma_page_prot代表该页的权限。


这些成员的赋值是在调用具体驱动的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中,如下:

[cpp]  view plain copy
  1. /* 
  2.  * We don't use supersection mappings for mmap() on /dev/mem, which 
  3.  * means that we can't map the memory area above the 4G barrier into 
  4.  * userspace. 
  5.  */  
  6. int valid_mmap_phys_addr_range(unsigned long pfn, size_t size)  
  7. {  
  8.     return !(pfn + (size >> PAGE_SHIFT) > 0x00100000);  
  9. }  
该函数确定mmap的范围是否超过4G,超过4G则为无效物理地址,这种情况用户空间一般不会出现。
而对于powerpc,平台没有定义ARCH_HAS_VALID_PHYS_ADDR_RANGE,所以valid_mmap_phys_addr_range在mem.c中定义为空函数,返回1 表示该物理地址一直有效。
物理地址有效,不会返回-EINVAL,继续往下走。

接下来是private_mapping_ok,对于有MMU的CPU,实现如下:
[cpp]  view plain copy
  1. static inline int private_mapping_ok(struct vm_area_struct *vma)  
  2. {  
  3.     return 1;  
  4. }  
MMU的权限管理可以支持私有映射,所以该函数一直成功。

接下来是一个最为关键的检查函数range_is_allowed,定义如下:
[cpp]  view plain copy
  1. #ifdef CONFIG_STRICT_DEVMEM  
  2. static inline int range_is_allowed(unsigned long pfn, unsigned long size)  
  3. {  
  4.     u64 from = ((u64)pfn) << PAGE_SHIFT;  
  5.     u64 to = from + size;  
  6.     u64 cursor = from;  
  7.   
  8.     while (cursor < to) {  
  9.         if (!devmem_is_allowed(pfn)) {  
  10.             printk(KERN_INFO  
  11.         "Program %s tried to access /dev/mem between %Lx->%Lx.\n",  
  12.                 current->comm, from, to);  
  13.             return 0;  
  14.         }  
  15.         cursor += PAGE_SIZE;  
  16.         pfn++;  
  17.     }  
  18.     return 1;  
  19. }  
  20. #else  
  21. static inline int range_is_allowed(unsigned long pfn, unsigned long size)  
  22. {  
  23.     return 1;  
  24. }  
  25. #endif  
可以看出如果不打开CONFIG_STRICT_DEVMEM,range_is_allowed是返回1,表示该物理地址范围是被允许的。查看kconfig文件(在相应平台目录下,如arch/arm/Kconfig.debug中)找到CONFIG_STRICT_DEVMEM说明如下

[cpp]  view plain copy
  1. config STRICT_DEVMEM  
  2.     def_bool y  
  3.     prompt "Filter access to /dev/mem"  
  4.     help  
  5.       This option restricts access to /dev/mem.  If this option is  
  6.       disabled, you allow userspace access to all memory, including  
  7.       kernel and userspace memory. Accidental memory access is likely  
  8.       to be disastrous.  
  9.       Memory access is required for experts who want to debug the kernel.  
  10.   
  11.       If you are unsure, say Y.  
该选项menuconfig时在kernel hacking目录下。
根据说明可以理解,CONFIG_STRICT_DEVMEM是严格的对/dev/mem访问检查,如果关掉该选项,用户就可以通过mem设备访问所有地址空间(根据对我提出的第一个问题理解,这里memory应该理解为地址空间)。该选项对于调试内核有帮助。
如果打开该选项,内核就会对mem设备访问加以检查,检查函数就是range_is_allowed。


range_is_allowed函数对要检查的物理地址范围以4K页为单位,一页一页的调用devmem_is_allowed,如果不允许,则会进行打印提示,并返回0,表示该物理地址范围不被允许

来看devmem_is_allowed.该函数是平台相关函数,不过arm跟powerpc的实现相差不大,以arm的实现为例。在arch/arm/mm/mmap.c中。

[cpp]  view plain copy
  1. /* 
  2.  * devmem_is_allowed() checks to see if /dev/mem access to a certain 
  3.  * address is valid. The argument is a physical page number. 
  4.  * We mimic x86 here by disallowing access to system RAM as well as 
  5.  * device-exclusive MMIO regions. This effectively disable read()/write() 
  6.  * on /dev/mem. 
  7.  */  
  8. int devmem_is_allowed(unsigned long pfn)  
  9. {  
  10.     if (iomem_is_exclusive(pfn << PAGE_SHIFT))  
  11.         return 0;  
  12.     if (!page_is_ram(pfn))  
  13.         return 1;  
  14.     return 0;  
  15. }  
首先iomem_is_exclusive检查该物理地址是否被独占保留,实现如下:

[cpp]  view plain copy
  1. #ifdef CONFIG_STRICT_DEVMEM  
  2. static int strict_iomem_checks = 1;  
  3. #else  
  4. static int strict_iomem_checks;  
  5. #endif  
  6.   
  7. /* 
  8.  * check if an address is reserved in the iomem resource tree 
  9.  * returns 1 if reserved, 0 if not reserved. 
  10.  */  
  11. int iomem_is_exclusive(u64 addr)  
  12. {  
  13.     struct resource *p = &iomem_resource;  
  14.     int err = 0;  
  15.     loff_t l;  
  16.     int size = PAGE_SIZE;  
  17.   
  18.     if (!strict_iomem_checks)  
  19.         return 0;  
  20.   
  21.     addr = addr & PAGE_MASK;  
  22.   
  23.     read_lock(&resource_lock);  
  24.     for (p = p->child; p ; p = r_next(NULL, p, &l)) {  
  25.         /* 
  26.          * We can probably skip the resources without 
  27.          * IORESOURCE_IO attribute? 
  28.          */  
  29.         if (p->start >= addr + size)  
  30.             break;  
  31.         if (p->end < addr)  
  32.             continue;  
  33.         if (p->flags & IORESOURCE_BUSY &&  
  34.              p->flags & IORESOURCE_EXCLUSIVE) {  
  35.             err = 1;  
  36.             break;  
  37.         }  
  38.     }  
  39.     read_unlock(&resource_lock);  
  40.   
  41.     return err;  
  42. }  
如果打开了CONFIG_STRICT_DEVMEM,iomem_is_exclusive遍历iomem_resource链表,查看要检查的物理地址所在resource的flags,如果是bug或者exclusive,则返回1,表明该物理地址是独占保留的。

据我了解,iomem_resource是来表征内核iomem资源的链表。

对于外设的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的实现如下。

[cpp]  view plain copy
  1. int page_is_ram(unsigned long pfn)  
  2. {  
  3. #ifndef CONFIG_PPC64    /* XXX for now */  
  4.     return pfn < max_pfn;  
  5. #else  
  6.     unsigned long paddr = (pfn << PAGE_SHIFT);  
  7.     struct memblock_region *reg;  
  8.   
  9.     for_each_memblock(memory, reg)  
  10.         if (paddr >= reg->base && paddr < (reg->base + reg->size))  
  11.             return 1;  
  12.     return 0;  
  13. #endif  
  14. }  
  15. max_pfn赋值在在do_init_bootmem中,如下.  
  16. void __init do_init_bootmem(void)  
  17. {  
  18.     unsigned long start, bootmap_pages;  
  19.     unsigned long total_pages;  
  20.     struct memblock_region *reg;  
  21.     int boot_mapsize;  
  22.   
  23.     max_low_pfn = max_pfn = memblock_end_of_DRAM() >> PAGE_SHIFT;  
  24.     total_pages = (memblock_end_of_DRAM() - memstart_addr) >> PAGE_SHIFT;  
max_pfn代表了内核lowmem的页个数,lowmem在内核下静态线性映射,系统启动之初完成映射之后不会改动,读写效率高,内核代码都是跑在lowmem。
lowmem大小我们可以通过cmdline的“mem=”来指定。

这里就明白了如果要映射的物理地址在lowmem范围内,也是不允许被映射的。

这样range_is_allowed就分析完了,exclusive的iomem以及lowmem范围内的物理地址是不允许被映射的。

接下来phys_mem_access_prot_allowed实现为空返回1,没有影响。

phys_mem_access_prot确定我们映射页的权限,该函数也是平台函数,以powerpc实现为例,如下:
[cpp]  view plain copy
  1. pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,  
  2.                   unsigned long size, pgprot_t vma_prot)  
  3. {  
  4.     if (ppc_md.phys_mem_access_prot)  
  5.         return ppc_md.phys_mem_access_prot(file, pfn, size, vma_prot);  
  6.   
  7.     if (!page_is_ram(pfn))  
  8.         vma_prot = pgprot_noncached(vma_prot);  
  9.   
  10.     return vma_prot;  
  11. }  
如果有平台实现的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实现如下:
[cpp]  view plain copy
  1. #ifdef CONFIG_DEVKMEM  
  2. static int mmap_kmem(struct file *file, struct vm_area_struct *vma)  
  3. {  
  4.     unsigned long pfn;  
  5.   
  6.     /* Turn a kernel-virtual address into a physical page frame */  
  7.     pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT;  
  8.   
  9.     /* 
  10.      * RED-PEN: on some architectures there is more mapped memory than 
  11.      * available in mem_map which pfn_valid checks for. Perhaps should add a 
  12.      * new macro here. 
  13.      * 
  14.      * RED-PEN: vmalloc is not supported right now. 
  15.      */  
  16.     if (!pfn_valid(pfn))  
  17.         return -EIO;  
  18.   
  19.     vma->vm_pgoff = pfn;  
  20.     return mmap_mem(file, vma);  
  21. }  
  22. #endif  
引起我注意的是__pa,完成内核虚拟地址到物理地址的转换,最后调用mmap_mem,简单一看kmem的确是映射的内核虚拟地址。
但是搞清楚__pa的实现,我就不这么认为了。以powerpc为例,在arch/powerpc/include/asm/page.h,定义如下:

[cpp]  view plain copy
  1. #define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET))  
  2. #define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET)  
  3. ....  
  4. #define VIRT_PHYS_OFFSET (KERNELBASE - PHYSICAL_START)  
内核中定义了4个变量来表示内核一些基本的物理地址和虚拟地址,如下:
KERNELBASE     内核的起始虚拟地址,我的是0xc0000000
PAGE_OFFSET    低端内存的起始虚拟地址,一般是0xc0000000
PHYSICAL_START 内核的起始物理地址,我的是0x80000000
MEMORY_START   低端内存的起始物理地址,我的是0x80000000

内核在启动过程中对于lowmem的静态映射,就是以上述的物理地址和虚拟地址的差值进行线性映射的。
所以__pa __va转换的是线性映射的内存部分,也就是lowmem。
所以kmem映射的是lowmem,如果我的cmdline参数中mem=512M,这就意味着通过kmem的mmap我最多可以访问内核地址空间开始的512M内存。
对于超过lowmem范围,访问highmem,如果使用__pa访问,由于highmem是动态映射的,其映射关系不是线性的那么简单了,根据__pa获取的物理地址与我们想要的内核虚拟地址是不对应的。

这篇关于/dev/mem的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1027400

相关文章

v0.dev快速开发

探索v0.dev:次世代开发者之利器 今之技艺日新月异,开发者之工具亦随之进步不辍。v0.dev者,新兴之开发者利器也,迅速引起众多开发者之瞩目。本文将引汝探究v0.dev之基本功能与优势,助汝速速上手,提升开发之效率。 何谓v0.dev? v0.dev者,现代化之开发者工具也,旨在简化并加速软件开发之过程。其集多种功能于一体,助开发者高效编写、测试及部署代码。无论汝为前端开发者、后端开发者

perf 基础 -- perf ext_mem_req

perf ext_mem_req ext_mem_req 是 perf 工具事件列表中的一项,主要用于追踪处理器对外部内存的访问请求。它在以下几种情况下会触发: 数据缓存未命中:当处理器访问的数据未命中缓存时,处理器会向外部内存发送请求,加载所需的数据。指令缓存未命中:处理器在获取指令时,如果指令未命中缓存,也会触发外部内存请求,加载所需的指令。内存控制器交互:ext_mem_req 还用于衡

LVM 'Can’t open /dev/sdb1 exclusively. Mounted filesystem?' Problem

在将几块盘做LVM时,遇到一个之前都没遇到过的问题: root@ubuntu:~# pvcreate /dev/sdc1Can't open /dev/sdc1 exclusively. Mounted filesystem? 首先第一反应就是查看这个分区是否已经在使用了,但是没有。 查看硬盘的一些信息: root@ubuntu:~# cat /proc/partitionsmajo

Linux 挂载须知: mount 一些小问题 mount: no medium found on /dev/sr0

我属于window+VM+ubuntu的配置,有时需要把windows上的文件,挂载到虚拟机上去。然后每次使用命令的时候会出现如下的问题:                其实解决问题很简单,但是由于我自己的粗心而花费了很长的时间:    这个时候一定要确认,虚拟机的设置处是否勾选了   具体如下图所示:                 然后按照  如下的命令:   mount

清华MEM作业-利用管理运筹学的分析工具slover求解最优解的实现 及 通过使用文件或者套节字来识别进程的fuser命令

一、清华MEM作业-利用管理运筹学的分析工具slover求解最优解的实现         最近又接触了一些线性求解的问题,以前主要都是在高中数学里接触到,都是使用笔算,最后通过一些函数式得出最小或者最大值,最近的研究生学业上接触到了一个Excel solver分析工具,对这种线性求最优解的问题感觉使用起来真是得心应手。在使用这个工具前,EXCEL里需要先装上solver工具,装起来很也简单,网上

Ubuntu上安装libdc1394-22-dev出现无法定位安装包的解决办法

一、libdc1394-22-dev介绍       libdc1394-22-dev 是一个开发库,用于与IEEE 1394 (FireWire)摄像头进行交互。具体来说,它是 libdc1394 的开发版本,提供了开发者头文件和链接库,方便在应用程序中集成对基于 IEEE 1394 标准的数码相机的支持。 主要功能: - IEEE 1394 (FireWire) 协议:这是一个支持高速数据

App使用Job中遇到的WAIT DEV_NOT_DOZING的解决方案

摘要: 由于原生Job机制会使用Doze的白名单,故遇到WAIT:DEV_NOT_DOZING现象,一般配置为Whitelist user apps名单即可解决问题 Doze名单类型 配置对象 配置方法 影响 Whitelist user apps 第三方应用 1.Adb shell 命令:adb shell dumpsys deviceidle whitelist +com.t

Dev C++:简单步骤下载与安装指南

1. 前言 在当今这个数字化时代,编程已成为连接创意与技术的桥梁,它不仅推动着科技的进步,也深刻地改变着我们的生活方式。对于初学者而言,选择一款合适且易于上手的集成开发环境(IDE)是学习编程旅程中的重要一步。而Dev C++,作为一款轻量级且功能强大的C/C++编程工具,凭借其简洁的界面、丰富的功能以及良好的兼容性,成为了众多编程爱好者尤其是C/C++初学者的首选。 本指南旨在为广大编程爱好

/dev/null笔记

/dev/null      是个黑洞,进去的东西永远出不来,它是一个空设备文件    在脚本编程的时候经常会用它(空设备文件)来屏蔽标准或错误输出(stdout 或 stderr)。 >      重定向输出,覆盖文件 >>     重定向输出,不覆盖文件 &      等同于 UNIX有几种输入输出流,与数字的对应关系如下:      0-标准输入流(stdin)

git合并dev分支的多个commit到master分支

这里主要使用新建指定位置分支,rebase,以及删除新建分支,三步操作进行 1.新建到合并的commit处的branch git checkout -b newbranch 74b73d6a 后面跟得的是commit_id,这个commit是最后需要合并到的那个[ , commit_id] 2.rebase到master git checkout mastergit rebase ne