Skip to content

运行内核

在boot loader 加载完内核后,通过elf header的e_entry进入内核的入口

((void (*)(void)) (ELFHDR->e_entry))();

这个入口在'entry.S'文件中,这个文件完成的功能是开启分页模式,并告诉CPU页表的地址。值得注意的是因为我们的运行的代码是面向虚拟地址的,但是在开启分页模式之前,这些虚拟地址都还是无效的,所以要把虚拟地址转换为物理地址,这里的物理实际是线性地址,也就是前面在 boot loader 中讲到的32位寻址的段保护模式,这里的线性地址等于物理地址。V2P_WO 就是实现虚拟地址向物理地址转换的一个宏。看一下V2P_WO 的实现代码

#define V2P_WO(x) ((x) - KERNBASE) 

即,物理地址=虚拟地址-KERNBASE。为什么会是这样哪,实际上在boot loader的讲解中,我们已经通过命令‘readelf -l kernel’分析过kernel elf文件的Program Headers

$ readelf -l kernel
Elf file type is EXEC (Executable file)
Entry point 0x10000c
There are 3 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x80100000 0x00100000 0x07aab 0x07aab R E 0x1000
  LOAD           0x009000 0x80108000 0x00108000 0x02516 0x0d4a8 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

 Section to Segment mapping:
  Segment Sections...
   00     .text .rodata 
   01     .data .bss 
   02     

在这里可以发现两个可以load的段的虚拟地址和物理地址差了0x80000000,正是KERNBASE的值。实际上虚拟地址和物理地址的位置都是在kernel.ld文件中设置的

SECTIONS
{
    /* Link the kernel at this address: "." means the current address */
        /* Must be equal to KERNLINK */
    . = 0x80100000;

    .text : AT(0x100000) {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

    ......
}

开启 4MB 内存分页支持

这是通过设置寄存器 cr4 的 PSE 位来完成的。cr4 寄存器是个 32 位的寄存器目前只用到低 21 位,每一位的至位都控制着一些功能的状态,所以 cr4 寄存器又叫做控制寄存器。

PSE 位是 cr4 控制寄存器的第 5 位,当该位置为 1 时表示内存页大小为 4MB,当置为 0 时表示内存页大小为 4KB。

# Turn on page size extension for 4Mbyte pages
movl    %cr4, %eax
orl     $(CR4_PSE), %eax
movl    %eax, %cr4

cr3 寄存器中保存着页表所在的内存物理地址

  # Set page directory
  movl    $(V2P_WO(entrypgdir)), %eax
  movl    %eax, %cr3

cr0控制分页模式的开启,(在boot loader中讲过)。

 # Turn on paging.
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

页表entrypgdir的代码如下:

// The boot page table used in entry.S and entryother.S.
// Page directories (and page tables) must start on page boundaries,
// hence the __aligned__ attribute.
// PTE_PS in a page directory entry enables 4Mbyte pages.

__attribute__((__aligned__(PGSIZE)))
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,
};

每条页表的记录是32位大小,这是个只有一级的页面,所以它的前20位是页表索引,后12位代表的意义如下:

+ 11 + 10 + 9 + 8 +  7 + 6 + 5 +  4  +  3  + 2  + 1  + 0 +
|    Avail    | G | PS | D | A | PCD | PWT | US | RW | P |
+--------------------------------------------------------+
|     000     | 0 |  1 | 0 | 0 |  0  |  0  |  0 |  1 | 1 |
+--------------------------------------------------------+

P     : 0 表示此页不在物理内存中,1 表示此页在物理内存中
RW    : 0 表示只读,1 表示可读可写(要配合 US 位)
US    : 0 表示特权级页面,1 表示普通权限页面
PWT   : 1 表示写这个页面时直接写入内存,0 表示先写到缓存中
PCD   : 1 表示该页禁用缓存机制,0 表示启用缓存
A     : 当该页被初始化时为 0,一但进行过读/写则置为 1
D     : 脏页标记(这里就不做具体介绍了)
PS    : 0 表示页面大小为 4KB,1 表示页面大小为 4MB
G     : 1 表示页面为共享页面(这里就不做具体介绍了)
Avail : 3 位保留位

通过上面的页表可以看出,在KERNBASE到KERNBASE+4M之间的虚拟地址对应的页表记录的基地址是0,那么这一段的“物理地址=虚拟地址-KERNBASE”与前面V2P_WO的计算相同。而在0到4M之间的虚拟地址对应的页表记录的基地址也是0,所以这一段的虚拟地址与物理地址相同。 这样保证了在分页机制开启的情况下内核也可以正常运行,但也限制了内核最多只能使用 4MB 的内存,不过对于现在的内核来说 4MB 足够了。

设置内核栈顶位置

# Set up the stack pointer.
  movl $(stack + KSTACKSIZE), %esp

  ....

.comm stack, KSTACKSIZE

这里通过 .comm 在内核 bbs 段开辟了一段 KSTACKSIZE = 4096 = 4KB

此时内核的内存布局如下:

内核初始化

到这里我们已经可以正常访问虚拟内存而不需要手工转换了,最后就是进入main函数,进行一系列的内核初始化的工作。

# 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