欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

通过devmem访问物理地址

程序员文章站 2022-04-30 19:54:26
[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的参数相互对应,现在就可以通过应用层访问物理地址了。