构建内核映像
- GNU工具链
- 自动化构建工具 –> make
- 编译器 –> gcc
- 构建内核映像
- 编写Makefile,使用自动化构建工具make
- Makefile的缩进必须使用制表符(tab键),而不是空格
- make –> 构建内核,包括编译、汇编、链接等
- make run –> 运行内核
- make clean –> 清理文件
- 因为run、clean不是真正的构建命令,所以将其作为伪命令.PHONY
- 内核映像的组成部分
- boot16.bin –> 16位模式,或者实模式
- boot32.bin –> 32位模式,或者保护模式
- 链接地址
- boot16.bin –> 加载地址0x10000,实模式基址0x10000,所以链接地址-Ttext=0x0
- boot32.bin –> 加载地址0x20000,保护模式基址0x0,所以链接地址-Ttext=0x20000
- 内核映像的组装
- kernel.bin为目标,boot16.bin、boot32.bin为依赖
- 使用dd命令,输入文件为boot16.bin,输出文件为kernel.bin,块大小为1B,不截断
- 使用dd命令,输入文件为boot32.bin,输出文件为kernel.bin,块大小为1B,不截断,偏移为64K = 0x20000 – 0x10000
- boot16.bin为目标,boot16.S为依赖
- boot32.bin为目标,boot32.S为依赖
// ----- Makefile -----
// Build kernel image
kernel.bin: boot16.bin boot32.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
// Real mode
boot16.bin: boot16.S
gcc -c boot16.S -o boot16.o
ld boot16.o -Ttext=0x0 -o boot16.elf
objcopy boot16.elf -O binary boot16.bin
// Protected mode
boot32.bin: boot32.S
gcc -c boot32.S -o boot32.o
ld boot32.o -Ttext=0x20000 -o boot32.elf
objcopy boot32.elf -O binary boot32.bin
.PHONY: run clean
// Run kvmtool
run: kernel.bin
./kvmtool/lkvm run -c 1 -k kernel.bin
// Remove files
clean:
rm -f *.o *.elf *.bin
实模式、保护模式、64位模式
- 三种模式的内存寻址
- 实模式 –> 段寄存器、段偏移,寻址范围为24 * 216B = 1MB
- 保护模式 –> 段描述符,寻址范围为232B = 4GB
- 64位模式 –> 虚拟内存的分页,寻址范围为264B = 16EB
- 实模式可以访问所有内存,保护模式只能访问受保护的内存。二者只是快速过渡,以保证X86指令集的内存寻址从16位到64位是兼容的,最终需要进入64位模式开启虚拟内存的分页
- 实模式
- 操作系统启动(Boot)之后,代码段寄存器CS = 0x1000,指令指针寄存器IP = 0x0000。因此,第一条指令的内存地址计算如下
- 基址 –> CS << 4,偏移 –> IP
- 0x1000 << 4 + 0x0000 = 0x10000
- 内存布局0x0~0xFFFFF(1MB)
- 0x0~0xFFFF(64KB)
- 0x10000~0x1FFFF(128KB)
- 0x20000~0x7FFFF(512KB)
- 0x80000~0xFFFFF(1MB)
- 扩展BIOS数据区、显存、显卡BIOS、扩展BIOS、主板BIOS
- 0x100000~
- 从实模式到保护模式
- 关闭中断(cli指令)
- 加载段描述符表(lgdt指令)
- 段描述符表长度为gdt_end – gdt,地址为0x1000 << 4 + gdt,它们都是.word数据(字,16位)
- 第一个段描述符为0,第二个段描述符为代码段,第三个段描述符为数据段,它们都是.quad数据(四字,64位)
- 段描述符包含基址等信息,比如我们将保护模式的基址,全部设置为0x0
- 开启保护模式(控制寄存器CR0)
- 长跳转到保护模式的代码(ljmpl指令)
- 此时已经为保护模式,需要按照段描述符寻址
- ljmpl指令的第一个参数为段选择子 –> 代码段索引(1)、全局描述符(0)、内核特权级(00),故段选择子为0x8
- 指令后缀表示操作的位数,比如movw(w,16位)、ljmpl(l,32位)、pushq(q,64位)
// ----- boot16.S -----
.text
.code16
.start16:
// Clear interrupt flag
cli
// Load global descriptor table
lgdt gdtr
// Protected mode enable (PE = 1 in CR0)
mov %cr0, %eax
or $0x1, %eax
mov %eax, %cr0
// Long jump to boot32
ljmpl $0x8, $0x20000
gdt:
.quad 0x0000000000000000
.quad 0x00c09a00000007ff // Code
.quad 0x00c09200000007ff // Data
gdt_end:
gdtr:
.word gdt_end - gdt
.word gdt, 0x1
// ----- boot32.S -----
.text
.code32
.start32:
mov $'A', %al
mov $0x3f8, %dx
out %al, %dx
hlt
// ----- Run Program -----
// Build kernel image
$ make
// Run kvmtool
$ make run
A