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

练习一

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等 将文件夹加入头文件搜索路径

第二部分

ld bin/kernel

ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o  obj/libs/string.o obj/libs/printfmt.o

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

  • -m elf_i386 模拟elf_i386连接器

  • -nostdlib 不使用标准库

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

第三部分

- cc boot/bootasm.S
  gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
- cc boot/bootmain.c
  gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
- cc tools/sign.c
  gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
  gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

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

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

第四部分

+ ld bin/bootblock
ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
  • 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读入内存中,实验过程如下

dd if=/dev/zero of=bootloader count=1
echo -ne "\x55\xaa" | dd seek=510 bs=1 of=bootloader
qemu-system-i386 bootloader

练习二

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

qemu-system-i386 -S -s -parallel stdio -hda ucore.img
gdb -q -tui -x /tools/gdbinit

即可进行单步调试

练习三

保护模式的进入

从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中提供的宏展开生成,一般描述符的结构如下

  31                23                15                7               0
 +-----------------+-+-+-+-+---------+-+-----+-+-----+-+-----------------+
 |                 | | | |A|         | |     | |     | |                 |
 |   BASE 31..24   |G|X|O|V| LIMIT   |P| DPL |1| TYPE|A|  BASE 23..16    | 4
 |                 | | | |L| 19..16  | |     | |     | |                 |
 |-----------------+-+-+-+-+---------+-+-----+-+-----+-+-----------------|
 |                                   |                                   |
 |        SEGMENT BASE 15..0         |       SEGMENT LIMIT 15..0         | 0
 |                                   |                                   |
 +-----------------+-----------------+-----------------+-----------------+

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的入口函数

练习五

输出如下

epb:0x00007b38 eip:0x00100967 arg: 0x00010074 0x00010074 0x00007b68 0x0010007f
    kern/debug/kdebug.c:306: print_stackframe+21
epb:0x00007b48 eip:0x00100c41 arg: 0x00000000 0x00000000 0x00000000 0x00007bb8
    kern/debug/kmonitor.c:125: mon_backtrace+10
epb:0x00007b68 eip:0x0010007f arg: 0x00000000 0x00007b90 0xffff0000 0x00007b94
    kern/init/init.c:48: grade_backtrace2+19
epb:0x00007b88 eip:0x001000a1 arg: 0x00000000 0xffff0000 0x00007bb4 0x00000029
    kern/init/init.c:53: grade_backtrace1+27
epb:0x00007ba8 eip:0x001000be arg: 0x00000000 0x00100000 0xffff0000 0x00100043
    kern/init/init.c:58: grade_backtrace0+19
epb:0x00007bc8 eip:0x001000df arg: 0x00000000 0x00000000 0x0010fd20 0x00103300
    kern/init/init.c:63: grade_backtrace+26
epb:0x00007be8 eip:0x00100050 arg: 0x00000000 0x00000000 0x00000000 0x00007c4f
    kern/init/init.c:28: kern_init+79
epb:0x00007bf8 eip:0x00007d6e arg: 0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
    <unknow>: -- 0x00007d6d --

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

练习六

中断描述符

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

   31                23                15                7                0
  +-----------------+-----------------+---+---+---------+-----+-----------+
  |           OFFSET 31..16           | P |DPL|0 1 1 1 0|0 0 0|(NOT USED) |4
  |-----------------------------------+---+---+---------+-----+-----------|
  |             SELECTOR              |           OFFSET 15..0            |0
  +-----------------+-----------------+-----------------+-----------------+

初始化中断描述符表

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

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

时钟中断处理

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

if (++ticks % TICK_NUM == 0) {
    print_ticks();
}

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

总结

实现与参考答案的区别

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

知识点

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

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

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

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

最后更新于