操作系统实验ucorelab1.docx
《操作系统实验ucorelab1.docx》由会员分享,可在线阅读,更多相关《操作系统实验ucorelab1.docx(13页珍藏版)》请在冰豆网上搜索。
![操作系统实验ucorelab1.docx](https://file1.bdocx.com/fileroot1/2022-11/30/696d04f2-7c21-4954-9270-c9448bac450c/696d04f2-7c21-4954-9270-c9448bac450c1.gif)
操作系统实验ucorelab1
一、通make生成执行文件的过程
a)操作系统镜像文件ucore.img是如何一步一步生成的
在编译时使用makeV=,相当于设置一个标记,使得把make编译执行的过程全部展示出来。
首先,调用了GCC,把一些C的源代码编译成.O的目标文件。
然后,调用了ld,把这些目标文件准换成一个可执行程序,比如下面示例,转换成为了bootloader的一个执行程序bootblock.out。
最后,调用了dd,把bootloader、bootblock和kernel放到虚拟的硬盘uCore.img里面。
b)硬盘的主引导扇区的特征是什么
在sign.c中,先申请了一个512字节的空间buf,然后将buf初始化为全0,再将主引导程序写入这个空间,最后,在buf的最后两个字节写入55AA。
所以硬盘的主引导扇区的特征是主引导扇区的512个字节的最后两个字节是55AA。
二、使用qemu执行并调试lab1中的软件
a)从启动开始,单步跟踪BIOS的执行
实验远程调用的方法,启动qemu,并让它进入-S状态。
开启另外一个终端,执行gdb命令,绑定端口1234。
在QEMU窗口中使用x/10i$pc查看最近10条指令的反汇编内容。
在gdb中使用si命令执行单步,显示位置。
可以看到一启动,就处于0xfffffff0的位置,第一条指令是个长跳转指令。
b)在初始化位置0x7c00设置实地址断点,测试断点正常
输入b*0x7c00设置断点,输入c执行到断点。
输入x/10i$pc查看最近10条指令的反汇编内容,可以看到qemu执行到断点0x7c00。
断点工作正常。
再次输入c执行,qemu继续工作。
得到结论,断点工作正常。
c)从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较
经过比较发现:
gdb得到的反汇编代码与bootasm.S和bootblock.asm中的代码基本相一致。
d)找一个bootloader或内核中的代码位置,设置断点并进行测试
输入b*0x7c10设置断点,输入c执行到断点。
输入x/10i$pc查看最近10条指令的反汇编内容。
输入stepi,单步执行一条机器指令。
再一次输入x/10i$pc查看最近10条指令的反汇编内容。
最后,输入c,qemu继续工作。
三、分析bootloader进入保护模式的过程
从%cs=0$,pc=0x7c00进入bootloader。
.globlstart
start:
准备:
将中断标志位清0,不允许中断,设置增址,将段寄存器置0。
.code16
cli
cld
xorw%ax,%ax
movw%ax,%ds
movw%ax,%es
movw%ax,%ss
开启A20:
通过将键盘控制器上的A20线置于高电位,使全部32条地址线可用,进而可以访问4G的内存空间。
seta20.1:
#等待8042键盘控制器不忙
inb$0x64,%al
testb$0x2,%al
jnzseta20.1
movb$0xd1,%al#发送写8042输出端口的指令
outb%al,$0x64
seta20.2:
#等待8042键盘控制器不忙
inb$0x64,%al
testb$0x2,%al
jnzseta20.2
movb$0xdf,%al#打开A20
outb%al,$0x60
加载GDTR、初始化GDT表:
GDT表和其描述符已经静态储存在引导区中,载入即可。
lgdtgdtdesc
进入保护模式:
通过将cr0寄存器PE位置1便开启了保护模式。
movl%cr0,%eax
orl$CR0_PE_ON,%eax
movl%eax,%cr0
通过长跳转更新cs的基地址。
ljmp$PROT_MODE_CSEG,$protcseg
.code32
protcseg:
设置段寄存器,并建立堆栈。
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
完成进入保护模式,进入bootmain。
callbootmain
四、分析bootloader加载ELF格式的OS的过程
a)bootloader读取硬盘扇区
readsect函数可以从设备的第secno扇区读取数据到dst位置。
设置读取扇区的数目为、扇区号,读取扇区。
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);
读取一个扇区。
insl(0x1F0,dst,SECTSIZE/4);
readseg函数包装了readsect,使得可以从设备读取任意长度的内容。
b)bootloader加载ELF格式的OS
读取ELF的头部。
readseg((uintptr_t)ELFHDR,SECTSIZE*8,0);
判断是否是合法的ELF文件。
if(ELFHDR->e_magic!
=ELF_MAGIC){
gotobad;}
根据ELFheader和proghdr程序头,读出代码段和数据段,并且加载到相应地方。
ph=(structproghdr*)((uintptr_t)ELFHDR+ELFHDR->e_phoff);
eph=ph+ELFHDR->e_phnum;
for(;phreadseg(ph->p_va&0xFFFFFF,ph->p_memsz,ph->p_offset);
根据ELF头部储存的入口信息,找到内核的入口。
((void(*)(void))(ELFHDR->e_entry&0xFFFFFF))();
五、实现函数调用堆栈跟踪函数
因为ss:
ebp指向的堆栈位置储存着跳转之前的ebp,所有,以此为线索可以得到所有使用堆栈的函数ebp。
ss:
ebp+4指向的是调用时的eip,ss:
ebp+8等是参数。
又因为bootloader设置的堆栈从0x7c00开始,使用"callbootmain"转入bootmain函数,所以,堆栈最深一层值为ebp:
0x00007bf8eip:
0x00007d68。
代码分析:
得到当前ebp,eip。
uint32_tebp=read_ebp(),eip=read_eip();
输出ebp,eip。
cprintf("ebp:
0x%08xeip:
0x%08xargs:
",ebp,eip);
设置指针,输出四个参数。
uint32_t*args=(uint32_t*)ebp+2;
for(j=0;j<4;j++){
cprintf("0x%08x",args[j]);}
调用print_debuginfo函数完成查找对应函数名并打印至屏幕。
print_debuginfo(eip-1);
获取上一层eip,ebp。
eip=((uint32_t*)ebp)[1];
ebp=((uint32_t*)ebp)[0];
六、完善中断初始化和处理
a)中断向量表中一个表项占多少字节?
其中哪几位代表中断处理代码的入口?
中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,两者联合便是中断处理程序的入口地址。
b)完善对中断向量表进行初始化的函数idt_init
获得IDT表的入口地址。
externuintptr_t__vectors[];
在中断门描述符表建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。
通过查询idt[i]就可定位到中断服务例程的起始地址。
for(i=0;iSETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);}
通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中。
lidt(&idt_pd);
c)完善对时钟中断进行处理的部分
当ticks每加100次后(大约1秒),输出“100ticks”。
ticks++;
if(ticks%TICK_NUM==0){
print_ticks();}
七、扩展练习
a)从内核空间切换到用户空间
调用lab1_switch_to_user函数进行从内核空间到用户空间的切换,lab1_switch_to_user函数内容如下:
asmvolatile(
从中断返回时,会多pop两位,并用这两位的值更新ss、sp,
所以要先把栈压两位。
"sub$0x8,%%esp\n"
调用T_SWITCH_TOU号中断。
"int%0\n"
修复esp。
"movl%%ebp,%%esp"
:
:
"i"(T_SWITCH_TOU));
调用T_SWITCH_TOU号中断代码如下:
if(tf->tf_cs!
=USER_CS){
设置switchk2u。
switchk2u=*tf;
将cs,ds,es,ss设置为用户态。
switchk2u.tf_cs=USER_CS;
switchk2u.tf_ds=switchk2u.tf_es=switchk2u.tf_ss=USER_DS;
设置esp值。
switchk2u.tf_esp=(uint32_t)tf+sizeof(structtrapframe)-8;
设置eflags值。
switchk2u.tf_eflags|=FL_IOPL_MASK;
设置新栈顶指向switchk2u,当返回出栈,则出栈switchk2u中的值。
*((uint32_t*)tf-1)=(uint32_t)&switchk2u;}
b)从用户空间切换到内核空间
调用lab1_switch_to_kernel函数进行从内核空间到用户空间的切换,lab1_switch_to_kernel函数内容如下:
asmvolatile(
调用T_SWITCH_TOK号中断。
"int%0\n"
从中断返回时,esp仍在TSS指示的堆栈中。
所以要在从中断返回后修复esp。
"movl%%ebp,%%esp"
:
:
"i"(T_SWITCH_TOK));
调用T_SWITCH_TOK号中断代码如下:
if(tf->tf_cs!
=KERNEL_CS){
将cs,ds,es设置为内核态。
tf->tf_cs=KERNEL_CS;
tf->tf_ds=tf->tf_es=KERNEL_DS;
设置eflags值。
tf->tf_eflags&=~FL_IOPL_MASK;
设置switchu2k值。
switchu2k=(structtrapframe*)(tf->tf_esp-(sizeof(structtrapframe)-8));
用tf值覆盖switchu2k值。
memmove(switchu2k,tf,sizeof(structtrapframe)-8);
设置新栈顶指向switchk2u,当返回出栈,则出栈switchk2u中的值。
*((uint32_t*)tf-1)=(uint32_t)switchu2k;}
八、问题
a)配置环境
我首先主要遇到的问题是安装一个代码阅读的工具,因为代码量比较大而且函数间相互调用很多。
最后安装了understand,首先下载软件压缩包,然后解压到安装目录,再添加路径,最后输入证书号。
还有一个是在运行makeqemu时报错,解决办法是:
建立符号链接文件”ln-s/usr/local/bin/qenu-system-i386/usr/local/bin/qemu”。
b)在练习2时,设置断点在0x7c20时,没有在断点处停止
后来才了解到设置断点时,断点处的信息会被修改,有时由于设置断点刚好设置在一条指令中间,这样信息修改后,原指令一部分未被覆盖,与断点的机器码可能恰好组成其他的指令,所以没有停止。
所以,设置断点时应该注意要设置在一条指令完成之后,而不要设置在一条指令中间。
c)在练习4,无法理解readseg((uintptr_t)ELFHDR,SECTSIZE*8,0)
在询问后知道,这句代码并不是获取bootloader,而是获取读取ELF文件的头部,所以大小不是512字节,应该是512*8字节,也就是一页4k。
d)在练习5,无法理解堆栈最深一层值为ebp:
0x00007bf8
查阅后知道,在编译器会在每个函数体之前插入
pushl%ebp
movl%esp,%ebp
所以堆栈最深一层ebp为0x7c00-0008=7bf8。
e)在扩展练习中,无法理解"sub$0x8,%%esp"和"movl%%ebp,%%esp"
讲解后知道,"sub$0x8,%%esp"是因为,从中断返回时,会多pop两位,并用这两位的值更新ss、sp,所以要先把栈压两位。
"movl%%ebp,%%esp"是因为,从中断返回时,esp仍在TSS指示的堆栈中,所以要在从中断返回后修复esp。
f)在扩展练习中,如何只使用一个栈空间实现从内核空间到用户空间的切换
修改代码如下,但是报错,不知道问题在哪里。
caseT_SWITCH_TOU:
if(tf->tf_cs!
=USER_CS){
tf->tf_cs=USER_CS;
tf->tf_ds=tf->tf_es=tf->tf_ss=USER_DS;
tf->tf_esp=(uint32_t)tf+sizeof(structtrapframe)-8;
tf->tf_eflags|=FL_IOPL_MASK;
*((uint32_t*)tf-1)=(uint32_t)&tf;}
和同学讨论后,发现是最后一句代码有误。
应该为
*((uint32_t*)tf-1)=(uint32_t)tf;
tf本来就是一个指针,不需要再加地址符。