[练习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
[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
截取sign.c文件中的部分源码
char buf[512]; //定义buf数组
memset(buf, 0, sizeof(buf));
// 把buf数组的最后两位置为 0x55, 0xAA
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) { //大小为512字节
fprintf(stderr, "write '%s' error,
size is %d.\n", argv[2], size);
return -1;
}
主引导扇区的规则如下:
1 大小为512字节
2 多余的空间填0
3 第510个(倒数第二个)字节是0x55,
4 第511个(倒数第一个)字节是0xAA。
[练习2]
[练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
修改lab1/tools/gdbinit ,内容为:
set architecture i8086
target remote :1234
然后在 lab1执行:
make debug
在gdb的调试界面,执行如下命令:
si
来单步跟踪
在gdb的调试界面,执行如下命令,来查看BIOS代码:
x /2i $pc //显示当前eip处的汇编指令
得到下面的截图:
[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常
修改 gdbinit文件:
set architecture i8086
target remote :1234
b *0x7c00
c
x/2i $pc
得到如下结果,断点正常
[练习2.3] 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
改写 makefile文件:
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
然后再执行
make debug
得到q.log文件:
查看bootasm.S文件:
查看 bootblock.asm文件:
从上面的结果可以看到:
bootasm.S文件中的代码和bootblock.asm是一样的,对于q.log文件,断点之后的代码和bootasm.S,bootblock.asm是一样的。
[练习2.4] 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
修改gdbinit文件,在0x7c4a处设置断点 (调用bootmain函数处)
set architecture i8086
target remote :1234
break *0x7c4a
输入 make debug ,得到结果:
断点设置正常
练习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的过程。
[练习4.1] bootloader如何读取硬盘扇区的?
读取扇区硬盘的代码:
bootloader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。
在上一个联系中我们的BootLoader已经成功的进入了保护模式,接下来我们要做的就是从硬盘读取并运行我们的OS。对于硬盘来说,我们知道是分成许多扇区的其中每个扇区的大小为512字节。读取扇区的流程我们通过查询指导书可以看到:
1、等待磁盘准备好;
2、发出读取扇区的命令;
3、等待磁盘准备好;
4、把磁盘扇区数据读到指定内存。
接下来我们需要了解下如何具体的从硬盘读取数据,因为我们所要读取的操作系统文件是存在0号硬盘上的,所以,我们来看一下关于0号硬盘的I/O端口:
static void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循环
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); //读取一个扇区
outb(0x1F3, secno & 0xFF); //要读取的扇区编号
outb(0x1F4, (secno >> 8)&0xFF);//用来存放读写柱面的低8位字节
outb(0x1F5, (secno >> 16)&0xFF);//用来存放读写柱面的高2位字节
// 用来存放要读/写的磁盘号及磁头号
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); //获取数据
}
一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的,具体参数见下表。一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。从outb()
可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表
可以看出,该函数一次只读取一个扇区。
readseg简单包装了readsect,可以从设备读取任意长度的内容。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
va -= offset % SECTSIZE;
uint32_t secno = (offset / SECTSIZE) + 1;
// 加1因为0扇区被引导占用
// ELF文件从1扇区开始
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
[练习4.2] bootloader是如何加载ELF格式的OS?
ELF定义:
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12];
uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image
uint16_t e_machine; // 3=x86, 4=68K, etc.
uint32_t e_version; // file version, always 1
uint32_t e_entry; // entry point if executable
uint32_t e_phoff; // file position of program header or 0
uint32_t e_shoff; // file position of section header or 0
uint32_t e_flags; // architecture-specific flags, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // size of an entry in program header
uint16_t e_phnum; // number of entries in program header or 0
uint16_t e_shentsize; // size of an entry in section header
uint16_t e_shnum; // number of entries in section header or 0
uint16_t e_shstrndx; // section number that contains section name strings
};
在这里我们只需要关注其中的几个参数,e_magic,是用来判断读出来的ELF格式的文件是否为正确的格式;e_phoff,是program header表的位置偏移;e_phnum,是program header表中的入口数目;e_entry,是程序入口所对应的虚拟地址。
宏定义:
#define ELFHDR ((struct elfhdr *)0x10000)
#define SECTSIZE 512
在bootmain函数中,
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过储存在头部的幻数判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,
// 先将描述表的头地址存在ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// 根据ELF头部储存的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
总结一下就是:
- 从硬盘读了8个扇区数据到内存
0x10000
处,并把这里强制转换成elfhdr
使用; - 校验
e_magic
字段; - 根据偏移量分别把程序段的数据读取到内存中。
[练习5]:实现函数调用堆栈跟踪函数
首先,可以通过read_ebp()
和read_eip()
函数来获取当前ebp寄存器和eip 寄存器的信息。
然后通过ebp+12,ebp+16,ebp+20,ebp+24来输出4个参数的值,最后更新ebp:ebp=ebp[0],更新eip:eip=ebp[1]。直到ebp 对应地址的值为0(表示当前函数为bootmain)。
read_eip()
函数定义在kdebug.c中:
static __noinline uint32_t
read_eip(void) {
uint32_t eip;
asm volatile("movl 4(%%ebp), %0" : "=r" (eip)); //内联汇编,读取(ebp-4)的值到变量eip
return eip; //返回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的值
}
实现函数如下:
void
print_stackframe(void) {
uint32_t ebp = read_ebp(), eip = read_eip(); //获取ebp和eip的值
int i, j;
//#define STACKFRAME_DEPTH 20
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2; //参数的首地址
for (j = 0; j < 4; j ++) {
cprintf("0x%08x ", args[j]); //打印4个参数
}
cprintf("\n");
print_debuginfo(eip - 1); //打印函数信息
eip = ((uint32_t *)ebp)[1]; //更新eip
ebp = ((uint32_t *)ebp)[0]; //更新ebp
}
}
执行 make qemu 得到输出:
最后一行的解释:
其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。(因为此时ebp对应地址的值为0)
bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。
[练习6]完善中断初始化和处理
[练习6.1]中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
一个表项的结构如下:
/*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中找到对应的基地址,然后基地址加上偏移量就是中断处理程序的地址。
[练习6.2] 请编程完善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}};
idt_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);
}
[练习6.3]请编程完善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");
}
}
}
运行结果:
[练习7]
增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),
当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务
[练习8]
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路
是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。