頁式存儲機制通過頁面目錄和頁面表將每個線性地址(或者虛擬地址), 轉化成物理地址。 然而, 如果在這個過程中遇到某種阻礙的話, 就會產生一次頁面異常, 也稱缺頁異常。
主要有下面 3 中障礙:
1. 相應的頁面目錄項或者頁面表項為空, ie, 線性地址到物理地址的映射關系並未建立或者已經被撤銷。
2. 相應的物理頁面不在內存中, 有頁面描述項 vma 結構
3. 指令中規定的訪問方式和頁面的權限不符
do_page_fault 是頁面異常服務的主體程序的入口。
==================== arch/i386/mm/fault.c 106 152 ====================
106 asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
107 {
108 struct task_struct *tsk;
109 struct mm_struct *mm;
110 struct vm_area_struct * vma;
111 unsigned long address;
112 unsigned long page;
113 unsigned long fixup;
114 int write;
115 siginfo_t info;
116
117 /* get the address */
118 __asm__("movl %%cr2,%0":"=r" (address));
119
120 tsk = current;
121
122 /*
123 * We fault-in kernel-space virtual memory on-demand. The
124 * 'reference' page table is init_mm.pgd.
125 *
126 * NOTE! We MUST NOT take any locks for this case. We may
127 * be in an interrupt or a critical region, and should
128 * only copy the information from the master page table,
129 * nothing more.
130 */
131 if (address >= TASK_SIZE)
132 goto vmalloc_fault;
133
134 mm = tsk->mm;
135 info.si_code = SEGV_MAPERR;
136
137 /*
138 * If we're in an interrupt or have no user
139 * context, we must not take the fault..
140 */
141 if (in_interrupt() || !mm)
142 goto no_context;
143
144 down(&mm->mmap_sem);
145
146 vma = find_vma(mm, address);
147 if (!vma)
148 goto bad_area;
149 if (vma->vm_start <= address)
150 goto good_area;
151 if (!(vma->vm_flags & VM_GROWSDOWN)) // 這裡實際討論的越界訪問會走到這裡
152 goto bad_area;
首先使用匯編代碼, 獲取CR2 寄存器中的 映射失敗時候的線性地址,傳入參數 regs 是內核中斷機制響應保留的現場, error_code 表征映射失敗的原因。
需要注意的是, 代碼 中 current 不是一個全局變量, 這是一個宏, 用來獲取當前進程的task_struct 結構的地址。
另外, cpu 實際進行的映射是通過頁面目錄 和 頁面表完成的, task_struct 中有一個指向mm_struct 結構的指針, 跟虛存管理和映射相關的信息都存放在這個結構中。
if (in_interrupt() || !mm) 用來處理兩種特殊情況, 1. 映射失敗發生在某個中斷服務中, 2. 進程映射還沒有被建立起來。 這些都不是我們這裡需要處理的。
由於下面需要操作進程中共享的mm_struct 結構, 所以需要加鎖, down () 就是起到這個加鎖的作用的。
然後, 通過 find_vma() 試圖在一個虛存空間中找到一個結束地址大於給定地址的第一個區間, 特別需要注意的是, 找出的 vma 的 vm_start 可能也是大於 address 的。
==================== mm/mmap.c 404 440 ====================
404 /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
405 struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
406 {
407 struct vm_area_struct *vma = NULL;
408
409 if (mm) {
410 /* Check the cache first. */
411 /* (Cache hit rate is typically around 35%.) */
412 vma = mm->mmap_cache;
413 if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
414 if (!mm->mmap_avl) {
415 /* Go through the linear list. */
416 vma = mm->mmap;
417 while (vma && vma->vm_end <= addr)
418 vma = vma->vm_next;
419 } else {
420 /* Then go through the AVL tree quickly. */
421 struct vm_area_struct * tree = mm->mmap_avl;
422 vma = NULL;
423 for (;;) {
424 if (tree == vm_avl_empty)
425 break;
426 if (tree->vm_end > addr) {
427 vma = tree;
428 if (tree->vm_start <= addr)
429 break;
430 tree = tree->vm_avl_left;
431 } else
432 tree = tree->vm_avl_right;
433 }
434 }
435 if (vma)
436 mm->mmap_cache = vma;
437 }
438 }
439 return vma;
440 }
這段代碼負責查找一個虛存空間中找到一個結束地址大於給定地址的第一個區間, 他利用 mmap_cache 輔助查找( 有 35% 的命中率), avl 樹, 鏈表搜索等方式查找。
回到我們的do_page_fault, find_vma 返回的結果可能是:
1. 沒有找到 vma == nullptr, 沒有一個區間的結束地址高於定義的地址, ie, 這個地址在堆棧上面去了, 地址越界了
2. 找到了一個 vma, 並且 vm_start <= address, 表明這個區間的描述是OK 的, 需要進一步看下是不是由於訪問權限 或者 由於對象不在 內存中引起的異常。
3. 找到一個 vma 但是 vm_start > address, 這就表明 我們的的 address 落到中間的空洞裡面去了。
<喎?http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxjb2RlIGNsYXNzPQ=="hljs objectivec">可以參考下面這張圖, 協助理解, 數據和代碼空間是從下向上增長的, 而堆棧是自上而下增長的。
根據 vm_area_t 中的 vm_flags 中的 VM_GROWSDOWN 這個標志位, 我們可以知道address 當前是在 代碼數據區裡面(僅僅因為映射被撤銷了) 還是 堆棧區裡面。
1.3 bad_area
==================== arch/i386/mm/fault.c 220 239 ====================
[do_page_fault()]
220 /*
221 * Something tried to access memory that isn't in our memory map..
222 * Fix it, but check if it's kernel or user first..
223 */
224 bad_area:
225 up(&mm->mmap_sem);
226
227 bad_area_nosemaphore:
228 /* User mode accesses just cause a SIGSEGV */
229 if (error_code & 4) {
230 tsk->thread.cr2 = address;
231 tsk->thread.error_code = error_code;
232 tsk->thread.trap_no = 14;
233 info.si_signo = SIGSEGV;
234 info.si_errno = 0;
235 /* info.si_code has been set above */
236 info.si_addr = (void *)address;
237 force_sig_info(SIGSEGV, &info, tsk);
238 return;
239 }
==================== arch/i386/mm/fault.c 96 105 ====================
96 /*
97 * This routine handles page faults. It determines the address,
98 * and the problem, and then passes it off to one of the appropriate
99 * routines.
100 *
101 * error_code:
102 * bit 0 == 0 means no page found, 1 means protection fault
103 * bit 1 == 0 means read, 1 means write
104 * bit 2 == 0 means kernel, 1 means user-mode
105 */
也就是說, error_code 的bit2 為 1, 表征cpu 處於用戶模式的時候發生了異常, 這時候, 就會給出一個軟中斷 SIGSEGV, 至此, 進程就因為異常訪問而掛掉了。
ps: SIGSEGV 是一個強制性信號, cpu 必須處理。
1.4 小結
我們這裡所討論的內存越界主要就是指, 訪問了一段數據或者代碼區的 data, 而這個data 的映射 正好被撤銷了, 留下一個孤立的空洞,或者就沒有建立過映射
通過進入
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
造成內存越界訪問。
為方便理解, 我們繪制了這麼一幅圖, 空洞2 是未分配的空間, 空洞1 是 建立過映射但是現在映射被撤銷的部分。 而我們這裡討論的內存越界 指的就是 訪問這裡的空洞 1 或者 空洞 2 的過程。
2. 用戶堆棧的擴展
這裡討論的是一種特殊情況, 我們的堆棧區間比較小, 並且在已經滿了情況下, 如果此時又發生了一個程序調用, 就需要將返回地址壓棧, 可是這時候, 棧滿了,於是, 觸發了頁面異常。
2.1 堆棧擴展請求判斷
==================== arch/i386/mm/fault.c 151 164 ====================
[do_page_fault()]
151 if (!(vma->vm_flags & VM_GROWSDOWN))
152 goto bad_area;
153 if (error_code & 4) {
154 /*
155 * accessing the stack below %esp is always a bug.
156 * The "+ 32" is there due to some instructions (like
157 * pusha) doing post-decrement on the stack and that
158 * doesn't show up until later..
159 */
160 if (address + 32 < regs->esp)
161 goto bad_area;
162 }
163 if (expand_stack(vma, address))
164 goto bad_area;
首先需要描述一下現在的情形, 由於堆棧區已經滿了, 我們現在落在堆棧區下方的空洞內,但是我們距離這個堆棧區很近。
由於 i386 cpu 有一條pusha 指令, 可以一次將32 個字節壓入堆棧, 所以這裡采用的判斷標准是 %esp - 32, 落在這個范圍內的, 我們認為是正常的擴展堆棧的需求, 否則不是。
2.2 expand_stack
==================== include/linux/mm.h 487 504 ====================
[do_page_fault()>expand_stack()]
487 /* vma is the first one with address < vma->vm_end,
488 * and even address < vma->vm_start. Have to extend vma. */
489 static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)
490 {
491 unsigned long grow;
492
493 address &= PAGE_MASK;
494 grow = (vma->vm_start - address) >> PAGE_SHIFT;
495 if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||
496 ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
497 return -ENOMEM;
498 vma->vm_start = address;
499 vma->vm_pgoff -= grow;
500 vma->vm_mm->total_vm += grow;
501 if (vma->vm_flags & VM_LOCKED)
502 vma->vm_mm->locked_vm += grow;
503 return 0;
504 }
我們這裡使用 address &= PAGE_MASK; 實現對齊頁面邊界。自此之後的address 都是對齊過頁面邊界的 address 了。
然後判斷 這段內存分配的量是不是超過了資源限制, 如果超過了限制, 返回 -ENOMEM。
如果成功, 更新vma, mm 結構中的數據信息。但是, 新擴展的頁面對物理內存的映射到這裡還是沒有建立起來, 需要good_area 繼續完成。
2.3 good_area
==================== arch/i386/mm/fault.c 165 207 ====================
[do_page_fault()]
165 /*
166 * Ok, we have a good vm_area for this memory access, so
167 * we can handle it..
63
168 */
169 good_area:
170 info.si_code = SEGV_ACCERR;
171 write = 0;
172 switch (error_code & 3) {
173 default: /* 3: write, present */
174 #ifdef TEST_VERIFY_AREA
175 if (regs->cs == KERNEL_CS)
176 printk("WP fault at %08lx\n", regs->eip);
177 #endif
178 /* fall through */
179 case 2: /* write, not present */
180 if (!(vma->vm_flags & VM_WRITE))
181 goto bad_area;
182 write++;
183 break;
184 case 1: /* read, present */
185 goto bad_area;
186 case 0: /* read, not present */
187 if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
188 goto bad_area;
189 }
190
191 /*
192 * If for any reason at all we couldn't handle the fault,
193 * make sure we exit gracefully rather than endlessly redo
194 * the fault.
195 */
196 switch (handle_mm_fault(mm, vma, address, write)) {
197 case 1:
198 tsk->min_flt++;
199 break;
200 case 2:
201 tsk->maj_flt++;
202 break;
203 case 0:
204 goto do_sigbus;
205 default:
206 goto out_of_memory;
207 }
這裡需要涉及寫操作, 但是頁面不在內存中, ie, code 為 2, 這時候需要檢測vma 的 寫屬性, 很明顯的, 堆棧區是允許寫入的, 於是這裡會調用 handle_mm_fault。
2.4 handle_mm_fault
==================== mm/memory.c 1189 1208 ====================
[do_page_fault()>handle_mm_fault()]
1189 /*
1190 * By the time we get here, we already hold the mm semaphore
1191 */
1192 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
1193 unsigned long address, int write_access)
1194 {
1195 int ret = -1;
1196 pgd_t *pgd;
1197 pmd_t *pmd;
1198
1199 pgd = pgd_offset(mm, address);
1200 pmd = pmd_alloc(pgd, address);
1201
1202 if (pmd) {
1203 pte_t * pte = pte_alloc(pmd, address);
1204 if (pte)
1205 ret = handle_pte_fault(mm, vma, address, write_access, pte);
1206 }
1207 return ret;
1208 }
==================== include/asm-i386/pgtable.h 311 312 ====================
311 /* to find an entry in a page-table-directory. */
312 #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
==================== include/asm-i386/pgtable.h 316 316 ====================
316 #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
通過 pgd_offset 我們獲取得到了一個pmd 頁面的地址, 在 i386 中, 實際上就是 pte 的地址。
因為, 在 pgtable_2level.h 中, 將 pmd_alloc 定義為了 return (pmd_t *)pgd;
2.5 pte_alloc
==================== include/asm-i386/pgalloc.h 120 141 ====================
[do_page_fault()>handle_mm_fault()>pte_alloc()]
120 extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address)
121 {
122 address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
123
124 if (pmd_none(*pmd))
125 goto getnew;
126 if (pmd_bad(*pmd))
127 goto fix;
128 return (pte_t *)pmd_page(*pmd) + address;
129 getnew:
130 {
131 unsigned long page = (unsigned long) get_pte_fast();
132
133 if (!page)
134 return get_pte_slow(pmd, address);
135 set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page)));
136 return (pte_t *)page + address;
137 }
138 fix:
139 __handle_bad_pmd(pmd);
140 return NULL;
141 }
首先, 由於pmd 所指向的目錄項一定是空的, 所以需要到 getnew 處分配一個頁面表, 這裡一個頁面表正好就是一個物理頁面。 內核對這個頁面表分配的過程做了一些優化:
當需要釋放一個物理頁面的時候, 內核不會立即將他釋放,而是把它放入到緩沖池中, 只有當緩沖池滿的時候, 才會真正釋放物理頁面, 如果這個池子是空的, 就只能通過 get_pte_kernel_slow 分配了, 效率會比較低, 否則, 從這個池子中獲取一個物理頁面作為我們的頁面表。
2.6 handle_pte_default
分配完一個物理頁面之後, 我們就需要設置相應的頁面表項了。
==================== mm/memory.c 1135 1187 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()]
1135 /*
1136 * These routines also need to handle stuff like marking pages dirty
1137 * and/or accessed for architectures that don't do it in hardware (most
1138 * RISC architectures). The early dirtying is also good on the i386.
1139 *
1140 * There is also a hook called "update_mmu_cache()" that architectures
1141 * with external mmu caches can use to update those (ie the Sparc or
1142 * PowerPC hashed page tables that act as extended TLBs).
1143 *
1144 * Note the "page_table_lock". It is to protect against kswapd removing
1145 * pages from under us. Note that kswapd only ever _removes_ pages, never
1146 * adds them. As such, once we have noticed that the page is not present,
66
1147 * we can drop the lock early.
1148 *
1149 * The adding of pages is protected by the MM semaphore (which we hold),
1150 * so we don't need to worry about a page being suddenly been added into
1151 * our VM.
1152 */
1153 static inline int handle_pte_fault(struct mm_struct *mm,
1154 struct vm_area_struct * vma, unsigned long address,
1155 int write_access, pte_t * pte)
1156 {
1157 pte_t entry;
1158
1159 /*
1160 * We need the page table lock to synchronize with kswapd
1161 * and the SMP-safe atomic PTE updates.
1162 */
1163 spin_lock(&mm->page_table_lock);
1164 entry = *pte;
1165 if (!pte_present(entry)) {
1166 /*
1167 * If it truly wasn't present, we know that kswapd
1168 * and the PTE updates will not touch it later. So
1169 * drop the lock.
1170 */
1171 spin_unlock(&mm->page_table_lock);
1172 if (pte_none(entry))
1173 return do_no_page(mm, vma, address, write_access, pte);
1174 return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access);
1175 }
1176
1177 if (write_access) {
1178 if (!pte_write(entry))
1179 return do_wp_page(mm, vma, address, pte, entry);
1180
1181 entry = pte_mkdirty(entry);
1182 }
1183 entry = pte_mkyoung(entry);
1184 establish_pte(vma, address, pte, entry);
1185 spin_unlock(&mm->page_table_lock);
1186 return 1;
1187 }
此時, 由於我們的頁面表項是空的, 所以一定是進入到 do_no_page 調用中去。
==================== mm/memory.c 1080 1098 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()]
1080 /*
1081 * do_no_page() tries to create a new page mapping. It aggressively
1082 * tries to share with existing pages, but makes a separate copy if
1083 * the "write_access" parameter is true in order to avoid the next
1084 * page fault.
1085 *
1086 * As this is called only for pages that do not currently exist, we
1087 * do not need to flush old virtual caches or the TLB.
1088 *
1089 * This is called with the MM semaphore held.
1090 */
1091 static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
1092 unsigned long address, int write_access, pte_t *page_table)
1093 {
1094 struct page * new_page;
1095 pte_t entry;
1096
1097 if (!vma->vm_ops || !vma->vm_ops->nopage)
1098 return do_anonymous_page(mm, vma, page_table, write_access, address);
......
==================== mm/memory.c 1133 1133 ====================
1133 }
然後,在do_no_page 中根據 vma 結構中的 vm_ops 中記錄的 no_page 函數指針, 進行相應處理, 但是這裡, 沒有與文件相關的操作, 因而不會有 no_page , 於是轉而調用了 do_anonymous_page
==================== mm/memory.c 1058 1078 ====================
[do_page_fault()>handle_mm_fault()>handle_pte_fault()>do_no_page()>do_anonymous_page()]
1058 /*
1059 * This only needs the MM semaphore
1060 */
1061 static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table,
int write_access, unsigned long addr)
1062 {
1063 struct page *page = NULL;
1064 pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));
1065 if (write_access) {
1066 page = alloc_page(GFP_HIGHUSER);
1067 if (!page)
1068 return -1;
1069 clear_user_highpage(page, addr);
1070 entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));
1071 mm->rss++;
1072 flush_page_to_ram(page);
1073 }
1074 set_pte(page_table, entry);
1075 /* No need to invalidate - it was non-present before */
1076 update_mmu_cache(vma, addr, entry);
1077 return 1; /* Minor fault */
1078 }
==================== include/asm-i386/pgtable.h 277 277 ====================
277 static inline pte_t pte_wrprotect(pte_t pte) { (pte).pte_low &= ~_PAGE_RW; return pte; }
==================== include/asm-i386/pgtable.h 271 271 ====================
271 static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; }
==================== include/asm-i386/pgtable.h 91 96 ====================
91 /*
92 * ZERO_PAGE is a global shared page that is always zero: used
93 * for zero-mapped memory areas etc..
94 */
95 extern unsigned long empty_zero_page[1024];
96 #define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
通過這個調用 實現對 pte 表項的設置工作。
需要注意的是, 如果是讀操作, 只使用 pte_wrprotect 將頁面設置為只讀權限, 並一律映射到同一個物理頁面 empty_zero_page, 這個頁面內容全部都是 0.
只有是 寫操作, 才會分配獨立的物理內存空間, 並設置寫權限等操作。
2.7 小結
總結一下這個堆棧擴展的流程:
1. 檢測是不是合法的堆棧擴展, 如果是的話, 就調用 expand_stack 完成 對堆棧區vm_area_struct 結構的改動。(虛擬空間)
2. 下面分配相應的物理頁面。handle_mm_fault, 先分配pmd, 然後是pte 頁面表 (pte_t * pte = pte_alloc(pmd, address)), 接下來 調用 handle_pte_fault 設置相應物理頁面的屬性。(其中, 利用中間 do_no_page 分配物理空間)