当前位置: 移动技术网 > 科技>操作系统>Linux > 通过devmem访问物理地址

通过devmem访问物理地址

2018年10月22日  | 移动技术网科技  | 我要评论

目录

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的参数相互对应,现在就可以通过应用层访问物理地址了。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网