本文共 3940 字,大约阅读时间需要 13 分钟。
对在启动启动时,操作系统的页面管理机制的启动一直存在一些疑惑:
假设页面机制启动的指令(类似movl %eax,%cr0)的指令寄存器的值为ip=0x0001,则这条指令的下一条应该执行的指令对应的ip应该是ip=0x0002。但是问题是,对于下一条指令来说,此时虚拟内存的机制已经启动,ip=0x0002所对应的应该是虚拟地址,但是按道理,这里我们的下一条指令应该是存储在了物理地址**0x0002中。难道是说虚拟内存地址ip=0x0002也是映射到物理地址0x0002中,但是我们记得在内核空间中,虚拟内存地址**x应该是映射到物理地址x-KERNBASE中才对。又或者说,难道对于物理地址x,映射到它的虚拟地址有两个,分别是x和x+KERNBASE?
我们下面结合xv6的源码看一下(这里就不贴源码了)。BIOS初始化工作完成之后,会将boot代码加载进入内存,boot包括xv6中的bootasm.S和Bootmain.c两个文件。boot的入口在bootasm.S中,在bootasm中并没有开启分页机制,只是从实模式转化到了保护模式,然后跳到Bootmain.c中执行bootmain函数
call bootmain
bootmain函数的代码如下:
voidbootmain(void){ struct elfhdr *elf; struct proghdr *ph, *eph; void (*entry)(void); uchar* pa; elf = (struct elfhdr*)0x10000; // scratch space // Read 1st page off disk readseg((uchar*)elf, 4096, 0); // Is this an ELF executable? if(elf->magic != ELF_MAGIC) return; // let bootasm.S handle error // Load each program segment (ignores ph flags). ph = (struct proghdr*)((uchar*)elf + elf->phoff); eph = ph + elf->phnum; for(; ph < eph; ph++){ pa = (uchar*)ph->paddr; readseg(pa, ph->filesz, ph->off); if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); } // Call the entry point from the ELF header. // Does not return! entry = (void(*)(void))(elf->entry); entry();}
显然这里的主要工作是将内核代码加载进内存。内核代码会编译到一个ELF文件中,内核代码的启动点是ELF符号表中的entry对应的地址。也可以看到知道bootmain,分页内存管理机制也没有启动。
下面我们看一下内核代码的启动部分,在entry.S中:
#include "asm.h"#include "memlayout.h"#include "mmu.h"#include "param.h"# Multiboot header. Data to direct multiboot loader..p2align 2.text.globl multiboot_headermultiboot_header: #define magic 0x1badb002 #define flags 0 .long magic .long flags .long (-magic-flags)# By convention, the _start symbol specifies the ELF entry point.# Since we haven't set up virtual memory yet, our entry point is# the physical address of 'entry'..globl _start#查看kernel.ld文件就可以明白了,连接器把kernel的elf文件连接到了0X80100000地址处,这里的start_将会是物理地址#外面文件调用这里的entry时会直接从物理地址调用_start = V2P_WO(entry) # Entering xv6 on boot processor, with paging off..globl entryentry: # Turn on page size extension for 4Mbyte pages movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4 # Set page directory movl $(V2P_WO(entrypgdir)), %eax # entrypgdir 在main.c里面定位 movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PG|CR0_WP), %eax movl %eax, %cr0 #这里开启分页,开启了分页之后, # Set up the stack pointer. movl $(stack + KSTACKSIZE), %esp # Jump to main(), and switch to executing at # high addresses. The indirect call is needed because # the assembler produces a PC-relative instruction # for a direct jump. mov $main, %eax jmp *%eax.comm stack, KSTACKSIZE
我们知道ELF中的地址对应的是虚拟地址,我们通过链接器脚本可以设置ELF的虚拟地址。从链接器脚本中我们可以知道内核代码的虚拟地址的起始地址为0X80100000,并且对应的物理地址是0x100000。也就是把bootmain中把内核代码加载到以0x100000起始的连续物理内存空间中。但是内核代码对应的虚拟地址是以0X80100000的起始的。
现在来看一下内核入口的代码,根据注释:
By convention, the _start symbol specifies the ELF entry point.
Since we haven’t set up virtual memory yet, our entry point is the physical address of ‘entry’.
我们知道
_start = V2P_WO(entry)
是为了更改ELF中的_start符号表项,这个符号表项主要是表示内核代码的入口地址,也是bootmain中调用的entery()入口。正如前面所说,ELF中所有地址都是基于虚拟地址的,但是此时页面管理机制还没有开启,所有虚拟地址不能用。这么一改之后,就相当与直接从物理地址进入内核代码了。
开启分页机制的代码主要是下面这部分:
movl $(V2P_WO(entrypgdir)), %eax movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PG|CR0_WP), %eax movl %eax, %cr0
先在cr3寄存器中设置好页目录,然后开启分页机制,从此就进入了虚拟内存时代。但是正如我们前面所说,在
movl %eax, %cr0
时系统用的是物理地址,而在它的下一条语句系统用的是虚拟地址。假设这条语句的ip为0x00001,则它的下一条语句的ip=0x00002。我们要保证,虚拟地址0x00002对应的是物理地址中的0x00002,否则代码执行就乱掉了。所以我们看一下开启内存分页管理时的页目录设置:
movl $(V2P_WO(entrypgdir)), %eax
entrypgdir在main.c中:
pde_t entrypgdir[NPDENTRIES] = { // Map VA's [0, 4MB) to PA's [0, 4MB) [0] = (0) | PTE_P | PTE_W | PTE_PS, // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB) [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,};
好了,答案很清晰。内核虚拟地址空间中,[0-4M)和[KERNBASE, KERNBASE+4MB)都映射到物理地址[0, 4MB)。这样就实现了从没有开启虚拟内存管理到开启虚拟内存管理的平滑过度。很妙!
转载地址:http://ksqxi.baihongyu.com/