虚拟内存的分页
- 因为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)
- 控制寄存器CR3设置为0x30000
- 开启64位模式(MSR寄存器EFER)
- 第8位置1,开启64位模式
- 第0位置1,开启系统调用
- 开启分页(控制寄存器CR0)
- 长跳转到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位模式下,重新加载段描述符表
- 创建栈空间
- 压栈,跳转到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