蜗窝科技

Linux下“用户空间修改设备寄存器或者物理内存”的实现

wowo · · 软件开发 · 阅读 28191 · 2015年5月存档

底层开发过程中,经常需要在终端查看或者修改设备寄存器的值,busybox有一个工具—-devmem,可用于读取或者修改物理寄存器的值,非常方便:

busybox devmem

BusyBox v1.14.3 (2014-11-05 16:48:48 CST) multi-call binary

Usage: devmem ADDRESS [WIDTH [VALUE]]

Read/write from physical address

ADDRESS Address to act upon

WIDTH Width (8/16/…)

VALUE Data to be written

但它有一个不足的地方:不能连续操作物理内存(虽然这很危险,但在显示相关的调试中,如果能向指定的物理内存加载一个图片,或者dump指定物理内存的内容,还是很方便的),因此我重新写了一些代码,实现如下功能:

  1. 利用/dev/mem,编写一个library,将物理地址访问的接口抽象出来。

  2. 基于该library,实现devmd工具,可以显示指定的某个或者某些物理地址的内容。

  3. 基于该library,实现devmcpy工具,可以将指定地址的内容,copy到另一个地址。类似memcpy。

  4. 基于该library,实现devmset工具,可以将指定地址设置为某一个value。类似memset。

  5. 基于该library,实现devmload和devmsave工具,将指定文件的内容加载到指定物理地址,以及从指定物理地址dump数据到指定的文件。

实现的过程比较简单,包括基本的文件open、read、write、mmap等操作,这里就不详细分析代码了,贴一下README文件,具体可参考github中的source code:https://github.com/wowotech/wowolib/tree/master/devmem。

Purpose:

A simple library to operate physical address in usersapce,
which is based on linux "/dev/mem".
Also, some useful tools is provided, including:
--devmd, display PA's value.
--devmcpy, copy from src PA to dst PA.
--devmset, set PA to special value.
--devmload, load file to PA.
--devmsave, save PA to file.
Uasge:
--devmem library
	Include devmem.h, and put devmem.c together with your souece code,
	call the APIs you need, and build them.
--devmd 
	./devmd { address } [type] [count]
	address: Address to act upon
	width:	Width (8/16/...), default is 32
	count:	Data count to be read, default is 1
--devmcpy 
	./devmcpy {src_addr} {dst_addr} {count} [width]
	src_addr:	source address
	dst_addr:	destination address
	count:		copy count, default is 1
	width:		width, 8/16/32/64..., default is 32
--devmset 
	./devmset {address} {value} {count} [width]
	address:	Address to act upon
	value:		Value to write
	count:		Set counts
	width:		width, 8/16/32/64..., default is 32
--devmload 
	./devmload {addr} {filename}
	addr:		destination physical address
	filename:	file name will load from
--devmsave 
	./devmsave {addr} {len} {filename}
	addr:	physical address need to save
	len:	length in byte
	filename: file save to
Author:
wowo<www.wowotech.net>
Date:
2015-05-15

标签:devmem

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(20 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • printk

    很好很强大!方便我调试bug了~
  • RobinHsiang

    #define ASSERT() \ do { \ PRINT("ASSERT: %s %s %d", \ __FILE__, __FUNCTION__, __LINE__); \ while (1); \ } while (0) 有时候不能用goto可以这样使用do{} while(0),但内核中定义宏很多时候也用do{} while(0),这样主要是为了编译器对宏的展开么?
    • RobinHsiang

      @RobinHsiang:呵呵,刚刚搜索到答案了,以前没多想这问题。 Wowo真是深受linux内核的熏陶啊~~~
      • wowo

        @RobinHsiang:主要是让宏定义规范一些,我也没想那么多,看别人用自己就用了。呵呵~~
  • descent

    請問 open("/dev/mem", O_RDWR | O_SYNC)) 需要使用 O_SYNC, 是為了不要有 cache 的干擾嗎?
    • linuxer

      @descent:首先声明:我的回答是基于linux 4.1.10下的ARM64代码的,其他的情况我没有去看代码,估计类似。 对"/dev/mem"这个设备的使用一般包括两步: (1)open并获取一个fd (2)通过mmap将该fd mapping到该进程的一段虚拟地址空间上去,当程序实际访问这段虚拟地址空间的时候,就象访问了fd指向的那个对象。对于"/dev/mem"的fd而言,其指向了系统中的物理地址空间 如何建立进程虚拟地址空间(vma)和"/dev/mem"设备fd之间的mapping是通过其驱动file operations数据结构中的mmap成员函数来完成的,在该函数中会建立页表,从而控制cache write buffer等的设定。该函数的代码如下: 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 */ if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, size, vma->vm_page_prot)) { return -EAGAIN; } return 0; } 其中,phys_mem_access_prot函数定义了页表中的memory attribute以及memory type的信息,代码如下: pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn, unsigned long size, pgprot_t vma_prot) { if (!pfn_valid(pfn)) return pgprot_noncached(vma_prot); else if (file->f_flags & O_SYNC) return pgprot_writecombine(vma_prot); return vma_prot; } 对于ARM64的处理器而言,其物理地址空间有两种: (1)memory (2)device 从上面的代码可以看到,实际上O_SYNC这个flag只会在你要mapping的物理地址是memroy的时候才是有效的,如果你要mapping的物理地址本身就是IO(这也是/dev/mem的大多数的应用场合),那么页表项中的内容是pgprot_noncached,即: #define pgprot_noncached(prot) \ __pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRnE) | PTE_PXN | PTE_UXN) 这时候,无论是否有O_SYNC,页表的属性都是device类型的,而对于memory type是device类型的地址影响,uncache是必须的。
      • anchen

        @linuxer:您好!我想请教一个和O_SYNC有关的具体问题。 我的系统是Altera的SoC,我要在用户空间为DMAC(pl330)更新其微码,结果发现DMA第一次运行正常,第二次运行依然执行前一次操作(虽然微码中的源和目的我都更新过了)。现在的结论是,因为这个SoC的架构,DMAC总是通过ACP访问L2 cache,意外着,DMA第一次会将微码锁存在L2 cache中,而用户空间在更新微码的过程中,没有处理L2 cache和memory的一致性,造成DMA第二次访问时,L2 cache依然命中,所以再次运行第一次的操作。我考虑过如下O_SYNC的使用,但也没有成功,不知道这样使用对不对? FdforOcm = open("/dev/mem", O_RDWR | O_SYNC); OcmAddr = mmap(NULL, 0x40000, PROT_READ|PROT_WRITE, MAP_SHARED, FdforOcm, 0xFFE00000); //这个返回的地址用来给程序更新DMA微码,使用O_SYNC,意味着一定会写入cache,然后更新memory吗? FdforDMA = open("/dev/mem", O_RDWR); DMAAddr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, FdforDMA, 0xFFDA0000); //这个返回的地址用来给程序访问DMA的寄存器
      • anchen

        @linuxer:您好!我想请教一个和O_SYNC有关的具体问题。 我的系统是Altera的SoC,我要在用户空间为DMAC(pl330)更新其微码,结果发现DMA第一次运行正常,第二次运行依然执行前一次操作(虽然微码中的源和目的我都更新过了)。现在的结论是,因为这个SoC的架构,DMAC总是通过ACP访问L2 cache,意外着,DMA第一次会将微码锁存在L2 cache中,而用户空间在更新微码的过程中,没有处理L2 cache和memory的一致性,造成DMA第二次访问时,L2 cache依然命中,所以再次运行第一次的操作。我考虑过如下O_SYNC的使用,但也没有成功,不知道这样使用对不对? FdforOcm = open("/dev/mem", O_RDWR | O_SYNC); OcmAddr = mmap(NULL, 0x40000, PROT_READ|PROT_WRITE, MAP_SHARED, FdforOcm, 0xFFE00000); //这个返回的地址用来给程序更新DMA微码,使用O_SYNC,意味着一定会写入cache,然后更新memory吗? FdforDMA = open("/dev/mem", O_RDWR); DMAAddr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, FdforDMA, 0xFFDA0000); //这个返回的地址用来给程序访问DMA的寄存器
        • linuxer

          @anchen:如果在open /dev/mem这个device node的时候有O_SYNC,那么,随后的mmap创建的映射将是non cache的(对于ARM64的平台,其他的平台我没有看,我估计是类似的),也就是说,你通过OcmAddr地址更新DMA微码的时候,应该不会通过cache,而直接进入最终的设备地址。
          • anchen

            @linuxer:感谢您的回复!看来我理解错了,但其实我两个都试了,无论是加还是不加 O_SYNC,结果似乎都是直接写入最终的设备地址,而没有同时更新到cache。有没有什么办法可以满足我的要求,写入最终设备的同时,更新到cache?或者说,如何在更新物理设备的同时,能够在用户空间,失效相对应的那部分cache?对现在的情况来说,我只要求物理memory和L2 cache能保持一致就可以了。
            • wowo

              @anchen:我觉得你的需求应该就是invalidate l2 cache? 以arm平台为例,其实就是一条写p15协处理器的指令(当然,需要一些内存屏障操作)。 arm平台为这个需求抽象出来了一系列的接口,头文件在:arch/arm/include/asm/outercache.h 可以参考kernel中某些CPU的实现,例如:arch/arm/mm/cache-feroceon-l2.c 当然,如果粗暴一些,你也可以自己写一个,开放给用户空间程序使用。 不知道还有没有其它好的方法?知道的同学也吱一声~~~
            • linuxer

              @anchen:你遇到的这个问题其实就是cache coherence的问题。根据你的描述,DMAC总是通过ACP访问L2 cache,而这就导致了你无论使用O_SYNC或者不使用O_SYNC都是错误的: 1、使用了O_SYNC,那么你的驱动在访问mmap返回的那些地址的时候是non cache的,也就是说,数据直接去到main memory,而不会操作L2 cache。对于DMAC,它的访问需要通过L2 cache,由于第一次访问的时候,L2是all invalidate的,因此数据访问都是cache miss的,需要从main memory加载数据并更新到L2 cache。后续,程序再次更新通过mmap返回的那些地址进行数据更新的时候,直达main memory,但是DMAC在访问的时候,L2 cache hit,从而不会访问main memory,从而导致运行失败。 2、不使用O_SYNC的时候,你的驱动在访问mmap返回的那些地址的时候是cache的,但是首先进入的是L1 cache(我猜测cache 策略是write back的),而不会操作L2 cache,因此,DMAC也不能从L2中获取正确的数据。 怎么破呢?我觉得首先是应该mapping成cache的,但是要辅以一些同步的手段和cache维护的指令。也就是说,当你的程序通过mmap返回的那些地址完成数据更新之后,要clean到POC,让系统中的所有的observer(CPU core和DMAC等等)看到一致性的数据。 BTW, invalidate操作不适合,我们这个场合需要的是将L1的数据write back到L2,invalidate是将cache中的数据设置为无效。另外,cache操作其实无法操作到指定的某个level的cache,只能是poc或者是pou。不过其实你没有详细描述你系统的memory hierarchy,因此上面我建议使用poc。
              • wowo

                @linuxer:为什么invalidate无效呢? 应用程序已经将微码更新到指定的memory中了,DMAC期望访问这个新的数据。 如果cache有效,则直接从cache中取出了旧的数据。 如果cache无效,DMAC将不得不从memory中从新读取数据(新的)。
              • linuxer

                @linuxer:wowo,我是这么想的: 1、程序的数据访问路径是CPU--->L1 cache--->L2 cache--->main meory 2、DMAC的数据访问路径是DMAC--->L2 cache--->main meory 程序写入的时候,首先进入L1 cache,硬件决定什么时候从L1进入L2以及main memory(除非程序手动去clean cache),因此DMAC无法从L2中得到正确的数据。当然,上面都是针对write back 类型的cache说的。
              • anchen

                @linuxer:真的很幸运!在这个平台上,获得这么清晰的建议(实在是楼主太强大了!)。 系统架构确实如你所述,CPU--->L1 cache--->L2 cache--->main memory。 我的经验不足,没能想到L1和L2的关系。 和你想法一样,我不打算invalidate L2 cache,这个不是最佳选择,也会引起效率的降低。关于poc或pou,我网上科普了下,能理解其概念。但具体怎么做? 如何在用户空间用代码去实现?我的应用都需要在用户空间去完成。 再次感谢,感谢楼主分享宝贵经验和知识!
              • anchen

                @linuxer:我的平台是dual core (Cortex A9)
              • anchen

                @linuxer:poc还是有点难懂。 https://community.arm.com/thread/8571 这里有些讨论,但是很难消化。 我其实有个疑问,L1 cache既然已经写入,那么它一定应该在某个时刻去同步L2和main memory,对吗?那这是一个什么样的时刻,或者有什么操作可以主动发起这个同步吗?
              • linuxer

                @linuxer:L1 cache既然已经写入,那么它一定应该在某个时刻去同步L2和main memory,对吗?那这是一个什么样的时刻,或者有什么操作可以主动发起这个同步吗? --------------------------- 基本上那个时刻是HW决定的。当L1 cache已经全部是valid的entry,当再次数据访问发生cache miss的时候,就需要从这些valid的entry中找出一个最近最少使用的那个entry把它踢出L1(具体算法是由硬件cache设计人员决定的),如果准备踢出的L1 entry是dirty的,那么就需要write back到L2 cache中。 ARM处理器提供了cache维护指令来进行clean cache的操作,具体可以参考ARM ARM
    • linuxer

      @descent:上面的回答稍显敷衍,主要是快下班了,着急着回家,现在老婆孩子睡了,可以安安静静的针对这个问题胡思乱考了,呵呵~~~ /dev/mem这个设备节点可以让用户空间建立页表并mapping到任意的系统中的物理地址上去,如果这个设备节点提供O_SYNC这种接口来控制映射的cache属性,那么这个接口是否是一个好的接口呢? 对于普通的memory而言,可以映射为non cached(例如DMA buffer,framebuffer等),也可以映射为cached(大部分的普通的data和heap上的memory都是cached),因此,通过O_SYNC来控制映射的cache属性是OK的,但是,对于IO memory而言,设置为cached真的适合吗?这可能会引入很多不必要的麻烦,因此,这种情况下,通过O_SYNC来控制映射的cache属性是徒增user的烦恼。而且,一旦提供了这样的接口,实际上用户空间的进程可以把多个虚拟地址映射到一个物理地址上去(建立多个页表,把不同的虚拟地址映射到同一个物理地址),并且指定不同的属性。软件这么任性,有没有和硬件商量呢? 这个问题转换成了cpu设计的问题了,即硬件如何处理同一个物理地址不同属性的访问,对于ARM64处理器而言,一个物理地址有memory type的设定(体现在页表中的描述符中的某些bit),要么是device,要么是memory。如果virtual_A地址被mapping到physical_A并通过页表设定为device,同时virtual_B地址也被mapping到physical_A并通过页表设定为memory,那么估计ARM处理的内心是崩溃的。ARM64处理器不愿意浪费多余的晶体管来处理这些破事,因此规定软件符合一定的规则,如果打破这个规则,那么cpu的执行逻辑是未知的。不同的cpu有不同的设计理念,因此phys_mem_access_prot是architecture specific的。ARM64的phys_mem_access_prot函数会根据硬件的要求,对页表进行的设定,确保符合处理器的要求。
      • descent

        @linuxer:謝謝你的回覆, 真的要看第0手資料才能知道這些事情, 我那時候 google 了老半天只得到個不太清楚的回答, 所以才會問問看, 沒想過你還真的為了回答我的問題, 親身去看程式碼, 真的專業。