操作系统lab1实验报告
练习1.1 操作系统镜像文件 ucore.img 是如何一步一步生成的?
生成ucore.imge的代码如下:
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)
首先先创建一个大小为10000字节的块儿,然后再将bootblock拷贝过去。
生成ucore.img需要先生成kernel和bootblock
生成kernel
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo "bbbbbbbbbbbbbbbbbbbbbb$(KOBJS)"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
生成bootblock
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo "========================$(call toobj,$(bootfiles))"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
在lab1下输入 make V= 有以下信息
+ cc kern/init/init.c
+ cc kern/libs/readline.c
+ cc kern/libs/stdio.c
+ cc kern/debug/kdebug.c
+ cc kern/debug/kmonitor.c
+ cc kern/debug/panic.c
+ cc kern/driver/clock.c
+ cc kern/driver/console.c
+ cc kern/driver/intr.c
+ cc kern/driver/picirq.c
+ cc kern/trap/trap.c
+ cc kern/trap/trapentry.S
+ cc kern/trap/vectors.S
+ cc kern/mm/pmm.c
+ cc libs/printfmt.c
+ cc libs/string.c
+ ld bin/kernel
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
+ ld bin/bootblock
练习1.2一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:
- 磁盘主引导扇区只有512字节
- 磁盘最后两个字节为0x55AA
- 由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符组成
练习2
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS
其实就是用qemu和gdb进行调试,需要了解一些简单的gdb命令
执行以下命令
make debug1
将加载tools/gdbinit文件的配置设置gdb
#######tools/gdbinit#######
file bin/kernel
target remote:1234
break kern_init
continue12345
file 加载被调试的可执行文件
target 连接qemu进行调试
break或b 设置断点
continue 继续运行
修改tools/gdbinit文件为
#######tools/gdbinit#######
file obj/bootblock.o
target remote:1234
break *0x7c00
continue12345
运行
make debug
有以下信息
输入si命令单步调试,
这是另一个终端会打印下一条命令的地址和内容:
然后输入b*0x7c00在初始化位置地址0x7c00设置上断点,如下:
然后输入continue使之继续运行
在0x7c00处停止运行,然后我们查看此处的反汇编代码:
对比此时bootasm.S中的起始代码,发现确实是一样的
附:
以Intel 80386为例,计算机加电后,CPU从物理地址0xFFFFFFF0(由初始化的CS:EIP确定,此时CS和IP的值分别是0xF000和0xFFF0))开始执行。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。
[练习3]分析从bootloader进入保护模式的过程。BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析bootloader是如何完成从实模式进入保护模式的
分析 bootloader
1.关闭中断,重置寄存器
2.开启A20
将A20置一,至于高电位,开启所有32条地址线
3、加载GDT表,将CR0第零位置一
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
4.长跳转到32位代码段,重装CS和EIP重装DS、ES等段寄存器等
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
5.完成装入保护模式,进入bootmain
[练习4]
分析bootloader加载ELF格式的OS的过程
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载 ELF格式的 OS?
这里主要分析是bootmain函数,
bootmain(void) {
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
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;
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);
while (1);
}
读取硬盘扇区
```c
readsect(void *dst, uint32_t secno) {
waitdisk(); // 等待硬盘就绪
// 写地址0x1f2~0x1f5,0x1f7,发出读取磁盘的命令
outb(0x1F2, 1);
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20);
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4);//读取一个扇区
}
加载 ELF格式的 OS
bootmain(void) {
..........
//首先判断是不是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头表中的入口信息,找到内核的入口并开始运行
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
..........
}
[练习5]
完成kdebug.c中函数print_stackframe的实现,可以通过函数>print_stackframe来跟踪函数调用堆栈中记录的返回地址。
1、函数堆栈
一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
pushl %ebp
movl %esp , %ebp12
这里有个小细节要小心,其实查看bootasm.S中的源码也可以发现的,在gun下采用的是AT&T汇编指令 和intel格式的汇编指令有一些区别,主要是源操作数与目的操作数,位置相反
例如:AT&T: movl %eax, %ebx Intel: mov ebx, eax
表示将eax寄存器的值移到ebx寄存器中。
这样就可以知道函数调用栈结构如下:
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 |
| 上一层[ebp] | <-------- [ebp]
| 局部变量 | 低位地址123456789
函数调用可以描述为以下几个步骤:
- 1、参数入栈:将参数从右向左依次压入栈中。
- 2、返回地址入栈:call指令内部隐含的动作,将call的下一条指令入栈
- 3、代码区跳转:跳转到被调用函数入口处
- 4、函数入口处前两条指令,为本地编译器自动插入的指令,执行这两条指令
- 4.1、将ebp的值入栈,即调用之前的那个栈帧的底部
- 4.2、将当前的esp值赋给ebp,当前的esp即为新的函数的栈帧的底部,保存到ebp中
- 4.3、给新栈帧分配空间(把ESP减去所需空间的大小)。
函数返回的大概步骤:
- 1、保存返回值,通常将函数返回值保存到寄存器EAX中。
- 2、将当前的ebp赋给esp
- 3、从栈中弹出一个值给ebp
- 4、弹出返回地址,从返回地址处继续执行
就是一个相反的过程。
对于函数调用时为什么会有4、3这个步骤,有如下解释:
 深入理解计算机系统一书P154这样描述:“GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括保存%ebp值的4个字节和返回值的4个字节,采用这个规则是为了保证访问数据的严格对齐。”
2、print_stackframe函数的实现
uint32_t ebp,eip,i,j;
ebp = read_ebp();
eip = read_eip();
for(i=0;(i<STACKFRAME_DEPTH)&&(ebp>0);i++){
cprintf("ebp:0x%08x eip:0x%08x ",(uint32_t*)ebp,(uint32_t*)eip);
cprintf("args:");
for(j=0;j<4;j++){
cprintf("0x%08x ",*((uint32_t*)ebp+2+j));
}
cprintf("\n");
print_debuginfo(eip-4);
eip = *((uint32_t*)ebp+1);
ebp = *((uint32_t*)ebp);
}
练习6
1、中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
IDT(Interrupt Description Table)一个表项为8个字节。
由gd_ss与gd_off构成中断处理代码入口。
/* 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数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
extern uintptr_t __vectors[];
int i;
for(i=0;i<sizeof(idt)/sizeof(struct gatedesc);i++){
SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);
}
lidt(&idt_pd);
#define SETGATE(gate, istrap, sel, off, dpl)
3、请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
在kern/driver/clock.h中声明了ticks为extern。
在这里直接用就是了。
ticks++;
if((ticks%TICK_NUM)==0){
print_ticks();
}