ucore Lab1
[练习1]
[练习1.1] 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
输入make V= (对每条命令进行了精简)
+ cc kern/init/init.c //编译init.c
gcc -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c //编译readline.c
gcc -c kern/libs/readline.c -o
obj/kern/libs/readline.o
+ cc kern/libs/stdio.c //编译stdlio.c
gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c //编译kdebug.c
gcc -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c //编译komnitor.c
gcc -c kern/debug/kmonitor.c -o
obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c //编译panic.c
gcc -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c //编译clock.c
gcc -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c //编译console.c
gcc -c kern/driver/console.c -o
obj/kern/driver/console.o
+ cc kern/driver/intr.c //编译intr.c
gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c //编译prcirq.c
gcc -c kern/driver/picirq.c -o
obj/kern/driver/picirq.o
+ cc kern/trap/trap.c //编译trap.c
gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S //编译trapentry.S
gcc -c kern/trap/trapentry.S -o
obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S //编译vectors.S
gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c //编译pmm.c
gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c //编译printfmt.c
gcc -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c //编译string.c
gcc -c libs/string.c -o obj/libs/string.o
+ ld bin/kernel //链接成kernel
ld -o bin/kernel
obj/kern/init/init.o obj/kern/libs/readline.o
obj/kern/libs/stdio.o obj/kern/debug/kdebug.o
obj/kern/debug/kmonitor.o obj/kern/debug/panic.o
obj/kern/driver/clock.o obj/kern/driver/console.o
obj/kern/driver/intr.o obj/kern/driver/picirq.o
obj/kern/trap/trap.o obj/kern/trap/trapentry.o
obj/kern/trap/vectors.o obj/kern/mm/pmm.o
obj/libs/printfmt.o obj/libs/string.o
+ cc boot/bootasm.S //编译bootasm.c
gcc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c //编译bootmain.c
gcc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c //编译sign.c
gcc -c tools/sign.c -o obj/sign/tools/sign.o
gcc -O2 obj/sign/tools/sign.o -o bin/sign
+ ld bin/bootblock //根据sign规范生成bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o
-o obj/bootblock.o
//创建大小为10000个块的ucore.img,初始化为0,每个块为512字节
dd if=/dev/zero of=bin/ucore.img count=10000
//把bootblock中的内容写到第一个块
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
//从第二个块开始写kernel中的内容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
从上面并没有看到:根据sign规范生成bootblock的命令
查看makefile文件找到:
@$(call totarget,sign) $(call outfile,bootblock)
$(bootblock)
所以从上面可以看出ucore.img的生成过程:
1 编译所有生成bin/kernel所需的文件
2 链接生成bin/kernel
3 编译bootasm.S bootmain.c sign.c
4 根据sign规范生成obj/bootblock.o
5 生成ucore.img
练习2: 使用qemu执行并调试lab1中的软件
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行
$ cd labcodes_answer/lab1_result/
$ make lab1-mon
在初始化位置0x7c00设置实地址断点,测试断点正常
b *0x7c00
c
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较
可以发现代码是一样的
练习3:分析bootloader进入保护模式的过程。
1 关中断和清除数据段寄存器
.globl start
start:
.code16
cli //关中断
cld //清除方向标志
xorw %ax, %ax //ax清0
movw %ax, %ds //ds清0
movw %ax, %es //es清0
movw %ax, %ss //ss清0
[练习3.1] 为何开启A20,以及如何开启A20?
初始时A20为0,访问超过1MB的地址时,就会从0循环计数,将A20地址线置为1之后,才可以访问4G内存。A20地址位由8042控制,8042有2个有两个I/O端口:0x60和0x64。
打开流程:
等待8042 Input buffer为空;
发送Write 8042 Output Port (P2)命令到8042 Input buffer;
等待8042 Input buffer为空;
将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer;
seta20.1: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.1 //al的第2位为0,则跳出循环
movb $0xd1, %al //将0xd1写入al中
outb %al, $0x64 //将0xd1写入到0x64端口中
seta20.2: //等待8042键盘控制器不忙
inb $0x64, %al //从0x64端口中读入一个字节到al中
testb $0x2, %al //测试al的第2位
jnz seta20.2 //al的第2位为0,则跳出循环
movb $0xdf, %al //将0xdf入al中
outb %al, $0x60 //将0xdf入到0x64端口中,打开A20
[练习3.2] 如何初始化GDT表?
1 载入GDT表
lgdt gdtdesc //载入GDT表
2 进入保护模式:
通过将cr0寄存器PE位置1便开启了保护模式
cro的第0位为1表示处于保护模式
movl %cr0, %eax //加载cro到eax
orl $CR0_PE_ON, %eax //将eax的第0位置为1
movl %eax, %cr0 //将cr0的第0位置为1
3 通过长跳转更新cs的基地址:
上面已经打开了保护模式,所以这里需要用到逻辑地址。$PROT_MODE_CSEG的值为0x80
ljmp $PROT_MODE_CSEG, $protcseg
.code32
protcseg:
4 设置段寄存器,并建立堆栈
movw $PROT_MODE_DSEG, %ax //
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $0x0, %ebp //设置帧指针
movl $start, %esp //设置栈指针
5 转到保护模式完成,进入boot主方法
call bootmain //调用bootmain函数
[练习3.3] 如何使能和进入保护模式
将cr0寄存器置1
练习4:分析bootloader加载ELF格式的OS的过程
阅读bootmain.c源码
这是一个简单的启动装载程序,唯一的工作就是启动来自第一个IDE硬盘的ELF内核映像。
磁盘布局
此程序(bootasm.S和bootmain.c)是引导加载程序.应该存储在磁盘的第一个扇区中。第二个扇区开始保存内核映像。内核映像必须为ELF格式。
启动步骤
当CPU启动时,它将BIOS加载到内存中并执行,BIOS初始化设备,中断例程集以及读取引导设备的第一个扇区(例如,硬盘驱动器)进入内存并跳转到它。假设此引导加载程序存储在该引导程序的第一个扇区中控制从bootasm.S开始-设置保护模式,和一个堆栈,然后运行C代码,然后调用bootmain()该文件中的bootmain()会接管,读取内核并跳转到该内核。
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4);
}
在linux的驱动程序中,都会使用大量的outb、outw、inb、inw等等宏来訪问硬件或寄存器。outb() I/O 上写入 8 位数据 ( 1 字节 )。inb() I/O 上读入 8 位数据 ( 1 字节 )。
ELF文件格式
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
用于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。 这也是本实验的OS文件类型。
用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
struct elfhdr {
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry; // 程序入口的虚拟地址
uint phoff; // program header 表的位置偏移
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum; //program header表中的入口数目
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
program header描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。可执行文件的程序头部是一个program header结构的数组, 每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的 “段” 包含一个或者多个 “节区”(section) ,也就是“段内容(Segment Contents)” 。程序头部仅对于可执行文件和共享目标文件有意义。可执行目标文件在ELF头部的e_phentsize和e_phnum成员中给出其自身程序头部的大小。程序头部的数据结构如下表所示:
struct proghdr {
uint type; // 段类型
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;
uint filesz;
uint memsz; // 段在内存映像中占用的字节数
uint flags;
uint align;
};
练习5:实现函数调用堆栈跟踪函数read_eip() 函数定义在kdebug.c中:
noinline uint32_t
read_eip(void) {
uint32_t eip;
asm volatile("movl 4(%%ebp), %0" : "=r" (eip));
return eip;
}
read_ebp() 函数定义在x86.h中:
static inline uint32_t
read_ebp(void) {
uint32_t ebp;
asm volatile ("movl %%ebp, %0" : "=r" (ebp)); //内联汇编,读取edp寄存器的值到变量ebp
return ebp; //返回ebp的值
}
执行make qume结果如下
ebp:0x00007b38 eip:0x00100bf2 args:0x00010094 0x0010e950 0x00007b68 0x001000a2
kern/debug/kdebug.c:297: print_stackframe+48
ebp:0x00007b48 eip:0x00100f40 args:0x00000000 0x00000000 0x00000000 0x0010008d
kern/debug/kmonitor.c:125: mon_backtrace+23
ebp:0x00007b68 eip:0x001000a2 args:0x00000000 0x00007b90 0xffff0000 0x00007b94
kern/init/init.c:48: grade_backtrace2+32
ebp:0x00007b88 eip:0x001000d1 args:0x00000000 0xffff0000 0x00007bb4 0x001000e5
kern/init/init.c:53: grade_backtrace1+37
ebp:0x00007ba8 eip:0x001000f8 args:0x00000000 0x00100000 0xffff0000 0x00100109
kern/init/init.c:58: grade_backtrace0+29
ebp:0x00007bc8 eip:0x00100124 args:0x00000000 0x00000000 0x00000000 0x0010379c
kern/init/init.c:63: grade_backtrace+37
ebp:0x00007be8 eip:0x00100066 args:0x00000000 0x00000000 0x00000000 0x00007c4f
kern/init/init.c:28: kern_init+101
ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d6d --
[练习6]完善中断初始化和处理
中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
一个表项的结构如下:
/*lab1/kern/mm/mmu.h*/
/* 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
};
中断处理过程:
可以看到,中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成偏移量,
通过段选择子去GDT中找到对应的基地址,然后基地址加上偏移量就是中断处理程序的地址。
编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。
SETGATE函数的实现:
#define SETGATE(gate, istrap, sel, off, dpl) { \
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
(gate).gd_ss = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
}
宏定义和数组说明:
#define GD_KTEXT ((SEG_KTEXT) << 3) // kernel text
#define DPL_KERNEL (0)
#define DPL_USER (3)
#define T_SWITCH_TOK 121 // user/kernel switch
static struct gatedesc idt[256] = {{0}};
dt_init函数的实现:
void
idt_init(void) {
extern uintptr_t __vectors[]; //保存在vectors.S中的256个中断处理例程的入口地址数组
int i;
//使用SETGATE宏,对中断描述符表中的每一个表项进行设置
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { //IDT表项的个数
//在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT,
__vectors[T_SWITCH_TOK], DPL_USER);
//建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。
lidt(&idt_pd);
}
编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数
首先加入 string.h头文件,为了使用memmove函数
void *memmove(void *dst, const void *src, size_t n);
定义变量:
struct trapframe switchk2u, *switchu2k;
结构体 trapframe
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
宏定义:
#define IRQ_OFFSET 32
#define IRQ_TIMER 0
#define IRQ_KBD 1
#define IRQ_COM1 4
#define T_SWITCH_TOU 120
#define USER_CS ((GD_UTEXT) | DPL_USER)
#define USER_DS ((GD_UDATA) | DPL_USER)
#define KERNEL_DS ((GD_KDATA) | DPL_KERNEL)
#define TICK_NUM 100
print_ticks函数
static void print_ticks() {
cprintf("%d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADE
cprintf("End of Test.\n");
panic("EOT: kernel seems ok.");
#endif
}
trap_dispatch函数的实现:
static void
trap_dispatch(struct trapframe *tf) {
char c;
switch (tf->tf_trapno) {
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
//下面的代码不用我们实现
case IRQ_OFFSET + IRQ_COM1:
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
switchk2u.tf_eflags |= FL_IOPL_MASK;
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}
break;
case IRQ_OFFSET + IRQ_IDE1:
case IRQ_OFFSET + IRQ_IDE2:
break;
default:
if ((tf->tf_cs & 3) == 0) {
print_trapframe(tf);
panic("unexpected trap in kernel.\n");
}
}
}