实验一:系统软件启动过程

练习一

ucore.img的生成

运行make "V="后输出类似如下内容可分为四个部分

第一部分

- cc kern/init/init.c
  gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
- cc kern/libs/stdio.c
  gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
- cc kern/libs/readline.c
  gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
......

该部分将各个C文件编译为obj文件,主要编译选项有:

  • -m32 生成32位环境下的目标

  • -ggdb,-gsatbs 提供调试信息

  • -nostdinc 不搜索当前环境下的标准头文件

  • -Ikern/libs等 将文件夹加入头文件搜索路径

第二部分

该部分将上一步生成的obj文件链接到一起,产生bin/kernel文件

  • -m elf_i386 模拟elf_i386连接器

  • -nostdlib 不使用标准库

  • -T tools/kernel.ld 使用kernel.ld中的配置链接文件

第三部分

编译BootLoader,并生成将BootLoader补齐到512字节的工具bin/sign

  • -Os 提示编译器尽量减小目标码的体积

第四部分

  • bootasm.obootmain.o链接到一起

    • -N 设置text和data部分可读可写,并且不对数据段进行对齐

    • -e start 将entry point设为start

    • -Ttext 0x7C00 将代码段的起始地址设为0x7C00

  • 执行bin/sign obj/bootblock.out bin/bootblock生成512字节的bootblock

    • 不足510字节的部分补0

    • 最后两个字节设为0x55 0xAA作为signature

  • 用dd生成一个空镜像,然后将bootblock和kernel按次序写入

    • if=FILE 从FILE读入数据

    • of=FILE 输出到FILE

    • conv=notrunc 不截断输出文件

    • seek=1 跳过输出开头的512个字节

主引导扇区

标准的MBR分区表结构为前466字节为代码段,紧跟64字节的分区表,最后两个字节为Boot Signature

实际上第511和512个字节分别为0x55和0xAA即可被BIOS读入内存中,实验过程如下

练习二

直接make debug或手动在terminal中输入如下

即可进行单步调试

练习三

保护模式的进入

从BootLoader进入保护模式可分为三个部分

第一部分

  • 进行基本设置

    • cli (clear interrupt flag) 使CPU不再接受外部中断

    • cld (clear direction flag) 使CPU按从低地址到高地址处理字符串

    • 将若干寄存器置0

  • 打开A20 Gate

    • 等待直到8042芯片的输入缓存为空

    • 向0x64端口发送0xD1,意为对P2端口写数据

    • 等待直到8042芯片的输入缓存为空

    • 向0x64端口发送0xDF,打开A20 Gate

第二部分

  • lgdt gdtdesc 将gdtdesc所指向的6个字节内容读入GDTR

    • 低2位的0x17表明表的大小为24

    • 高4位为表的起始地址

  • 将控制寄存器CR0的PE (Protection Enable, bit 0)设为1

  • ljmp $PROT_MODE_CSEG, $protcseg 将cs寄存器设为$PROT_MODE_CSEG,将eip设为protcseg所指地址

其中GDT由asm.h中提供的宏展开生成,一般描述符的结构如下

SEG_NULLASM会产生一个空描述符,SEG_ASM(type,base,lim)会根据参数生成所需的描述符,注意这里取了lim的高20位作为LIMIT。

此处描述符定义的代码段和数据段都是从0x0开始,容量4GB的段。

第三部分

除了cs外的段寄存器都指向数据段,将epb置0,将栈顶指向BootLoader起始处(0x7c00),调用bootmain

练习四

读硬盘扇区

BootLoader中读取硬盘的功能是基于static void readsect(void *dst, uint32_t secno)函数实现的,该函数可以实现读一个扇区,反复调用可以将kernel完整读入内存。大致过程如下

  • 等待磁盘准备好

    • 从0x1F7读入状态码,检查BSY和RDY两个bit

  • 向0x1F2端口发送读取的扇区数

  • 传送LBA的各个位,发送读取命令

  • 等待磁盘准备好

  • 读入数据

加载ELF格式OS

  • 首先读入8个扇区,将ELF文件头读入;

  • 上一步从硬盘读入内存的起始地址为0x10000,因此该地址也是ELF文件头的地址;

  • 根据文件头提供的信息将kernel读入正确的内存地址

  • 调用kernel的入口函数

练习五

输出如下

最后一行的0x7d6d = 0x7d6c + 1,而打开bootblock.asm可以看到0x7d6c为调用kernel入口的最后一句指令,所以0x7d6d实际上是调用call指令后压栈的返回地址

练习六

中断描述符

每个终端描述符占8个字节空间,其中16-31位的bits为段选择子,0-15和48-63位的bits分别为偏移地址的两个字,大致结构如下

初始化中断描述符表

对idt数组中的每一个终端描述符调用SETGATE进行设置,要注意段描述符应该与voidpmm_init(void)中设置的内核代码段描述符地址(0x8)一致。

由于要支持应用程序发出软中断int 0x80,需特别将T_SYSCALL的DPL设为3。

时钟中断处理

static void trap_dispatch(struct trapframe *tf)中处理时钟中断部分添加如下代码

每次收到中断即将全局变量ticks加1,当为100整数倍时调用print_ticks

总结

实现与参考答案的区别

参考答案限制了print_stackframe的最大深度

知识点

  • BIOS启动过程:加电后,CPU执行物理地址0xFFFFFFF0处的跳转指令,开始执行BIOS程序

  • 保护模式的进入:打开A20地址线->设置全局描述符表->打开保护模式

  • 分段机制:逻辑地址可分为段选择子和偏移量,通过段选择子可以找到段描述符,从中取出段基址加上偏移量可得到线性地址,未启用分页机制时即为物理地址

  • ELF文件格式:ucore中的struct elfhdrstruct proghdr表示了ELF文件头的结构

最后更新于

这有帮助吗?