通过devmem访问物理地址
程序员文章站
2022-08-17 17:34:08
[TOC] 1.写在前面 最近在调试时需要在用户层访问物理内存,发现应用层可以使用 devmem 工具访问物理地址。查看源码,实际上是对 /dev/mem 操作,通过 mmap 可以将物理地址映射到用户空间的虚拟地址上,在用户空间完成对设备寄存器的读写。藉由此原因,想深入理解下 mmap 的具体实现 ......
目录
1.写在前面
最近在调试时需要在用户层访问物理内存,发现应用层可以使用devmem工具访问物理地址。查看源码,实际上是对/dev/mem操作,通过mmap可以将物理地址映射到用户空间的虚拟地址上,在用户空间完成对设备寄存器的读写。藉由此原因,想深入理解下mmap的具体实现。
2.devmem使用
devmem的配置,可以在busybox的杂项中找到。
config_user_busybox_devmem: devmem is a small program that reads and writes from physical memory using /dev/mem. symbol: user_busybox_devmem [=y] prompt: devmem defined at ../user/busybox/busybox-1.23.2/miscutils/kconfig:216 depends on: user_busybox_busybox location: -> busybox (user_busybox_busybox [=y]) -> miscellaneous utilities
# busybox devmem busybox v1.23.2 (2018-08-02 11:08:33 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
参数 | 详细说明 |
---|---|
address | 需要进行读写访问的物理地址 |
width | 访问数据类型 |
value | 如果是读操作省略;如果是写操作,表示需要写入的数据 |
基本测试用法
# devmem 0x44e07134 16 0xffef # devmem 0x44e07134 32 0xffffffef # devmem 0x44e07134 8 0xef
3.应用层
接口定义如下:
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
详细参数如下:
参数 | 详细说明 |
---|---|
addr | 需要映射的虚拟内存地址;如果为null,系统会自动选定。映射成功后返回该地址 |
length | 需要映射多大的数据量 |
prot | 描述映射区域内存保护方式,包括:prot_exec、prot_read、prot_write、prot_none. |
flags | 描述映射区域的特性,比如是否对其他进程共享,是否建立匿名映射,是否创建私有的cow. |
fd | 要映射到内存中的文件描述符 |
offset | 文件映射的偏移量 |
以devmem的实现为例,
如果argv[3]存在,需要映射读写权限;如果不存在,只需要映射读权限。
map_base = mmap(null, mapped_size, argv[3] ? (prot_read | prot_write) : prot_read, map_shared, fd, target & ~(off_t)(page_size - 1));
4.内核层
因篇幅有限,这里不在表述glibc、系统调用的关系,直接查找系统调用的代码实现。
arch/arm/include/uapi/asm/unistd.h
#define __nr_oabi_syscall_base 0x900000 #if defined(__thumb__) || defined(__arm_eabi__) #define __nr_syscall_base 0 #else #define __nr_syscall_base __nr_oabi_syscall_base #endif #define __nr_mmap (__nr_syscall_base+ 90) #define __nr_munmap (__nr_syscall_base+ 91) #define __nr_mmap2 (__nr_syscall_base+192)
arch/arm/kernel/entry-common.s
/*============================================================================= * swi handler *----------------------------------------------------------------------------- */ .align 5 entry(vector_swi) #ifdef config_cpu_v7m v7m_exception_entry #else sub sp, sp, #s_frame_size stmia sp, {r0 - r12} @ calling r0 - r12 arm( add r8, sp, #s_pc ) arm( stmdb r8, {sp, lr}^ ) @ calling sp, lr thumb( mov r8, sp ) thumb( store_user_sp_lr r8, r10, s_sp ) @ calling sp, lr mrs r8, spsr @ called from non-fiq mode, so ok. str lr, [sp, #s_pc] @ save calling pc str r8, [sp, #s_psr] @ save cpsr str r0, [sp, #s_old_r0] @ save old_r0 #endif zero_fp #ifdef config_alignment_trap ldr ip, __cr_alignment ldr ip, [ip] mcr p15, 0, ip, c1, c0 @ update control register #endif enable_irq ...
/* * note: off_4k (r5) is always units of 4k. if we can't do the requested * offset, we return einval. */ sys_mmap2: #if page_shift > 12 tst r5, #pgoff_mask moveq r5, r5, lsr #page_shift - 12 streq r5, [sp, #4] beq sys_mmap_pgoff mov r0, #-einval mov pc, lr #else str r5, [sp, #4] b sys_mmap_pgoff #endif endproc(sys_mmap2)
arch/arm/kernel/calls.s
/* 90 */ call(obsolete(sys_old_mmap)) /* used by libc4 */ call(sys_munmap) ... /* 190 */ call(sys_vfork) call(sys_getrlimit) call(sys_mmap2)
include/linux/syscalls.h
asmlinkage long sys_mmap_pgoff(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff);
搜索mmap_pgoff函数定义,位于mm/mmap.c,省略一些我们不太关心的代码。
syscall_define6(mmap_pgoff, unsigned long, addr, unsigned long, len, unsigned long, prot, unsigned long, flags, unsigned long, fd, unsigned long, pgoff) { struct file *file = null; unsigned long retval = -ebadf; if (!(flags & map_anonymous)) { audit_mmap_fd(fd, flags); file = fget(fd); if (!file) goto out; if (is_file_hugepages(file)) len = align(len, huge_page_size(hstate_file(file))); retval = -einval; if (unlikely(flags & map_hugetlb && !is_file_hugepages(file))) goto out_fput; } ... flags &= ~(map_executable | map_denywrite); retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff); out_fput: if (file) fput(file); out: return retval; }
mm/util.c
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long pgoff) { unsigned long ret; struct mm_struct *mm = current->mm; unsigned long populate; ret = security_mmap_file(file, prot, flag); if (!ret) { down_write(&mm->mmap_sem); ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff, &populate); up_write(&mm->mmap_sem); if (populate) mm_populate(ret, populate); } return ret; }
vm_area_struct结构用来描述进程的虚拟内存区域,和进程的内存描述符mm_struct关联,通过链表和红黑树进行管理。
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff, unsigned long *populate) { struct mm_struct * mm = current->mm; vm_flags_t vm_flags; *populate = 0; //搜索进程地址空间,查找一个可以使用的线性地址区间,len指定区间的长度,非空addr参数指定从哪个地址开始进行查找 addr = get_unmapped_area(file, addr, len, pgoff, flags); vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags | vm_mayread | vm_maywrite | vm_mayexec; //file指针不为空,建立从文件到虚拟空间的映射,根据flags标志设定访问权限。 if (file) { struct inode *inode = file_inode(file); switch (flags & map_type) { case map_shared: vm_flags |= vm_shared | vm_mayshare; break; ... } else { //file指针为空,仅创建虚拟空间,不做映射。 switch (flags & map_type) { case map_shared: pgoff = 0; vm_flags |= vm_shared | vm_mayshare; break; case map_private: pgoff = addr >> page_shift; break; } //创建虚拟空间,并进行映射。 addr = mmap_region(file, addr, len, vm_flags, pgoff); return addr; }
unsigned long mmap_region(struct file *file, unsigned long addr, unsigned long len, vm_flags_t vm_flags, unsigned long pgoff) { ... //检查是否需要对该虚拟空间进行扩容 if (!may_expand_vm(mm, len >> page_shift)) { unsigned long nr_pages; /* * map_fixed may remove pages of mappings that intersects with * requested mapping. account for the pages it would unmap. */ if (!(vm_flags & map_fixed)) return -enomem; nr_pages = count_vma_pages_range(mm, addr, addr + len); if (!may_expand_vm(mm, (len >> page_shift) - nr_pages)) return -enomem; } //扫描当前进程地址空间的vm_area_struct结构相关的红黑树,确定线性区域的位置,如果找到一个区域,说明addr所在的虚拟区间已经被使用,表示已经被映射;因此需要调用do_munmap把这个区域从进程地址空间中撤销。 munmap_back: if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) { if (do_munmap(mm, addr, len)) return -enomem; goto munmap_back; } vma = vma_merge(mm, prev, addr, addr + len, vm_flags, null, file, pgoff, null); if (vma) goto out; //分配映射虚拟空间 vma = kmem_cache_zalloc(vm_area_cachep, gfp_kernel); if (!vma) { error = -enomem; goto unacct_error; } vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = vm_flags; vma->vm_page_prot = vm_get_page_prot(vm_flags); vma->vm_pgoff = pgoff; init_list_head(&vma->anon_vma_chain); if (file) { if (vm_flags & vm_denywrite) { error = deny_write_access(file); if (error) goto free_vma; } vma->vm_file = get_file(file); error = file->f_op->mmap(file, vma); if (error) goto unmap_and_free_vma; /* can addr have changed?? * * answer: yes, several device drivers can do it in their * f_op->mmap method. -davem * bug: if addr is changed, prev, rb_link, rb_parent should * be updated for vma_link() */ warn_on_once(addr != vma->vm_start); addr = vma->vm_start; vm_flags = vma->vm_flags; } else if (vm_flags & vm_shared) { error = shmem_zero_setup(vma); if (error) goto free_vma; } ... }
mmap_region函数实现中的file->f_op->mmap(file, vma),对应mmap_mem,位于/drivers/char/mem.c,代码如下:
static const struct file_operations mem_fops = { .llseek = memory_lseek, .read = read_mem, .write = write_mem, .mmap = mmap_mem, .open = open_mem, .get_unmapped_area = get_unmapped_area_mem, }; 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; }
remap_pfn_range函数建立物理地址与虚拟地址页表。其中vm_pgoff代表要映射的物理地址,vm_page_prot代表该页的权限。这些参数和mmap的参数相互对应,现在就可以通过应用层访问物理地址了。