1. 程式人生 > >/dev/mem可沒那麽簡單

/dev/mem可沒那麽簡單

mst oba his fig文件 conf resources ict win 4.5

這幾天研究了下/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))
        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 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++;
    }
    return 1;
}
#else
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
    return 1;
}
#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;
    return 0;
}
首先iomem_is_exclusive檢查該物理地址是否被獨占保留,實現例如以下:

#ifdef CONFIG_STRICT_DEVMEM
static int strict_iomem_checks = 1;
#else
static int strict_iomem_checks;
#endif

/*
 * 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)
        return 0;

    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;
            break;
        }
    }
    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。


依據我的實際操作測試。內存資源也都沒有exclusive。所以假設我映射地址是內存。檢查也能夠通過。




所以這裏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;
#else
    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;
    return 0;
#endif
}
max_pfn賦值在在do_init_bootmem中。例如以下.
void __init do_init_bootmem(void)
{
    unsigned long start, bootmap_pages;
    unsigned long total_pages;
    struct memblock_region *reg;
    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);

    if (!page_is_ram(pfn))
        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);
}
#endif
引起我註意的是__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獲取的物理地址與我們想要的內核虛擬地址是不正確應的。







/dev/mem可沒那麽簡單