核心態訪問使用者態地址
一、 與頁相關的資料結構及巨集的定義
分頁機制是硬體對分頁的支援,這是虛擬記憶體管理的硬體基礎。要想使這種硬體機制充分發揮其功能,必須有相應軟體的支援,我們來看一下Linux所定義的一些主要資料結構,其分佈在include/asm-i386/目錄下的page.h,pgtable.h及pgtable-2level.h三個檔案中。
1. 表項的定義
如上所述,PGD、PMD及PT表的表項都佔4個位元組,因此,把它們定義為無符號長整數,分別叫做pgd_t、pmd_t及pte_t(pte 即Page
table Entry),在page.h中定義如下:
typedef struct { unsignedlong pte_low; } pte_t;
typedef struct { unsignedlong pmd; } pmd_t;
typedef struct { unsigned long pgd; }pgd_t;
typedefstruct { unsigned long pgprot; } pgprot_t;
可以看出,
#definepte_val(x) ((x).pte_low)
#define pmd_val(x) ((x).pmd)
#define pgd_val(x) ((x).pgd)
從圖2.13可以看出,對這些表項應該定義成位段,但核心並沒有這樣定義,而是定義了一個頁面保護結構pgprot_t和一些巨集:
typedefstruct { unsigned long pgprot; } pgprot_t;
#definepgprot_val(x) ((x).pgprot)
欄位
#define _PAGE_PRESENT 0×001
#define _PAGE_RW 0×002
#define _PAGE_USER 0×004
#define _PAGE_PWT 0×008
#define _PAGE_PCD 0×010
#define _PAGE_ACCESSED 0×020
#define _PAGE_DIRTY 0×040
#define _PAGE_PSE 0×080 /* 4 MB (or 2MB) page, Pentium+, if present.. */
#define _PAGE_GLOBAL 0×100 /* Global TLB entry PPro+ */
在你閱讀原始碼的過程中你能體會到,把標誌位定義為巨集而不是位段更有利於編碼。
另外,頁目錄表及頁表在pgtable.h中定義如下:
externpgd_t swapper_pg_dir[1024];
externunsigned long pg0[1024];
swapper_pg_dir為臨時頁目錄表,是在核心編譯的過程中被靜態初始化的。pg0為初始化過程中使用的一臨時頁表。
2.線性地址域的定義
(1)頁偏移量的位數
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PTRS_PER_PTE 1024
#define PAGE_MASK (~(PAGE_SIZE-1))
其中PAGE_SHIFT巨集定義了頁偏移量的位數為12,因此頁大小PAGE_SIZE為212=4096位元組;
PTRS_PER_PTE為頁表的項數;最後PAGE_MASK值定義為0xfffff000,用以遮蔽掉偏移量域的所有位(12位)。
(2)PGDIR_SHIFT
#define PGDIR_SHIFT 22
#define PTRS_PER_PGD 1024
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
PGDIR_SHIFT是頁表所能對映區域線性地址的位數,它的值為22(12位的偏移量加上10位的頁表);PTRS_PER_PGD為頁目錄目錄項數;PGDIR_SIZE為頁目錄的大小,為222,即4MB;PGDIR_MASK為0xffc00000,用於遮蔽偏移量位與頁表域的所有位。
(3)PMD_SHIFT
#definePMD_SHIFT 22
#definePTRS_PER_PMD 1
PMD_SHIFT為中間目錄表對映的地址位數,其值也為22,但是對於兩級頁表結構,讓其目錄項個數為1,這就使得中間目錄在指標序列中的位置被儲存,以便同樣的程式碼在32位系統和64位系統下都能使用。後面的討論我們不再提及中間目錄。
3 對頁目錄及頁表的處理
在page.h,pgtable.h及pgtable-2level.h三個檔案中還定義有大量的巨集,用以對頁目錄、頁表及表項的處理,我們在此介紹一些主要的巨集和函式。
3.1.表項值的確定
staticinline int pgd_none(pgd_t pgd) { return 0; }
staticinline int pgd_present(pgd_t pgd) { return 1; }
#definepte_present(x) ((x).pte_low &(_PAGE_PRESENT | _PAGE_PROTNONE))
pgd_none()函式直接返回0,表示尚未為這個頁目錄建立對映,所以頁目錄項為空。pgd_present()函式直接返回1,表示對映雖然還沒有建立,但頁目錄所對映的頁表肯定存在於記憶體(即頁表必須一直在記憶體)。
pte_present巨集的值為1或0,表示P標誌位。如果頁表項不為0,但標誌位為0,則表示對映已經建立,但所對映的物理頁面不在記憶體。
3.2. 清相應表的表項:
#definepgd_clear(xp) do { } while (0)
#definepte_clear(xp) do { set_pte(xp,__pte(0)); } while (0)
pgd_clear巨集實際上什麼也不做,定義它可能是為了保持程式設計風格的一致。pte_clear就是把0寫到頁表表項中。
3.3.對頁表表項標誌值進行操作的巨集。
這些巨集的程式碼在pgtable.h檔案中,表2.2給出巨集名及其功能。
表2.2 對頁表表項標誌值進行操作的巨集及其功能
巨集名 |
功能 |
Set_pte() |
把一個具體的值寫入表項 |
Pte_read() |
返回User/Supervisor標誌值(由此可以得知是否可以在使用者態下訪問此頁) |
Pte _write() |
如果Present標誌和Read/Write標誌都為1,則返回1(此頁是否存在並可寫) |
Pte _exec() |
返回User/Supervisor標誌值 |
Pte _dirty() |
返回Dirty標誌的值(說明此頁是否被修改過) |
Pte _young() |
返回Accessed標誌的值(說明此頁是否被存取過) |
Pte _wrprotect() |
清除Read/Write標誌 |
Pte _rdprotect() |
清除User/Supervisor標誌 |
Pte _mkwrite |
設定Read/Write標誌 |
Pte _mkread |
設定User/Supervisor標誌 |
Pte _mkdirty() |
把Dirty標誌置1 |
Pte _mkclean() | 把Dirty標誌置0 |
Pte _mkyoung | 把Accessed標誌置1 |
Pte _mkold() | 把Accessed標誌置0 |
Pte _modify(p,v) | 把頁表表項p的所有存取許可權設定為指定的值v |
mk_pte() | 把一個線性地址和一組存取許可權合併來建立一個32位的頁表表項 |
pte _pte_phys() | 把一個實體地址與存取許可權合併來建立一個頁表表項 |
pte _page() | 從頁表表項返回頁的線性地址 |
實際上頁表的處理是一個複雜的過程,在這裡我們僅僅讓讀者對軟硬體如何結合起來有一個初步的認識。
三、模組程式設計舉例
結合上面的介紹,我們編寫一個核心模組,把一個給定的虛地址轉換為記憶體的實體地址:
- /*****************************************************************
- 檔名:mem.c
- 輸入引數:
- pid 接收待查詢程序的PID
- va 接收待查詢的虛擬地址
- *****************************************************************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <asm/pgtable.h>
#include <asm/page.h>
MODULE_LICENSE("GPL");
static int pid;
static unsigned long va;
module_param(pid,int,0644);
module_param(va,ulong,0644);
static int find_pgd_init(void)
{
unsigned long pa = 0;
struct task_struct *pcb_tmp = NULL;
pgd_t *pgd_tmp = NULL;
pud_t *pud_tmp = NULL;
pmd_t *pmd_tmp = NULL;
pte_t *pte_tmp = NULL;
printk("<0>""PAGE_OFFSET = 0x%lx\n",PAGE_OFFSET);
printk("<0>""PGDIR_SHIFT = %d\n",PGDIR_SHIFT);
printk("<0>""PUD_SHIFT = %d\n",PUD_SHIFT);
printk("<0>""PMD_SHIFT = %d\n",PMD_SHIFT);
printk("<0>""PAGE_SHIFT = %d\n",PAGE_SHIFT);
printk("<0>""PTRS_PER_PGD = %d\n",PTRS_PER_PGD);
printk("<0>""PTRS_PER_PUD = %d\n",PTRS_PER_PUD);
printk("<0>""PTRS_PER_PMD = %d\n",PTRS_PER_PMD);
printk("<0>""PTRS_PER_PTE = %d\n",PTRS_PER_PTE);
printk("<0>""PAGE_MASK = 0x%lx\n",PAGE_MASK);
if (!(pcb_tmp = pid_task(find_vpid(pid), PIDTYPE_PID))) {
printk("<0>""Can't find the task %d .\n",pid);
return 0;
}
printk("<0>""pgd = 0x%p\n",pcb_tmp->mm->pgd);
/* 判斷給出的地址va是否合法(va<vm_end)*/
if(!find_vma(pcb_tmp->mm,va)){
printk("<0>""virt_addr 0x%lx not available.\n",va);
return 0;
}
pgd_tmp = pgd_offset(pcb_tmp->mm,va);
printk("<0>""pgd_tmp = 0x%p\n",pgd_tmp);
printk("<0>""pgd_val(*pgd_tmp) = 0x%lx\n",pgd_val(*pgd_tmp));
if(pgd_none(*pgd_tmp)){
printk("<0>""Not mapped in pgd.\n");
return 0;
}
pud_tmp = pud_offset(pgd_tmp,va);
printk("<0>""pud_tmp = 0x%p\n",pud_tmp);
printk("<0>""pud_val(*pud_tmp) = 0x%lx\n",pud_val(*pud_tmp));
if(pud_none(*pud_tmp)){
printk("<0>""Not mapped in pud.\n");
return 0;
}
pmd_tmp = pmd_offset(pud_tmp,va);
printk("<0>""pmd_tmp = 0x%p\n",pmd_tmp);
printk("<0>""pmd_val(*pmd_tmp) = 0x%lx\n",pmd_val(*pmd_tmp));
if(pmd_none(*pmd_tmp)){
printk("<0>""Not mapped in pmd.\n");
return 0;
}
/*在這裡,把原來的pte_offset_map()改成了pte_offset_kernel*/
pte_tmp = pte_offset_kernel(pmd_tmp,va);
printk("<0>""pte_tmp = 0x%p\n",pte_tmp);
printk("<0>""pte_val(*pte_tmp) = 0x%lx\n",pte_val(*pte_tmp));
if(pte_none(*pte_tmp)){
printk("<0>""Not mapped in pte.\n");
return 0;
}
if(!pte_present(*pte_tmp)){
printk("<0>""pte not in RAM.\n");
return 0;
}
pa = (pte_val(*pte_tmp)& PAGE_MASK) |(va & ~PAGE_MASK);
printk("<0>""virt_addr 0x%lx in RAM is 0x%lx .\n",va,pa);
printk("<0>""contect in 0x%lx is 0x%lx\n",pa, *(unsigned long *)((char *)pa + PAGE_OFFSET));
//此處有問題,只有低端記憶體才會有可以線性對映,我想應用copy_to_user
return 0;
}
static void find_pgd_exit(void)
{
printk("<0>""Goodbye!\n");
}
module_init(find_pgd_init);
module_exit(find_pgd_exit);
Makefile如下:
obj-m := pgd.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
測試:開啟gedit, 再開啟工作管理員,檢視gedit的程序號pid=12749,
右鍵檢視其記憶體對映,找到一個有效的虛擬地址va=0xb8041000,然後:
sudo insmod mem.ko pid=12749 va=0xb8041000
如果你的核心是2.6.24以後的,需要將find_task_by_pid改為find_task_by_vpid