练习一
练习1.1 操作系统镜像文件 ucore.img 是如何一步一步生成的?
输入make V=
查看makefile文件可以找到
@$(call totarget,sign) $(call outfile,bootblock)
$(bootblock)
所以从上面可以看出ucore.img的生成过程:
- 编译所有生成bin/kernel所需的文件
- 链接生成bin/kernel
- 编译bootasm.S bootmain.c sign.c
- 根据sign规范生成obj/bootblock.o
- 生成ucore.img
练习1.2
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
截取sign.c文件中的部分源码
试验发现主引导扇区
1.大小为512字节
2.多余的空间填0
3.第510个(倒数第二个)字节是0x55,
4.第511个(倒数第一个)字节是0xAA。
练习二
练习2.1 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
修改lab1/tools/gdbinit ,内容为:
set architecture i8086
target remote :1234
然后在 lab1执行
make debug
在 gdb 的调试界面,执行如下命令:
si //单步跟踪
在 gdb 的调试界面,执行如下命令,来查看BIOS代码:
x /2i $pc
得到下面的截图
练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常
修改 gdbinit文件
set architecture i8086
target remote :1234
b *0x7c00
c
x/2i $pc
得到如下结果,断点正常
练习2.3 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较
1单步跟踪
输入两次 si
2使用meld对比 bootasm.S和bootlock.asm的代码
meld +bootasm.S和bootlock.asm
练习3:分析bootloader进入保护模式的过程。
3.0 关中断和清除数据段寄存器
.globl start
start:
.code16 //使用16位模式编译
cli //关中断
cld //清除方向标志
xorw %ax, %ax // ax 清0
movw %ax, %ds // ds 清0
movw %ax, %es // es 清0
movw %ax, %ss // ss 清
#
练习3.1为何开启A20,以及如何开启A20?
15seta20.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端口中
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.进入保护模式
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
4.设置段寄存器,并建立堆栈
.code32 // 使用32位模式编译
protcseg:
movw $PROT_MODE_DSEG, %ax // ax赋0x80
movw %ax, %ds // ds赋0x80
movw %ax, %es // es赋0x80
movw %ax, %fs // fs赋0x80
movw %ax, %gs // gs赋0x80
movw %ax, %ss // ss赋0x80
movl $0x0, %ebp // 设置帧指针
movl $start, %esp // 设置栈指针
5.转到保护模式完成,进入boot主方法
call bootmain //调用bootmain函数
练习3.3 如何使能和进入保护模式
将cr0寄存器置1,cro的第0位为1表示处于保护模式
练习4 分析bootloader加载ELF格式的OS的过程
bootloader如何读取硬盘扇区
等待磁盘准备好;
发出读取扇区的命令;
等待磁盘准备好;
把磁盘扇区数据读到指定内存。
我们来看一下关于0号硬盘的I/O端口:
static void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循环
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
/* 读节 - 将“secno”处的单个扇区读入“dst” */
static void
readsect(void *dst, uint32_t secno) {
// 等待磁盘准备就绪
waitdisk();
// 用LBA模式的PIO(Program IO)方式来访问硬盘
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-读为扇区
// 等待磁盘准备就绪
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4); //获取数据
}
从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// 转至分区边界
va -= offset % SECTSIZE;
// 从字节转换到扇区;内核从扇区1开始
uint32_t secno = (offset / SECTSIZE) + 1; //加1因为0扇区被引导占用
// 如果速度慢,我们一次就能读到很多扇区。
// 我们会写更多的记忆,而不是要求,但这并不重要--我们以增序加载。
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
练习4.2 bootloader是如何加载ELF格式的OS?
从硬盘读了8个扇区数据到内存0x10000处;
校验e_magic字段
根据偏移量分别把程序段的数据读取到内存中
ELF定义:
struct elfhdr {
uint32_t e_magic; // 判断读出来的ELF格式的文件是否为正确的格式
uint8_t e_elf[12];
uint16_t e_type; // 1=可重定位,2=可执行,3=共享对象,4=核心映像
uint16_t e_machine; // 3=x86,4=68K等.
uint32_t e_version; // 文件版本,总是1
uint32_t e_entry; // 程序入口所对应的虚拟地址。
uint32_t e_phoff; // 程序头表的位置偏移
uint32_t e_shoff; // 区段标题或0的文件位置
uint32_t e_flags; // 特定于体系结构的标志,通常为0
uint16_t e_ehsize; // 这个elf头的大小
uint16_t e_phentsize; // 程序头中条目的大小
uint16_t e_phnum; // 程序头表中的入口数目
uint16_t e_shentsize; // 节标题中条目的大小
uint16_t e_shnum; // 节标题中的条目数或0
uint16_t e_shstrndx; // 包含节名称字符串的节号。
};
宏定义
bootmain.c:
void
bootmain(void) {
// 从磁盘上读取第一页
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 这是有效的ELF吗?
if (ELFHDR->e_magic != ELF_MAGIC) { // 通过储存在头部的幻数判断是否是合法的ELF文件
goto bad;
}
struct proghdr *ph, *eph;
// 加载每个程序段(忽略ph标志)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); // 先将描述表的头地址存在ph
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);
/* do nothing */
while (1);
}
练习5 实现函数调用堆栈跟踪函数
------通过read_ebp()和read_eip()函数来获取当前ebp寄存器和eip 寄存器的信息
ebp表示上一层,eip表示第一个局部变量。
read_ebp()(在kdebug.c中)
read_ebp()
static __noinline uint32_t
read_eip(void) {
uint32_t eip;
asm volatile("movl 4(%%ebp), %0" : "=r" (eip)); //读取(ebp-4)的值到变量eip
return eip; //返回eip的值
}
**re
ad_eip()(在 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开始一步步调用函数,bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
练习6 完善中断初始化和处理
中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
表结构
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
mmu.h中的SETGATE(gate,istrap,sel,off,dpl)**
#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;
}
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);
}
make debug 执行验证
请编程完善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 print_ticks() {
cprintf("%d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADE
cprintf("End of Test.\n");
panic("EOT: kernel seems ok.");
#endif
}
执行
make debug