从 0 开始写一个操作系统
作者:解琛
时间:2020 年 8 月 30 日
三、Bootloader 的实现
安装开发环境。
sudo apt install build-essential git qemu-system-x86 vim-gnome gdb make diffutils exuberant-ctags tmux openssh-server cscope meld
3.1 项目整体框架
项目完整的代码见:lab1_result
.
├── boot
│ ├── asm.h
│ ├── bootasm.S
│ └── bootmain.c
├── kern
│ ├── debug
│ │ ├── assert.h
│ │ ├── kdebug.c
│ │ ├── kdebug.h
│ │ ├── kmonitor.c
│ │ ├── kmonitor.h
│ │ ├── panic.c
│ │ └── stab.h
│ ├── driver
│ │ ├── clock.c
│ │ ├── clock.h
│ │ ├── console.c
│ │ ├── console.h
│ │ ├── intr.c
│ │ ├── intr.h
│ │ ├── kbdreg.h
│ │ ├── picirq.c
│ │ └── picirq.h
│ ├── init
│ │ └── init.c
│ ├── libs
│ │ ├── readline.c
│ │ └── stdio.c
│ ├── mm
│ │ ├── memlayout.h
│ │ ├── mmu.h
│ │ ├── pmm.c
│ │ └── pmm.h
│ └── trap
│ ├── trap.c
│ ├── trapentry.S
│ ├── trap.h
│ └── vectors.S
├── libs
│ ├── defs.h
│ ├── elf.h
│ ├── error.h
│ ├── printfmt.c
│ ├── stdarg.h
│ ├── stdio.h
│ ├── string.c
│ ├── string.h
│ └── x86.h
├── Makefile
└── tools
├── function.mk
├── gdbinit
├── grade.sh
├── kernel.ld
├── lab1init
├── moninit
├── sign.c
└── vector.c
10 directories, 48 files
3.1.1 bootloader
boot/bootasm.S
- 定义并实现了 bootloader 最先执行的函数 start;
- 此函数进行了一定的初始化;
- 完成了从实模式到保护模式的转换;
- 调用 bootmain.c 中的 bootmain 函数;
boot/bootmain.c
- 定义并实现了 bootmain 函数实现了通过屏幕、串口和并口显示字符串;
- bootmain 函数加载 ucore 操作系统到内存,然后跳转到 ucore 的入口处执行;
boot/asm.h
- 是 bootasm.S 汇编文件所需要的头文件,主要是一些与 X86 保护模式的段访问方式相关的宏定义。
3.1.2 ucore 操作系统
3.1.2.1 系统初始化
kern/init/init.c
:ucore 操作系统的初始化启动代码。
3.1.2.2 内存管理
kern/mm/memlayout.h
:ucore 操作系统有关段管理(段描述符编号、段号等)的一些宏定义;kern/mm/mmu.h
:ucore 操作系统有关 X86 MMU 等硬件相关的定义;- 包括 EFLAGS 寄存器中各位的含义;
- 应用、系统段类型;
- 中断门描述符定义;
- 段描述符定义;
- 任务状态段定义;
- NULL 段声明的宏 SEG_NULL;
- 特定段声明的宏 SEG;
- 设置中断门描述符的宏 SETGATE;
kern/mm/pmm.[ch]
:设定了 ucore 操作系统在段机制中要用到的全局变量;- 任务状态段 ts;
- 全局描述符表 gdt[];
- 加载全局描述符表寄存器的函数 lgdt;
- 临时的内核栈 stack0;
- 对全局描述符表和任务状态段的初始化函数 gdt_init。
3.1.2.3 外设驱动
kern/driver/intr.[ch]
:实现了通过设置 CPU 的 eflags 来屏蔽和使能中断的函数;kern/driver/picirq.[ch]
:实现了对中断控制器 8259A 的初始化和使能操作;kern/driver/clock.[ch]
:实现了对时钟控制器 8253 的初始化操作;kern/driver/console.[ch]
:实现了对串口和键盘的中断方式的处理操作。
3.1.2.4 中断处理
kern/trap/vectors.S
:包括 256 个中断服务例程的入口地址和第一步初步处理实现;- 此文件是由
tools/vector.c
在编译 ucore 期间动态生成的;
- 此文件是由
kern/trap/trapentry.S
:- 紧接着第一步初步处理后,进一步完成第二步初步处理;
- 并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
kern/trap/trap.[ch]
:紧接着第二步初步处理后,继续完成具体的各种中断处理操作。
3.1.2.5 内核调试
kern/debug/kdebug.[ch]
:提供源码和二进制对应关系的查询功能,用于显示调用栈关系;kern/debug/kmonitor.[ch]
:实现提供动态分析命令的 kernel monitor,便于在 ucore 出现 bug 或问题后,能够进入 kernel monitor 中,查看当前调用关系;kern/debug/panic.c | assert.h
:提供了 panic 函数和 assert 宏,便于在发现错误后,调用 kernel monitor;- 可以充分利用 assert 宏和 panic 函数,提高查找错误的效率。
3.1.3 公共库
libs/defs.h
:包含一些无符号整型的缩写定义;libs/x86.h
:一些用 GNU C 嵌入式汇编实现的 C 函数(由于使用了 inline 关键字,所以可以理解为宏)。
3.1.4 工具
Makefile 和 function.mk
:指导 make 完成整个软件项目的编译,清除等工作;sign.c
:一个 C 语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区;tools/vector.c
:生成vectors.S,此文件包含了中断向量处理的统一实现。
3.2 bootloader 进入保护模式
完整的代码见 boot/bootasm.S。
定义基本的段信息和标志位。
#include <asm.h>
.set PROT_MODE_CSEG, 0x08 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x01 # protected mode enable flag
机器上电,从 %cs=0
$pc=0x7c00
,进入后:
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
3.2.1 清理环境
首先,清理环境,包括将 flag 置 0 和将段寄存器置 0。
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
3.2.2 使能 A20
开启 A20:通过将键盘控制器上的 A20 线置于高电位,全部 32 条地址线可用,可以访问 4G 的内存空间。
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64,发送写8042输出端口的指令;
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60,打开 A20;
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
3.2.3 初始化 GDT 表
一个简单的 GDT 表和其描述符已经静态储存在引导区中,载入即可。
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc
在文件的最后定义 gdtdesc。
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for boo