通過devmem訪問物理地址


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的參數相互對應,現在就可以通過應用層訪問物理地址了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM