歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux 內核源代碼情景分析 chap 2 存儲管理 (三)

1. 越界訪問

1.1 頁面異常

頁式存儲機制通過頁面目錄和頁面表將每個線性地址(或者虛擬地址), 轉化成物理地址。 然而, 如果在這個過程中遇到某種阻礙的話, 就會產生一次頁面異常, 也稱缺頁異常。
主要有下面 3 中障礙:
1. 相應的頁面目錄項或者頁面表項為空, ie, 線性地址到物理地址的映射關系並未建立或者已經被撤銷。
2. 相應的物理頁面不在內存中, 有頁面描述項 vma 結構
3. 指令中規定的訪問方式和頁面的權限不符

1.2 do_page_fault

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 分配物理空間)

Copyright © Linux教程網 All Rights Reserved