练习一
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文件,主要编译选项有:
-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连接器
-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
第四部分
+ 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.o
和bootmain.o
链接到一起
-N
设置text和data部分可读可写,并且不对数据段进行对齐
-e start
将entry point设为start
-Ttext 0x7C00
将代码段的起始地址设为0x7C00
执行bin/sign obj/bootblock.out bin/bootblock
生成512字节的bootblock
最后两个字节设为0x55 0xAA
作为signature
用dd生成一个空镜像,然后将bootblock和kernel按次序写入
主引导扇区
标准的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按从低地址到高地址处理字符串
第二部分
lgdt gdtdesc
将gdtdesc所指向的6个字节内容读入GDTR
将控制寄存器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
加载ELF格式OS
上一步从硬盘读入内存的起始地址为0x10000,因此该地址也是ELF文件头的地址;
根据文件头提供的信息将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 elfhdr
和struct proghdr
表示了ELF文件头的结构