ucore_lab1
练习一
1、操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
(1)先进入文件夹
cd home/moocos/ucore_lab/labcodes_answer/lab1_result/
(2)执行make命令
make clean
然后输入
make "V="
得到了以下信息:
其中生成kernel的细节为:
ld -m elf_i386 -nostdlib -T tools/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
生成bootblock的代码为:
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o
obj/boot/bootmain.o
-o obj/bootblock.o
2 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
(1) 大小为512字节
(2) 多余的空间填0
(3 )第510个(倒数第二个)字节是0x55,
(4) 第511个(倒数第一个)字节是0xAA。
练习二
1 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行
(1) 首先通过make qemu指令运行出等待调试的qemu虚拟机,然后再打开一个终端,通过下述命令连接到qemu虚拟机:
gdb
target remote 127.0.0.1:1234
(2)输入si命令,单步跟踪。
(3)在初始化位置0x7c00设置实地址断点,测试断点正常。
(4) 查看反汇编代码
x/2i $pc
(5)使用meld对比bootasm.S和bootlock.asm的代码对bootasm.S和 bootblock.asm进行比较
meld /home/moocos/moocos/ucore_lab/labcodes/lab1/boot/bootasm.S /home/moocos/moocos/ucore_lab/labcodes/lab1/obj/bootblock.asm
练习三
1、BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
(1)关闭中断,将各个段寄存器重置
.globl start
start:
.code16 # 使用16位模式编译
cli # 禁用中断
cld # 清除方向标志
# 建立重要的数据段寄存器(DS,ES,SS)。
xorw %ax, %ax # ax清0
movw %ax, %ds # ds清0
movw %ax, %es # es清0
movw %ax, %ss # ss清0
(2)开启A20
seta20.1:
inb $0x64, %al # 读取状态寄存器,等待8042键盘控制器闲置
testb $0x2, %al # 判断输入缓存是否为空
jnz seta20.1
movb $0xd1, %al # 0xd1表示写输出端口命令,参数随后通过0x60端口写入
outb %al, $0x64
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 通过0x60写入数据11011111 即将A20置1
outb %al, $0x60
(3)加载GDT表,将CR0的第0位置1,长跳转到32位代码段,重装CS和EIP,转到保护模式完成,进入boot主方法。
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
ljmp $PROT_MODE_CSEG, $protcseg
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
movl $0x0, %ebp
movl $start, %esp
call bootmain
练习四
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
1 bootloader如何读取硬盘扇区
等待磁盘准备好;
发出读取扇区的命令;
等待磁盘准备好;
把磁盘扇区数据读到指定内存。
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC elf的魔数
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第一个programheader的位置,
//这是个结构体,通过这个指针可以找到结构体数组的位置结合e_phnum可以取得所有ph结构体
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
};
/* program section header */
struct proghdr {
uint32_t p_type; // loadable code or data, dynamic linking info,etc.
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
uint32_t p_pa; // physical address, not used
uint32_t p_filesz; // size of segment in file
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
uint32_t p_flags; // read/write/execute bits
uint32_t p_align; // required alignment, invariably hardware page size
};
2 bootloader是如何加载ELF格式的OS
从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用。
校验e_magic字段。
根据偏移量分别把程序段的数据读取到内存中。
bootmain(void) {
..........
//首先判断是不是ELF
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
//获得第一个程序头结构体的地址
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头表中的入口信息,找到内核的入口并开始运行
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
..........
}
练习五
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
1、函数堆栈
(1)函数调用栈结构如下
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 |
| 上一层[ebp] | <-------- [ebp]
| 局部变量 | 低位地址
(2)函数调用可以描述为以下几个步骤:
参数入栈:将参数从右向左依次压入栈中。
返回地址入栈:call指令内部隐含的动作,将call的下一条指令入栈 、代码区跳转:跳转到被调
用函数入口处 、函数入口处前两条指令,为本地编译器自动插入的指令,执行这两条指令
将ebp的值入栈,即调用之前的那个栈帧的底部
将当前的esp值赋给ebp,当前的esp即为新的函数的栈帧的底部,保存到ebp中
给新栈帧分配空间(把ESP减去所需空间的大小)。
(3)函数返回的大概步骤:
保存返回值,通常将函数返回值保存到寄存器EAX中。
将当前的ebp赋给esp
从栈中弹出一个值给ebp
弹出返回地址,从返回地址处继续执行
(4)具体函数
print_stackframe(void) {
int i,j;
uint32_t ebp=read_ebp();
uint32_t eip=read_eip();
for(i=0;i<STACKFRAME_DEPTH&&ebp!=0;i++){
cprintf("ebp:0x%08x eip:0x%08x\n",ebp,eip);
uint32_t *args=(uint32_t *)ebp+2;
cprintf("参数:");
for(j=0;j<4;j++){
cprintf("0x%08x ", args[j]);
}
cprintf("\n");
print_debuginfo(eip-1);
eip=((uint32_t *)ebp)[1];
ebp=((uint32_t *)ebp)[0];
}
}
read_eip(void) {
uint32_t eip;
asm volatile("movl 4(%%ebp), %0" : "=r" (eip)); //内联汇编,读取(ebp-4)的值到变量eip
return eip;
}static inline uint32_t
read_ebp(void) {
uint32_t ebp;
asm volatile ("movl %%ebp, %0" : "=r" (ebp)); //内联汇编,读取edp寄存器的值到变量ebp
return ebp;
}
void
print_debuginfo(uintptr_t eip) {
struct eipdebuginfo info;
if (debuginfo_eip(eip, &info) != 0) {
cprintf(" <unknow>: -- 0x%08x --\n", eip);
}
else {
char fnname[256];
int j;
for (j = 0; j < info.eip_fn_namelen; j ++) {
fnname[j] = info.eip_fn_name[j];
}
fnname[j] = '\0';
cprintf(" %s:%d: %s+%d\n", info.eip_file, info.eip_line,
fnname, eip - info.eip_fn_addr);
}
}
练习六
1、中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断描述符表一个表项占8字节。其中015位和4863位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。
/* 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
};
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。注意除了系统调用中断(T_SYSCALL)以外,其它中断均使用中断门描述符,权限为内核态权限;而系统调用中断使用异常,权限为陷阱门描述符。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3.请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用 print_ticks子程序,向屏幕上打印一行文字100 ticks。
在kern/driver/clock.h中声明了ticks为extern。
ticks++;
if((ticks%TICK_NUM)==0){
print_ticks();
}