从227行开始,著名的分页机制就粉墨登场了:
227page_pde_offset = (__PAGE_OFFSET >> 20);
228
229 movl $pa(__brk_base), %edi
230 movl $pa(swapper_pg_dir), %edx
231 movl $PTE_IDENT_ATTR, %eax
23210:
233 leal PDE_IDENT_ATTR(%edi),%ecx /* Create PDE entry */
234 movl %ecx,(%edx) /* Store identity PDE entry */
235 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
227行设置一个常量page_pde_offset,其值0xc0000000 >> 20 = 0xc00,其作用待会再说。229行,__brk_base,这个东西咱们见过,在链接vmlinux时,__brk_base作为BRK段的开始地址。BRK段是保留给用户进程通过系统调用brk() 向内核申请的内存空间。
对这个brk我还要多说几句,传统UNIX系统有两个系统调用brk和sbrk,主要的工作是实现虚拟内存到实际内存的映射。在GNU C程序中,内存分配是这样的:
每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用brk系统调用将数据段的下界移动,brk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。
所以,这个__brk_base对应的空间就是编译链接vmlinux时的数据段下界,229行就是把这个下界的物理地址赋给了edi寄存器。
继续走,230行,swapper_pg_dir。这个东西就更重要了,我们操作系统的分页单元就全靠它了。它的定义在哪儿呢?同一文件621行:
621ENTRY(swapper_pg_dir)
622 .fill 1024,4,0
623#endif
我们看到.fill 1024,4,0的意思是重复1024次,每组4个字节,内容为0。这正好是一个页面的大小,4k字节。这个页面的物理地址是$pa(swapper_pg_dir),内核把它作为第一个页表,把它的物理地址赋给edx寄存器。
再来,231行$PTE_IDENT_ATTR常量,来自arch/x86/include/asm/pgtable_types.h:
#define PTE_IDENT_ATTR 0x003 /* PRESENT+RW */
#define PDE_IDENT_ATTR 0x067 /* PRESENT+RW+USER+DIRTY+ACCESSED */
#define PGD_IDENT_ATTR 0x001 /* PRESENT (no other attributes) */
把0x003保存到eax寄存器中。随后233行lea指令,保存地址值。不明白lea指令的注意了,这个指令太重要了。lea指令有两个操作数,其目的操作数,表示操作结果保存在此,该指令目的操作数只能是8个通用寄存器之一。而源操作数只能是一个存储单元,表达存储单元有多种寻址方式。
lea指令的功能是将源操作数、即存储单元的有效地址(偏移地址)传送到目的操作数。代码指令中PDE_IDENT_ATTR(%edi)采用间接寻址方式表达存储单元,它表示的存储单元的有效地址是:__brk_base的物理地址再加上0x67,然后被传送到ecx中。
这样做的目的是什么呢?我们看到,__brk_base的物理地址是32位的,而一个页面是4KB,于是乎最低12位必须为0,不然这个页面就没法对其了。所以,这里__brk_base物理地址的最低12位是无效的,内核另作他用,这里把最低12位中的0x67置位,就是说明把PRESENT、RW、USER、DIRTY和ACCESSED置位,然后把整个值传递到ecx中。然后234行就把这个地址放到swapper_pg_dir的第一个4字节的单元,也就是edx寄存器对应的那个存储单元开始的四个存储单元,作为页全局目录的第1项。
继续走,235行,page_pde_offset,其值0xc00,我们刚才看到了。这里再把ecx中存放的__brk_base物理地址存放到swapper_pg_dir偏移0xc00的内存单元中。0xc00换算成十进制就是:3072,由于每个表项占4个内存单元,所以这个偏移就是3072/4=768,也就是以swapper_pg_dir开始的全局页目录的第768个表项。这样我们就得出了一个重要的结论,页全局目录为swapper_pg_dir开始的4096个内存单元,正好占用1个页面,包含1024个表项,但是同时从第1个和第768个表项开始分配,而且这些个表项的PRESENT、RW、USER、DIRTY和ACCESSED被置位。没有用到表项都为空。
236 addl $4,%edx
237 movl $1024, %ecx
23811:
239 stosl
240 addl $0x1000,%eax
241 loop 11b
236行,edx加上4个字节,正好对应页全局目录的第2个表项。237行,ecx的值设为1024。看到这里,大家马上会想到肯定又要进入循环了,不错241行那个loop正是这个循环的标志。
239行存储字符串数据(Store String Data),将累加器ecx内容存储到由es:edi寻址的内存地址。如果使用stos指令,必须指定目的操作数,stosl拷贝eax到es:edi所寻址的内存中。所以239行将eax存放的4个字节,32位的数值0x3存放到es:edi对应的内存单元,也就是__brk_base对应的那个内存单元。
240行,eax再加0x1000,等于0x1003,这样进入循环,循环1024次后,__brk_base对应的那个内存单元就是首地址为swapper_pg_dir,共有1024个表项的第一个页表。第1个表项的内容为0x3,第2个为0x1003,第3个为0x2003,第4个为0x3003,……,第1024个表项为0x3FF003。很多人分析代码时都不太注意这些细节,其实内核代码主要考验大家的就是掌握计算机知识的基本功,都是我们在学校学过的,没有什么高深的东西。人家Torvalds当年发明Linux的时候不也就是个小国家的本科生嘛。
Intel指令集中有5组处理字节,字和双字数组的指令,称为基本字符串指令,对于他们的这些基本概念请查阅博客“Intel 8086/8088 指令系统(四)”
245 movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
246 cmpl %ebp,%eax
247 jb 10b
248 addl $__PAGE_OFFSET, %edi
249 movl %edi, pa(_brk_end)
250 shrl $12, %eax
251 movl %eax, pa(max_pfn_mapped)
252
253 /* Do early initialization of the fixmap area */
254 movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax
255 movl %eax,pa(swapper_pg_dir+0xffc)
256#endif
257 jmp 3f
由于我们的页表映射关系是以end + MAPPING_BEYOND_END结束,所以还有些尾巴摇处理了,245到255行就是处理这个,首先比较一下end + MAPPING_BEYOND_END是否超过了1页,如果超过了,就还要跳回去再申请若干页全局目录的表项,和若干个作为页表的页面。最后还要申请些空间来处理fixmap区。
如上图所示,最后以_brk_base开始的内存单元存放了若干个页表,每个页表占据一个页面。页表项的内容从0x003开始,到$pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR取决于_end的物理地址。所以在这里页全局目录和页表建立完成后,就完成了从物理地址到链接vmlinux后的结束地址_end的所有代码的映射,包括__brk_base到__brk_limit这段BRK段本身的代码。
略过SMP和MMU的相关初始化代码(其实这些代码也很重要,只不过太偏了,感兴趣的同学可以自行研究),来到331行:
328/*
329 * Enable paging
330 */
331 movl $pa(swapper_pg_dir),%eax
332 movl %eax,%cr3 /* set the page table pointer.. */
333 movl %cr0,%eax
334 orl $X86_CR0_PG,%eax
335 movl %eax,%cr0 /* ..and set paging (PG) bit */
336 ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
3371:
338 /* Set up the stack pointer */
339 lss stack_start,%esp
当页全局目录和页表建立好一会,就可以启动分页机制了,其步骤很简单,就是把页全局目录的32位物理地址传递给cr3寄存器,然后启动cr0的分页装置($X86_CR0_PG项字段)。综上所述,我们看到在32位内核的分页环境是启用的三级机制,即1个页全局目录,若干页表和页面。
最后336行,进入分页后的内核代码段,执行lss stack_start,%esp指令,立即为进程0建立内核态堆栈。stack_start定义在657行:
657 ENTRY(stack_start)
658 .long init_thread_union+THREAD_SIZE
659 .long __BOOT_DS
我们看到内核态堆栈由init_thread_union表示,其在include/linux/sched.h中被定义成一个全局变量:
extern union thread_union init_thread_union;
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
#define THREAD_SIZE (PAGE_SIZE << THREAD_ORDER)
由于PAGE_SIZE是4096,THREAD_ORDER在32位x86体系中是1,所以THREAD_SIZE的值为8k。所以这个thread_union的大小也为8k。关于内核栈的详细内容请参考博客“进程相关的数据结构”