1、操作系统实验ucorelab1一、通make生成执行文件的过程a)操作系统镜像文件ucore.img是如何一步一步生成的在编译时使用make V=,相当于设置一个标记,使得把make编译执行的过程全部展示出来。首先,调用了GCC,把一些C的源代码编译成.O的目标文件。然后,调用了ld,把这些目标文件准换成一个可执行程序,比如下面示例,转换成为了bootloader的一个执行程序bootblock.out。最后,调用了dd,把bootloader、bootblock和kernel放到虚拟的硬盘uCore.img里面。b)硬盘的主引导扇区的特征是什么在sign.c中,先申请了一个512字节的空间b
2、uf,然后将buf初始化为全0,再将主引导程序写入这个空间,最后,在buf的最后两个字节写入55AA。所以硬盘的主引导扇区的特征是主引导扇区的512个字节的最后两个字节是55AA。二、使用qemu执行并调试lab1中的软件a)从启动开始,单步跟踪BIOS的执行实验远程调用的方法,启动qemu,并让它进入-S状态。开启另外一个终端,执行gdb命令,绑定端口1234。在QEMU窗口中使用 x/10i $pc 查看最近10条指令的反汇编内容。在gdb中使用si命令执行单步,显示位置。可以看到一启动,就处于0xfffffff0的位置,第一条指令是个长跳转指令。b)在初始化位置0x7c00设置实地址断点
3、,测试断点正常输入 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
4、$pc 查看最近10条指令的反汇编内容。输入stepi,单步执行一条机器指令。再一次输入x/10i $pc 查看最近10条指令的反汇编内容。最后,输入c,qemu继续工作。三、分析bootloader进入保护模式的过程从%cs=0 $,pc=0x7c00进入bootloader。.globl startstart: 准备:将中断标志位清0,不允许中断,设置增址,将段寄存器置0。 .code16 cli cld xorw %ax, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss 开启A20:通过将键盘控制器上的A20线置于高电位,使全部32条地址线可用
5、,进而可以访问4G的内存空间。 seta20.1: # 等待8042键盘控制器不忙 inb $0x64, %al testb $0x2, %aljnz seta20.1movb $0xd1, %al # 发送写8042输出端口的指令 outb %al, $0x64 seta20.2: # 等待8042键盘控制器不忙inb $0x64, %al testb $0x2, %aljnz seta20.2movb $0xdf, %al # 打开A20 outb %al, $0x60 加载GDTR、初始化GDT表:GDT表和其描述符已经静态储存在引导区中,载入即可。 lgdt gdtdesc进入保护模式
6、:通过将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完成进入保护模式,进入bootm
7、ain。 call bootmain四、分析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(0x1F
8、0, dst, SECTSIZE / 4); readseg函数包装了readsect,使得可以从设备读取任意长度的内容。 b)bootloader加载ELF格式的OS读取ELF的头部。readseg(uintptr_t)ELFHDR, SECTSIZE * 8, 0);判断是否是合法的ELF文件。if (ELFHDR-e_magic != ELF_MAGIC) goto bad;根据ELFheader和proghdr程序头,读出代码段和数据段,并且加载到相应地方。ph=(struct proghdr *)(uintptr_t)ELFHDR + ELFHDR-e_phoff); eph = p
9、h + ELFHDR-e_phnum; for (; 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开始,使用call bootmain转入bootmain函数,所以,堆栈最
10、深一层值为ebp:0x00007bf8 eip:0x00007d68。代码分析:得到当前ebp,eip。uint32_t ebp = read_ebp(), eip = read_eip();输出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 , argsj);调用print_debuginfo函数完成查找对应函数名并打印至屏幕。print_debuginfo(e
11、ip - 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表的入口地址。extern uintptr_t _vectors;在中断门描述符表建立中断门描述符,其中存储了中断处理例程 的代码段GD_KTEXT和偏移量_vectorsi,特权
12、级为 DPL_KERNEL。通过查询idti就可定位到中断服务例程的起始地 址。for (i = 0; i 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(struct trapframe) - 8;设置eflags值。 switchk2u.tf_
13、eflags |= FL_IOPL_MASK;设置新栈顶指向switchk2u,当返回出栈,则出栈switchk2u 中的 值。 *(uint32_t *)tf - 1) = (uint32_t)&switchk2u;b)从用户空间切换到内核空间调用lab1_switch_to_kernel函数进行从内核空间到用户空间的切换,lab1_switch_to_kernel函数内容如下:asm volatile (调用T_SWITCH_TOK号中断。 int %0 n从中断返回时,esp仍在TSS指示的堆栈中。所以要在从中断返 回后修复esp。 movl %ebp, %esp : : i(T_SWI
14、TCH_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=(struct trapframe *)(tf-tf_esp-(sizeof(struct trapframe) - 8);用tf值覆盖switchu2k值。 memmove(switchu2k, tf, sizeo
15、f(struct trapframe) - 8);设置新栈顶指向switchk2u,当返回出栈,则出栈switchk2u 中的值。 *(uint32_t *)tf - 1) = (uint32_t)switchu2k;八、问题a)配置环境我首先主要遇到的问题是安装一个代码阅读的工具,因为代码量比较大而且函数间相互调用很多。最后安装了understand,首先下载软件压缩包,然后解压到安装目录,再添加路径,最后输入证书号。还有一个是在运行make qemu时报错,解决办法是:建立符号链接文件”ln -s /usr/local/bin/qenu-system-i386 /usr/local/bin
16、/qemu”。b)在练习2时,设置断点在0x7c20时,没有在断点处停止后来才了解到设置断点时,断点处的信息会被修改,有时由于设置断点刚好设置在一条指令中间,这样信息修改后,原指令一部分未被覆盖,与断点的机器码可能恰好组成其他的指令,所以没有停止。所以,设置断点时应该注意要设置在一条指令完成之后,而不要设置在一条指令中间。c)在练习4,无法理解readseg(uintptr_t)ELFHDR, SECTSIZE * 8, 0)在询问后知道,这句代码并不是获取bootloader,而是获取读取ELF文件的头部,所以大小不是512字节,应该是512*8字节,也就是一页4k。d)在练习5,无法理解堆
17、栈最深一层值为ebp:0x00007bf8查阅后知道,在编译器会在每个函数体之前插入pushl %ebpmovl %esp,%ebp所以堆栈最深一层ebp为0x 7c00-0008=7bf8。e)在扩展练习中,无法理解sub $0x8, %esp 和movl %ebp, %esp讲解后知道,sub $0x8, %esp 是因为,从中断返回时,会多pop两位,并用这两位的值更新ss、sp,所以要先把栈压两位。movl %ebp, %esp是因为,从中断返回时,esp仍在TSS指示的堆栈中,所以要在从中断返回后修复esp。f)在扩展练习中,如何只使用一个栈空间实现从内核空间到用户空间的切换修改代码
18、如下 ,但是报错,不知道问题在哪里。 case T_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(struct trapframe) - 8; tf-tf_eflags |= FL_IOPL_MASK; *(uint32_t *)tf - 1) = (uint32_t)&tf; 和同学讨论后,发现是最后一句代码有误。应该为 *(uint32_t *)tf - 1) = (uint32_t)tf;tf本来就是一个指针,不需要再加地址符。
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1