天天看點

/dev/mem可沒那麼簡單【轉】

版權聲明:本文為部落客kerneler辛苦原創,未經允許不得轉載。

這幾天研究了下/dev/mem,發現功能很神奇,通過mmap可以将實體位址映射到使用者空間的虛拟位址上,在使用者空間完成對裝置寄存器的操作,于是上網搜了一些/dev/mem的資料。網上的說法也很統一,/dev/mem是實體記憶體的全映像,可以用來通路實體記憶體,一般用法是open("/dev/mem",O_RDWR|O_SYNC),接着就可以用mmap來通路實體記憶體以及外設的IO資源,這就是實作使用者空間驅動的一種方法。

使用者空間驅動聽起來很酷,但是對于/dev/mem,我覺得沒那麼簡單,有2個地方引起我的懷疑:

(1)網上資料都說/dev/mem是實體記憶體的全鏡像,這個概念很含糊,/dev/mem到底可以完成哪些位址的虛實映射?

(2)/dev/mem看似很強大,但是這也太危險了,黑客完全可以利用/dev/mem對kernel代碼以及IO進行一系列的非法操作,後果不可預測,難道核心開發者們沒有意識到這點嗎?

網上資料說法都很泛泛,隻對mem裝置的使用進行說明,沒有對這些問題進行深究。要搞清這一點,我覺得還是從/dev/mem驅動開始下手。

參考核心版本:3.4.55 

參考平台:powerpc/arm

mem驅動在drivers/char/mem.c,mmap是系統調用,産生軟中斷進入核心後調用sys_mmap,最終會調用到mem驅動的mmap實作函數。

來看下mem.c中的mmap實作:

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))  

    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 and VM_RESERVED */  

    if (remap_pfn_range(vma,  

                vma->vm_start,  

                vma->vm_pgoff,  

                size,  

                vma->vm_page_prot)) {  

        return -EAGAIN;  

    }  

    return 0;  

}  

vma是核心記憶體管理很重要的一個結構體,

其結構成員中start end代表要映射到的使用者空間虛拟位址範圍,使用者空間的動态映射是以PAGE_SIZE也就是4K為一頁,

vma_pgoff是要映射的實體位址,vma_page_prot代表該頁的權限。

這些成員的指派是在調用具體驅動的mmap實作函數之前,在sys_mmap中進行的。

在mmap_mem最後調用remap_pfn_range,該函數完成指定實體位址與使用者空間虛拟位址頁表的建立。

remap_pfn_range參數中vma->vm_pgoff即代表要映射的實體位址,并沒有範圍限制僅能夠操作記憶體。

mmap系統調用的函數定義如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr指定要映射到的虛拟位址,寫NULL則有sys_mmap來配置設定該虛拟位址。

mmap參數與mem_mmap參數對應關系如下:

prot      ===> vma->vma_page_prot

offset    ===> vma->vma_pgoff

length    ===> size

從剛才分析的mem_mmap流程來看,可以得出一個簡單的結論:

mem_mmap可以映射整個處理器的位址空間,而不單單是記憶體。這裡要說明的是,位址空間不等于記憶體空間。站在處理器角度看,位址空間指處理器總線上的所有可尋址空間,除了記憶體,還有外設的IO空間,以及其他總線映射過來的mem(如PCI)

我的了解,mem_mmap完全可以映射0-0xffffffff的所有實體位址(填TLB頁表完成映射),但前提是保證該實體位址是真實有效的,也就是處理器通路該總線實體位址可以擷取有效資料。

是以現在看來mmap /dev/mem,隻要确定我們處理器的位址空間分布,就可以将我們需要的位址映射到使用者空間進行操作。

如果位址不是一個有效實體位址(處理器位址空間分布中該位址沒用),mmap建立該實體位址與使用者空間虛拟位址的映射,填TLB,CPU經過TLB翻譯後去通路該不存在的實體位址通路就有可能導緻CPU挂掉。

這也就解釋了我第一個疑問,但是kernel的安全機制不會允許使用者這麼肆無忌憚的操作。接着來看remap_pfn_range之前mmap_mem如何進行防護。

首先是valid_mmap_phys_addr_range,檢查該實體位址是否是一個有效的mmap位址,如果平台定義了ARCH_HAS_VALID_PHYS_ADDR_RANGE則會實作該函數,

arm中定義并實作了該函數,在arch/arm/mm/mmap.c中,如下:

/* 

 * We don't use supersection mappings for mmap() on /dev/mem, which 

 * means that we can't map the memory area above the 4G barrier into 

 * userspace. 

 */  

int valid_mmap_phys_addr_range(unsigned long pfn, size_t size)  

    return !(pfn + (size >> PAGE_SHIFT) > 0x00100000);  

該函數确定mmap的範圍是否超過4G,超過4G則為無效實體位址,這種情況使用者空間一般不會出現。

而對于powerpc,平台沒有定義ARCH_HAS_VALID_PHYS_ADDR_RANGE,是以valid_mmap_phys_addr_range在mem.c中定義為空函數,傳回1 表示該實體位址一直有效。

實體位址有效,不會傳回-EINVAL,繼續往下走。

接下來是private_mapping_ok,對于有MMU的CPU,實作如下:

static inline int private_mapping_ok(struct vm_area_struct *vma)  

    return 1;  

MMU的權限管理可以支援私有映射,是以該函數一直成功。

接下來是一個最為關鍵的檢查函數range_is_allowed,定義如下:

#ifdef CONFIG_STRICT_DEVMEM  

static inline int range_is_allowed(unsigned long pfn, unsigned long size)  

    u64 from = ((u64)pfn) << PAGE_SHIFT;  

    u64 to = from + size;  

    u64 cursor = from;  

    while (cursor < to) {  

        if (!devmem_is_allowed(pfn)) {  

            printk(KERN_INFO  

        "Program %s tried to access /dev/mem between %Lx->%Lx.\n",  

                current->comm, from, to);  

            return 0;  

        }  

        cursor += PAGE_SIZE;  

        pfn++;  

#else  

#endif  

可以看出如果不打開CONFIG_STRICT_DEVMEM,range_is_allowed是傳回1,表示該實體位址範圍是被允許的。檢視kconfig檔案(在相應平台目錄下,如arch/arm/Kconfig.debug中)找到CONFIG_STRICT_DEVMEM說明如下

config STRICT_DEVMEM  

    def_bool y  

    prompt "Filter access to /dev/mem"  

    help  

      This option restricts access to /dev/mem.  If this option is  

      disabled, you allow userspace access to all memory, including  

      kernel and userspace memory. Accidental memory access is likely  

      to be disastrous.  

      Memory access is required for experts who want to debug the kernel.  

      If you are unsure, say Y.  

該選項menuconfig時在kernel hacking目錄下。

根據說明可以了解,CONFIG_STRICT_DEVMEM是嚴格的對/dev/mem通路檢查,如果關掉該選項,使用者就可以通過mem裝置通路所有位址空間(根據對我提出的第一個問題了解,這裡memory應該了解為位址空間)。該選項對于調試核心有幫助。

如果打開該選項,核心就會對mem裝置通路加以檢查,檢查函數就是range_is_allowed。

range_is_allowed函數對要檢查的實體位址範圍以4K頁為機關,一頁一頁的調用devmem_is_allowed,如果不允許,則會進行列印提示,并傳回0,表示該實體位址範圍不被允許。

來看devmem_is_allowed.該函數是平台相關函數,不過arm跟powerpc的實作相差不大,以arm的實作為例。在arch/arm/mm/mmap.c中。

 * devmem_is_allowed() checks to see if /dev/mem access to a certain 

 * address is valid. The argument is a physical page number. 

 * We mimic x86 here by disallowing access to system RAM as well as 

 * device-exclusive MMIO regions. This effectively disable read()/write() 

 * on /dev/mem. 

int devmem_is_allowed(unsigned long pfn)  

    if (iomem_is_exclusive(pfn << PAGE_SHIFT))  

        return 0;  

    if (!page_is_ram(pfn))  

        return 1;  

首先iomem_is_exclusive檢查該實體位址是否被獨占保留,實作如下:

static int strict_iomem_checks = 1;  

static int strict_iomem_checks;  

 * check if an address is reserved in the iomem resource tree 

 * returns 1 if reserved, 0 if not reserved. 

int iomem_is_exclusive(u64 addr)  

    struct resource *p = &iomem_resource;  

    int err = 0;  

    loff_t l;  

    int size = PAGE_SIZE;  

    if (!strict_iomem_checks)  

    addr = addr & PAGE_MASK;  

    read_lock(&resource_lock);  

    for (p = p->child; p ; p = r_next(NULL, p, &l)) {  

        /* 

         * We can probably skip the resources without 

         * IORESOURCE_IO attribute? 

         */  

        if (p->start >= addr + size)  

            break;  

        if (p->end < addr)  

            continue;  

        if (p->flags & IORESOURCE_BUSY &&  

             p->flags & IORESOURCE_EXCLUSIVE) {  

            err = 1;  

    read_unlock(&resource_lock);  

    return err;  

如果打開了CONFIG_STRICT_DEVMEM,iomem_is_exclusive周遊iomem_resource連結清單,檢視要檢查的實體位址所在resource的flags,如果是bug或者exclusive,則傳回1,表明該實體位址是獨占保留的。

據我了解,iomem_resource是來表征核心iomem資源的連結清單。

對于外設的IO資源,kernel中使用platform device機制來注冊平台裝置(platform_device_register)時調用insert_resource将該裝置相應的io資源插入到iomem_resource連結清單中。

如果我要對某外設的IO資源進行保護,防止使用者空間通路,可以将其resource的flags置位exclusive即可。

不過我檢視我平台支援包裡的所有platform device的resource,flags都沒有置位exclusive或者busy。如果我映射的實體位址範圍是外設的IO,檢查可以通過。

對于記憶體的mem資源,如何注冊到iomem_resource連結清單中,核心代碼中我還沒找到具體的位置,不過iomem在proc下有相應的表征檔案,可以cat /proc/iomem。

是以這裡iomem_is_exclusive檢查一般是通過的,接下來看page_is_ram,看devmem_is_range的邏輯,如果位址是ram位址,則該位址不被允許。page_is_ram也是平台函數,檢視powerpc的實作如下。

int page_is_ram(unsigned long pfn)  

#ifndef CONFIG_PPC64    /* XXX for now */  

    return pfn < max_pfn;  

    unsigned long paddr = (pfn << PAGE_SHIFT);  

    struct memblock_region *reg;  

    for_each_memblock(memory, reg)  

        if (paddr >= reg->base && paddr < (reg->base + reg->size))  

            return 1;  

max_pfn指派在在do_init_bootmem中,如下.  

void __init do_init_bootmem(void)  

    unsigned long start, bootmap_pages;  

    unsigned long total_pages;  

    int boot_mapsize;  

    max_low_pfn = max_pfn = memblock_end_of_DRAM() >> PAGE_SHIFT;  

    total_pages = (memblock_end_of_DRAM() - memstart_addr) >> PAGE_SHIFT;  

max_pfn代表了核心lowmem的頁個數,lowmem在核心下靜态線性映射,系統啟動之初完成映射之後不會改動,讀寫效率高,核心代碼都是跑在lowmem。

lowmem大小我們可以通過cmdline的“mem=”來指定。

這裡就明白了如果要映射的實體位址在lowmem範圍内,也是不允許被映射的。

這樣range_is_allowed就分析完了,exclusive的iomem以及lowmem範圍内的實體位址是不允許被映射的。

接下來phys_mem_access_prot_allowed實作為空傳回1,沒有影響。

phys_mem_access_prot确定我們映射頁的權限,該函數也是平台函數,以powerpc實作為例,如下:

pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,  

                  unsigned long size, pgprot_t vma_prot)  

    if (ppc_md.phys_mem_access_prot)  

        return ppc_md.phys_mem_access_prot(file, pfn, size, vma_prot);  

        vma_prot = pgprot_noncached(vma_prot);  

    return vma_prot;  

如果有平台實作的phys_mem_access_prot,則調用之。如果沒有,對于不是lowmem範圍内的實體位址,權限設定為uncached。

以上的檢查完畢,最後調用remap_pfn_range完成頁表設定。

是以如果打開CONFIG_STRICT_DEVMEM,mem驅動會對mmap要映射的實體位址進行範圍和位置的檢查然後才進行映射,檢查條件如下:

(1)映射範圍不能超過4G。

(2)該實體位址所在iomem不能exclusive.

(3)該實體位址不能處在lowmem中。

是以說對于網上給出的各種利用/dev/mem來操作記憶體以及寄存器的文章,如果操作範圍在上述3個條件内,核心必須關閉CONFIG_STRICT_DEVMEM才行。

這樣對于mem裝置我的2個疑問算是解決了。檢視mem.c時我還看到了另外一個有趣的裝置kmem,這個裝置mmap的是哪裡的位址,網上的說法是核心虛拟位址,這個說法我不以為然,這裡記錄下我的想法。

如果核心打開CONFIG_KMEM,則會建立kmem裝置,它與mem裝置主要差别在mmap的實作上,kmem的mmap實作如下:

#ifdef CONFIG_DEVKMEM  

static int mmap_kmem(struct file *file, struct vm_area_struct *vma)  

    unsigned long pfn;  

    /* Turn a kernel-virtual address into a physical page frame */  

    pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT;  

    /* 

     * RED-PEN: on some architectures there is more mapped memory than 

     * available in mem_map which pfn_valid checks for. Perhaps should add a 

     * new macro here. 

     * 

     * RED-PEN: vmalloc is not supported right now. 

     */  

    if (!pfn_valid(pfn))  

        return -EIO;  

    vma->vm_pgoff = pfn;  

    return mmap_mem(file, vma);  

引起我注意的是__pa,完成核心虛拟位址到實體位址的轉換,最後調用mmap_mem,簡單一看kmem的确是映射的核心虛拟位址。

但是搞清楚__pa的實作,我就不這麼認為了。以powerpc為例,在arch/powerpc/include/asm/page.h,定義如下:

#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET))  

#define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET)  

....  

#define VIRT_PHYS_OFFSET (KERNELBASE - PHYSICAL_START)  

核心中定義了4個變量來表示核心一些基本的實體位址和虛拟位址,如下:

KERNELBASE     核心的起始虛拟位址,我的是0xc0000000

PAGE_OFFSET    低端記憶體的起始虛拟位址,一般是0xc0000000

PHYSICAL_START 核心的起始實體位址,我的是0x80000000

MEMORY_START   低端記憶體的起始實體位址,我的是0x80000000

核心在啟動過程中對于lowmem的靜态映射,就是以上述的實體位址和虛拟位址的內插補點進行線性映射的。

是以__pa __va轉換的是線性映射的記憶體部分,也就是lowmem。

是以kmem映射的是lowmem,如果我的cmdline參數中mem=512M,這就意味着通過kmem的mmap我最多可以通路核心位址空間開始的512M記憶體。

對于超過lowmem範圍,通路highmem,如果使用__pa通路,由于highmem是動态映射的,其映射關系不是線性的那麼簡單了,根據__pa擷取的實體位址與我們想要的核心虛拟位址是不對應的。

【新浪微網誌】 張昺華--sky

【twitter】 @sky2030_

【facebook】 張昺華 zhangbinghua

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.

繼續閱讀