练习一:
1、操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
+ cc kern/init/init.c //编译 init.c
+ cc kern/libs/readline.c //编译 readline.c
+ cc kern/libs/stdio.c //编译 stdio.c
+ cc kern/debug/kdebug.c//编译 kdebug.c
+ cc kern/debug/kmonitor.c //编译 kmonitor
+ cc kern/debug/panic.c//编译 panic.c
+ cc kern/driver/clock.c //编译 clock.c
+ cc kern/driver/console.c //编译 console.c
+ cc kern/driver/intr.c//编译 intr.c
+ cc kern/driver/picirq.c //编译 picirq.c
+ cc kern/trap/trap.c //编译 trap.c
+ cc kern/trap/trapentry.S //编译 trapentry.S
+ cc kern/trap/vectors.S //编译 vector.S
+ cc kern/mm/pmm.c//编译 pmm.c
+ cc libs/printfmt.c // printgmt.c
+ cc libs/string.c //编译 string.c
+ ld bin/kernel//接下来用ld合并目标文件(object) 和 库文件(archive),生成kernel程序
+ cc boot/bootasm.S //编译 bootasm.S
+ cc boot/bootmain.c //编译 bootmain.c
+ cc tools/sign.c //编译 sign.c
+ ld bin/bootblock//接下来连接源文件与目标文件,生成bootblock程序
//最后将bootloader放入虚拟硬盘ucore.img中去。
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
2、一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
由tools/sign.c中的报错检查条件易知
大小必须为512字节并且以55AA标志结尾
练习二:使用qemu执行并调试lab1中的软件。
make lab1-mon //在Makefile中设置调试
target remote localhost:1234 //qemu和gdb之间使用网络端口1234进行通讯
define hook-stop//强制反汇编x/i $pcend
next 单步到程序源代码的下一行,不进入函数。nexti 单步一条机器指令,不进入函数。step 单步到下一个不同的源代码行(包括进入函数)。stepi 单步一条机器指令。
练习三:分析bootloader进入保护模式的过程。
Bootasm.S代码段分析:
cli # (在16位下)关闭中断
cld # 设置字符串操作是递增方向
# 设置重要数据段计数器(DS, ES, SS).
xorw %ax, %ax # eax寄存器(16位)设置为0
movw %ax, %ds # -> 数据段寄存器
movw %ax, %es # -> 扩展段寄存器
movw %ax, %ss # -> 栈段寄存器
# 将A20使能(打开A20地址线):
# 为了兼容早期的PC机,第20根地址线在实模式下不能使用
#所以超过1MB的地址,默认就会返回到地址0,重新从0循环计数#下面的代码打开A20地址线
seta20.1:
inb $0x64, %al #从0x64端口读入一个字节的数据到al中
(al寄存器为eax寄存器的低8位)
testb $0x2, %al #test指令可以当作and指令,只不过它不会影响操作数
jnz seta20.1 #如果上面的测试中发现al的第2位为0(代表键盘缓冲区为空),就不执行该指令 否则就循环检查
movb $0xd1, %al #将0xd1(edx低8位)写入到al中
outb %al, $0x64 # 将al中的数据写入到端口0x64中
seta20.2:
inb $0x64, %al # 从0x64端口读取一个字节的数据到al中
testb $0x2, %al #测试al的第2位是否为0
jnz seta20.2 #如果上面的测试中发现al的第2位为0,就不执行该指令 否则就循环检查
movb $0xdf, %al # 将0xdf写入到al中
outb %al, $0x60 # 将al中的数据写入到0x60端口中 # 将全局描述符表描述符加载到全局描述符表寄存器
# cr0中的第0位为1表示处于保护模式
# cr0中的第0位为0,表示处于实模式
lgdt gdtdesc
movl %cr0, %eax #把控制寄存器cr0加载到eax中
orl $CR0_PE_ON, %eax #将eax中的第0位设置为1
movl %eax, %cr0 #将eax中的值装入cr0中
# 跳转到32位模式中的下一条指令
# 将处理器切换为32位工作模式
#下面这条指令执行的结果会将$PROT_MODE_CSEG加载到cs中,cs对应的高速缓冲存储器会加载代码段描述符,同样将$protcseg加载到ip中
ljmp $PROT_MODE_CSEG,$protcseg//PROT_MODE_CASE=0x8
gdt:
SEG_NULLASM # null段SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # 代码段描述符
SEG_ASM(STA_W, 0x0, 0xffffffff) # 数据段描述符
Bootload的启动过程可以概括如下:
首先,BIOS从主加载分区中物理地址为0x7c00的位置加载以下代码并开始执行实模式代码,段寄存器cs值为0,ip值为7c00。
CLI屏蔽中断;CLD使DF复位,即DF=0,串操作方向控制。
设置寄存器 ax,ds,es,ss寄存器值清0;地址线20被*,高于1MB的地址都默认回卷到0。怎么激活A20呢,由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20
8042有两个IO端口:0x60和0x64, 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60
从实模式转换到保护模式,用到了全局描述符表和段表,使得虚拟地址和物理地址匹配,保证转换时有效的内存映射不改变;lgdt汇编指令把GDTR描述符表的大小和起始位置存入gdtr寄存器中;将CR0的最后一位设置为1,进入保护模式;指令跳转由代码段跳到protcseg的起始位置。
设置保护模式下数据段寄存器;设置堆栈寄存器并调用main函数;对GDT作处理。
练习四:分析bootloader加载ELF格式的OS的过程:
1、bootloader是如何加载ELF格式的OS?
void bootmain(void) {
// 从硬盘读取第一页(读到内存的位置,大小,ELF文件偏移)
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过魔数判断是否为合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
#加载每个程序头表中的段 (ignores ph flags)
//定义两个程序头表段
struct proghdr *ph, *eph;
//ph表示ELF段表首地址;eph表示ELF段表末地址
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
//循环读每个段
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// 调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad: //出现问题时的处理
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
2、bootloader如何读取硬盘扇区的?
//检查硬盘是否就绪(检查0x1F7的最高两位,如果是01,则跳出循环;否则等待)
static void waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
static void readsect(void *dst, uint32_t secno) {
// 等待磁盘准备就绪
waitdisk();
outb(0x1F2, 1); #count = 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); //获取数据
}
练习五:实现函数调用堆栈跟踪函数
完成函数print_stackframe的实现,观察输出,并解释最后一行各个数值的含义。
参考源代码如下:
void print_stackframe(void) {
uint32_t ebp = read_ebp();
uint32_t eip = read_eip();
int i, j;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH;i++){
//打印当前ebp和eip的地址
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]);
}
cprintf("\n");
print_debuginfo(eip - 1);
eip = ((uint32_t *)ebp)[1];//eip为压到栈中的eip地址的内容
ebp = ((uint32_t *)ebp)[0];//ebp为压入栈中的ebp所在地址的内容
}
}
运行结果如下图:
分析最后一行为:
<unknow>: -- 0x00007d62 --
显示的信息是“文件名、文件行数、函数名称、函数入口偏移量”
汇编语言在调用C函数的时候,先将参数按照倒序压到栈里,然后压入返回地址,即call语句的下一条指令的地址,然后将ebp的值压入栈中,之后将esp的值赋给ebp,然后再调整eip的值为函数入口地址。
练习六:
1、中断向量表中一个表项占用多少字节,其中哪几位代表中断处理代码的入口?
如中断向量表中一个表项struct gatedesc {}:将bit位相加共64bit为8字节;其第31-16位是段选择子,第63-48位和第15-0位分别是偏移量的高16位和低16位。通过段选择子和段偏移量,就可以找到中断处理代码入口。
2、完成初始化函数idt_init;
查看mmu.h中的SETGATE宏:
#define SETGATE(gate, istrap, sel, off, dpl)
主要使用这个宏进行段选择符的构造
gate:为相应的idt数组内容
istrap:系统段设置为1,中断门设置为0
sel:段选择子
off:为__vectors数组内容
dpl:设置优先级
//保存在vectors.S中的256个中断处理例程的入口地址数组
extern long __vectors[];
int i;
//在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
// set for switch from user to kernel
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
//建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。
lidt(&idt_pd);
}
3、完成中断处理函数trap();
设置时钟进行操作:
case IRQ_OFFSET + IRQ_TIMER:
ticks ++; //一次中断累加1
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
看到了吗?你没有看错,时钟中断成功通过屏幕展示出来了,哈哈,还是比较简单的。