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.Bkjia.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 分配物理空间)

相关内容