实现内核

虚拟内存的分页

  • 因为64位模式的寻址范围为16EB,所以通常只使用第47~0位,寻址范围为248B = 256TB
    • 虚拟内存对应于虚拟地址(Virtual Address,VA),范围为16EB,通常只使用256TB
      • 操作系统使用最高的128TB(0xFFFF800000000000~,第63~48位为1)
      • 应用程序使用最低的128TB(~0x00007FFFFFFFFFFF,第63~48位为0)
    • 物理内存对应于物理地址(Physical Address,PA),范围为实际的内存大小,比如64GB
  • 在Linux中,VA = 0xFFFFFFFF80000000,映射到PA = 0x0
    • VA的第47~12位分为4个9位(511、510、0、0),作为4~1级页表的索引,由索引取出的页表项作为下一级页表的基址;第11~0位为PA的偏移
    • 页表的寻址范围为256TB(4级)、512GB(3级)、1GB(2级)、2MB(1级)
    • 一个页表项为64位,一个页表为29 * 8B = 4KB,它恰好等于1级页表项的寻址范围212B = 4KB。因此,页表可以按照4KB对齐
  • 虚拟内存的分页
    • 在保护模式下,重新加载段描述符表
      • 段选择子 –> 数据段索引(10)、全局描述符(0)、内核特权级(00),故段选择子为0x10
      • 段描述符表地址为gdt,它是.long数据(双字,32位)
    • 分配内核的四级页表
      • 实模式 –> 0x10000~0x1FFFF,保护模式 –> 0x20000~0x2FFFF,页表 –> 0x30000~
      • 操作系统使用64MB内存 –> 1个4级页表(0x30000)、1个3级页表(0x31000)、1个2级页表(0x32000)、32个1级页表(0x33000~0x52FFF)
    • 填充内核的四级页表
      • 4级页表(0x30000),索引511,写入0x31003
      • 3级页表(0x31000),索引510,写入0x32003
      • 2级页表(0x32000),索引0~31,写入0x33003~
      • 1级页表*32(0x33000~0x52FFF),索引0~511,写入0x3~
      • 在填充页表时,我们使用了循环。写入地址%edi和最终地址进行比较(cmp指令),如果前者小于等于后者,那么跳转(jle指令)到开始
    • 为长跳转到64位模式的代码,建立恒等映射的页表
      • 在开启虚拟内存的分页之后,我们需要使用ljmpl $0x8, $0x100000跳转到0x100000,所以需要建立恒等映射的页表
      • 1级页表的寻址范围为2M,跳转地址为1M,所以我们只需使用1个1级页表即可。因为4级、1级页表可以复用,所以我们增加了1个3级页表、1个2级页表
      • 4级页表(0x30000),索引0,写入0x53003
      • 3级页表(0x53000),索引0,写入0x54003
      • 2级页表(0x54000),索引0,写入0x33003(复用上面的1级页表)
// ----- boot32.S -----

.text
.code32
start32:
    // Segment selector
    mov $0x10, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    mov %ax, %ss

    // Load global descriptor table (in protected mode)
    lgdt gdtr

    // Level 4
    movl $(0x30000 + 511 * 8), %edi
    movl $0x31003, (%edi)

    // Level 3
    movl $(0x31000 + 510 * 8), %edi
    movl $0x32003, (%edi)

    // Level 2
    movl $0x32000, %edi
    movl $0x33003, %eax
fill_level_2:
    movl %eax, (%edi)
    add $8, %edi
    add $0x1000, %eax
    cmp $(0x32000 + 32 * 8 - 8), %edi
    jle fill_level_2

    // Level 1 * 32
    movl $0x33000, %edi
    movl $0x3, %eax
fill_level_1:
    movl %eax, (%edi)
    add $8, %edi
    add $0x1000, %eax
    cmp $(0x33000 + 32 * 512 * 8 - 8), %edi
    jle fill_level_1

    // Identity map, level 4
    movl $0x30000, %edi
    movl $0x53003, (%edi)

    // Identity map, level 3
    movl $0x53000, %edi
    movl $0x54003, (%edi)

    // Identity map, level 2
    movl $0x54000, %edi
    movl $0x33003, (%edi)

    // Enter 64-bit Mode...

gdt:
    .quad 0x0000000000000000
    .quad 0x00209a0000000000  // Code
    .quad 0x0000920000000000  // Data
gdt_end:

gdtr:
    .word gdt_end - gdt
    .long gdt

// Page tables, origin = 0x20000 + 0x10000
.org 0x10000
page_tables:
    .fill (3 + 32 + 2) * 512, 8, 0

进入64位模式

  • 进入64位模式
    • 开启64位寻址(控制寄存器CR4)
      • 第5位PAE置1
    • 控制寄存器CR3设置为0x30000
    • 开启64位模式(MSR寄存器EFER)
      • 第8位置1,开启64位模式
      • 第0位置1,开启系统调用
    • 开启分页(控制寄存器CR0)
      • 第31位PG置1
    • 长跳转到64位模式的代码
// ----- boot32.S, Enter 64-bit Mode... -----

// Physical address extension (PAE = 1 in CR4)
movl %cr4, %eax
btsl $5, %eax
movl %eax, %cr4

// Set CR3 to 0x30000
movl $0x30000, %eax
movl %eax, %cr3

// Enable 64-bit mode and system call
movl $0xc0000080, %ecx
rdmsr
btsl $8, %eax
btsl $0, %eax
wrmsr

// Paging (PG = 1 in CR0)
movl %cr0, %eax
btsl $31, %eax
movl %eax, %cr0

// Long jump to head64
ljmpl $0x8, $0x100000

使用C语言实现内核

  • 使用C语言实现内核
    • 在64位模式下,重新加载段描述符表
    • 创建栈空间
      • 栈空间大小为4KB
    • 压栈,跳转到main()函数
      • 我们需要从最低的0x0000…,跳转到最高的0xFFFF…。这是一个64位绝对地址跳转,只能通过pushq、ret实现
    • main()函数通过一个宏print(x),以及内联汇编,打印Hello World
      • 内联汇编使用__asm__(),括号内为汇编指令的字符串,%%表示转义字符%
      • 第一个冒号后为输出操作数,第二个冒号后为输入操作数(将x存入寄存器AX),第三个冒号后为汇编指令修改的寄存器和内存(修改了寄存器DX)
  • 我们还需要修改Makefile,以编译head64.S、main.c
    • kernel.bin,增加依赖system.bin
      • 使用dd命令,输入文件为system.bin,输出文件为kernel.bin,块大小为1B,不截断,偏移为960K = 0x100000 – 0x10000
    • 使用隐式规则,自动使用gcc编译head64.S、main.c,得到head64.o、main.o
      • CFLAGS指定gcc编译的标志,比如C语言标准为C11、不使用栈和控制流的保护
      • $^表示所有的依赖(比如head64.o、main.o),$@表示目标(比如system.bin)
// ----- head64.S -----

.text
.code64
start64:
    // Load global descriptor table (in 64-bit mode)
    lgdt gdtr

    // Create stack
    movq $task0_stack, %rsp

    // Push the address of main()
    pushq $main

    // Pop the address of main(), and jump to it
    ret

gdt:
    .quad 0x0000000000000000
    .quad 0x00209a0000000000  // Kernel Code
    .quad 0x0000920000000000  // Kernel Data
    .quad 0x0000000000000000  // User32 Code
    .quad 0x0000f20000000000  // User Data
    .quad 0x0020fa0000000000  // User64 Code
    .fill 128, 8, 0
gdt_end:

gdtr:
    .word gdt_end - gdt
    .quad gdt

    // Stack size = 4KB
    .fill 4096, 1, 0
task0_stack:



// ----- main.c -----

# define print(x) \
    __asm__( \
        "mov $0x3f8, %%dx\n" \
        "out %%ax, %%dx\n" \
        : \
        : "ax"(x) \
        : "dx" \
    )

int main() {
    print('H'); print('e'); print('l'); print('l'); print('o');
    print(' ');
    print('W'); print('o'); print('r'); print('l'); print('d');
    __asm__("hlt");
}



// ----- Makefile -----

// Build kernel image
kernel.bin: boot16.bin boot32.bin system.bin
    dd if=boot16.bin of=kernel.bin bs=1 conv=notrunc
    dd if=boot32.bin of=kernel.bin bs=1 conv=notrunc seek=64K
    dd if=system.bin of=kernel.bin bs=1 conv=notrunc seek=960K
...

// C Flags for compiling head64.S, main.c
CFLAGS = -std=c11 -fno-pic -mcmodel=kernel \
    -fno-stack-protector -fcf-protection=none

// System
system.bin: head64.o main.o
    ld $^ -Ttext=0xffffffff80100000 -o system.elf
    objcopy system.elf -O binary $@



// ----- Run Program -----

// Run kvmtool
$ make run
Hello World