Lab 1
实验介绍
操作系统也是一个软件,需要通过Bootloader(小于512字节,这样才能放入硬盘的主引导扇区)加载启动,lab1自带了一个bootloader和微型ucore。
Bootloader具备如下功能:
- 切换到X86保护模式
- 读取磁盘并加载ELF格式(Executable and Linkable Format)的文件
- 显示字符
lab1 的ucore具备如下功能:
- 处理时钟中断
- 显示字符
实验内容
练习1、make生成img镜像过程
make命令将自动执行当前目录下的MakeFile,常用命令:
- make V= 执行MakeFile,并显示具体执行内容
- make clean 清理通过make命令生成的各种文件
ucore.img生成流程如下(为不影响阅读体验,make输出的内容贴在附录一):
- 生成kernel:编译kern和libs目录下的所有.c文件,生成同名.o文件;然后通过模拟elf_i386链接器,并使用tools/kernel.ld作为链接器脚本将上述.o文件链接成bin/kernel可执行文件
- 生成sign:编译tools/sign.c生成bin/sign可执行文件;外部执行程序,用于生成虚拟硬盘主引导扇区;
- 生成bootblock:编译boot目录(这个目录即bootloader)下的bootmain.c文件和bootasm.S,生成同名.o文件;通过模拟elf_i386链接器,并使用使用-Ttext 0x7c00(不太理解,恳请大佬解惑)作为链接器脚本,将boot目录下的.o文件链接成512字节大小(一个块)bin/bootblock(真实计算机中引导块一般烧写在BIOS中,不可更改,其中包含用于引导的最小指令集)
- 生成ucore.img:使用/dev/zero设备文件将ucore.img扩充为10000个块的空文件;将bootblock和kernel依次写入ucore.img
查看sign.c和生产的bin/bootblock文件,可知符合系统规范的硬盘主引导扇区(即引导块)特征如下:
- 大小为一个块(512字节)
- 最后两个字符为0x55 0xAA
练习2、使用qemu执行并调试lab1的ucore.img
lab1目录下执行如下指令即可正常启动镜像:
qemu-system-i386 bin/ucore.img
如需边运行qemu,边单步调试,在lab1目录下执行如下指令:
make debug
该命令会在启动qemu的同时进入gdb的调试模式,调试模式的断电设置以及其他信息在tools/gdbinit文件中定义:
file bin/kernel
target remote :1234
set architecture i8086
b *0x7c00
continue
x /2i $pc # gdb x(examine)查看内存地址的值,i(info)查看寄存器信息,pc为指令计数器(存储下一条要执行的指令)
# break kern_init
# continue
执行后跳出qemu和新的terminal窗口,此时qemu在第一条输出暂停"Booting from Hard Disk…",新terminal上输出如下
The target architecture is assumed to be i8086
Breakpoint 1 at 0x7c00
Breakpoint 1, 0x00007c00 in ?? ()
=> 0x7c00: cli # Clear interrupt 屏蔽中断,与sti相对
0x7c01: cld # Clear direction 使传送方向从低地址向高地址,与std相对
# 接下来交替输入si和x/i $pc,逐条指令执行,可以看到输出内容和boot/bootasm.s从0x7c00开始的汇编代码相同(源码分析参见附录四、bootasm.s)
练习3、分析bootloader进入保护模式过程
以下练习答案均可基于bootasm.S得知
为何开启A20,如何开启A20
开启A20地址线后才可以完成从实模式到保护模式的转换。A20本来是为了对下兼容20位地址线设备,最多访问到1MB,超出即回滚。
开启A20 Gate后,关闭上述功能,才能在实模式下访问高端内存区,保护模式同样需要开启A20 Gate。
如何初始化GDT表
GDT (Global Descriptor Table) 全局描述符表,进入保护模式后使用分段式内存需要GDT,所有任务可见且唯一,含有2^13个段描述符,段描述符维护了每个分段的基址,引用段描述符时必须知道GDT入口。
每个分段包含逻辑地址VA、段描述符(表)、段选择子(段寄存器,用于定位段描述符在GDT中索引)。
VA转换为PA过程如下图,如果不采用分页机制,则下面的线性地址即物理地址;否则需要再一次转换。线性地址长度为32位。
如何使能和进入保护模式
进入32位模式,通过ljmp指令将保护模式代码段长跳转到相应逻辑地址($ PROT_MODE_CSEG = 0X8)。将数据段选择子放到DS ES FS GS SS段寄存器上,设置栈指针ebp esp开辟好0~0x7c00的栈后,最后调用bootmain函数。cr0寄存器置1($CR0_PE_ON = 0x1)即进入保护模式。
练习4、分析bootloader加载ELF格式的OS的过程
参见附录bootmain.c源码的注释解析
练习5、实现函数调用堆栈跟踪函数
kern/debug/kdebug.c用于内核调试,提供源码和二进制对应关系的查询功能,显示调用栈关系,练习任务为补全print_stackframe函数。
void
print_stackframe(void) {
/* LAB1 YOUR CODE : STEP 1 */
/* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
* (2) call read_eip() to get the value of eip. the type is (uint32_t);
* (3) from 0 .. STACKFRAME_DEPTH
* (3.1) printf value of ebp, eip
* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
* (3.3) cprintf("\n");
* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
* (3.5) popup a calling stackframe
* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]
* the calling funciton's ebp = ss:[ebp]
*/
// 获取ebp、eip寄存器内的值
uint32_t ebp = read_ebp();
uint32_t eip = read_eip();
int i, j;
for(i = 0; i < STACKFRAME_DEPTH && ebp != 0; i++) {
cprintf("ebp:0x%08x eip:0x%08x", ebp, eip);
uint32_t *arg = (uint32_t *)ebp + 2; // 参数列表在ebp + 2往栈底(高地址)方向
cprintf(" arg:");
for(j = 0; j < 4; j++) {
cprintf("0x%08x ", arg[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);·// 输出C调用函数名称和行号等
eip = ((uint32_t *)ebp)[1]; // 将下一条指令设为ebp + 1,即返回地址
ebp = ((uint32_t*)ebp)[0]; // ebp读取上一级栈基址,回到调用函数栈
}
}
qemu运行镜像时,相关输出如下:
ebp:0x00007b08 eip:0x001009a6 arg:0x00010094 0x00000000 0x00007b38 0x00100092
kern/debug/kdebug.c:306: print_stackframe+21
ebp:0x00007b18 eip:0x00100ca1 arg:0x00000000 0x00000000 0x00000000 0x00007b88
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b38 eip:0x00100092 arg:0x00000000 0x00007b60 0xffff0000 0x00007b64
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b58 eip:0x001000bb arg:0x00000000 0xffff0000 0x00007b84 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b78 eip:0x001000d9 arg:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007b98 eip:0x001000fe arg:0x0010349c 0x00103480 0x0000130a 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007bc8 eip:0x00100055 arg:0x00000000 0x00000000 0x00000000 0x00010094
kern/init/init.c:28: kern_init+84
ebp:0x00007bf8 eip:0x00007d68 arg:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d67 --
注意到最后两行的ebp=0x7bf8,刚好在0x7c00前4位,由之前bootloader(确切来说是)设置的堆栈从0x7c00开始,转入bootmain函数,可知call bootmain的call指令压栈,故bootmain()栈帧的ebp为0x7bf8。
练习6、完善中断初始化和处理
中断相关功能在kern/trap下,其下有如下文件:
- vectors.s中定义了256个(保护模式下允许的最大值)中断服务例程的入口地址,其中[0,31]用于异常和NMI,[32, 255]保留给用户定义。该文件由tools/vector.c编译ucore时动态生成
- trapentry.s(中断入口)将ds es fs gs段寄存器压栈构成类似struct的中断栈帧,将GD_KDATA存入ds es来为内核配置数据段,并将esp压栈作为参数以调用trap.c->trap(esp),调用结束后逆过程依次出栈、恢复各段寄存器(恢复中断上下文),最后通过addl $0x8 %esp避开中断号和错误代码
- trap.c/trap.h中由trapentry.s调用的trap(struct trapframe *tf)函数只有一个操作:调用trap_dispatch(tf),该函数会基于中断类型(tf->tf_trapno,有时钟中断、COM1串口中断、键盘输入中断)分配不同操作,如果不是已知类型,则会调用print_trapframe(tf)并报错
- idt_init()初始化中断描述符表,即初始化在vectors.S定义的各个中断入口;中断描述符表把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8字节的描述符数组,但IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8做为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。
- lab1中trapentry.s发来的中断类型为IRQ_OFFSET+IRQ_TIMER(时钟中断),此时会调用print_ticks(),即在qemu输出TICK_NUM
中断描述符表(保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
查看kern/mm/mmu.h中gatedesc定义(interrupt和trap的门描述符),将其中各值相加得64bit,故为8字节;并且得知gd_ss所在的[16, 31]为段选择子,由此得知GDT中对应段的基地址,加上gd_off_15_0, gd_off_31_16组成的偏移量获得中断处理程序的入口地址。
/* Gate descriptors for interrupts and traps */
struct gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_ss : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init
idt_init()初始化中断描述符表,即初始化在vectors.S定义的各个中断入口
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item