UCORE实验一思路 BIOS 段机制 函数调用栈机制 中断机制

本文详细解释了UCORE实验的各个任务,包括通过makefile生成操作系统镜像文件的过程,主引导扇区的特征,使用QEMU进行调试,bootloader进入保护模式的步骤,特别是A20门和全局段描述符表(GDT)的初始化。此外,还介绍了加载ELF格式OS的过程,以及函数调用堆栈跟踪和中断处理的概念,包括中断描述符表的初始化和时钟中断的处理。
摘要由CSDN通过智能技术生成

UCORE实验原理

实验一

本文章只解释实验的原理和相关内容,并不会附带题目代码。
在这里插入图片描述

task1:理解通过make生成执行文件的过程。

操作系统镜像文件ucore.img是如何一步一步生成的?

原理为使用make指令,对makefile文件进行解释。makefile是对于复杂工程文件中的一个定义文件,主要是告诉make指令编译的顺序,以及一些更复杂的操作。故可以使用make指令来查看具体的编译内容,就可以看到makefile的执行顺序,也就可以看到镜像文件如何一步步生成。

一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

根据主引导扇区的数据结构可以看出,主引导扇区主要分为三部分:启动代码、磁盘分区表、结束引导字。问题的解答只需要对代码进行分析即可,查看是否包括这三部分,对于的字节数大小如何即可。

task2:使用qemu进行调试

qemu是调试工具。

task3:分析bootloader进入保护模式的过程。

该任务主要描述了bootloader被BIOS读取之后做的事情。其实,bootloader被读取后做了三件事:

(1)开启保护模式和段机制

(2)将内核从硬盘读进内存

(3)跳转到内核入口(kernel entry),并将控制权交给内核。

而task3讲述了三件事:开启A20、初始化GDT,进入保护模式。其中A20是进入保护模式中的一个过程,而初始化GDT则是进入段机制的一个过程。

首先是A20,为何开启A20,以及如何开启A20

开启A20是为了向下兼容。在实模式下,由于8086有20根地址线,故寻址最大为1MB,而segment+offset的寻址方式最多为1088KB(0xffff0+0xfff0),故会出现超出最大寻址范围的情况。因此设置了回卷的特性(理解成正溢出)。但由于后续的80286地址线的增多,导致最大寻址范围超过了1088KB,因此不可能出现回卷的情况(如果再回卷,那么就浪费了多出的地址空间。也就是说,回卷本身就是应对寻址方式和地址线寻址的最大值无法对应的情况,但如果已经得到解决,那么就不需要回卷了)。由于希望对高版本对8086的向下兼容,故设置了A20,来进行模拟回卷,做到向下兼容。A20关闭时,高版本芯片仍实行8086的模式,而A20开启时,芯片则可以向高位地址访问,即超过1MB

初始化全局段描述符GDT,只需要一条语句即可。这里主要说明该机制:

实模式下的地址表示就是段地址+偏移量,虽然是一种开创性的表述方式,但仍存在了严重的问题。首先是该模式用户权限等同于系统,因此可以随意修改地址。其次,该模式得到的地址是实打实的物理地址,因此程序可随意修改物理地址,也就会影响到内存,会产生严重的问题。

因此,创立了保护模式(值得一提,真正时间上的命名顺序并不是先有实模式的。因为保护模式是为了解决实模式产生的问题,为了保护地址所用,因此才叫保护模式,而为了与之前的模式进行区分,称之前的模式为实模式。)保护模式下的寻址则会多一层映射,也就会变得更加复杂。同时引入了更加复杂的数据结构。
在这里插入图片描述

按照图中的逻辑,无分页情况下的逻辑地址到线性地址的转化就完成了。需要注意的是,逻辑地址中的部分叫段标识符,其包含了段选择符,段选择符指向段描述符,而多个段描述符则构成了段描述符表。段描述符中的内容加上偏移量则可以得到线性地址。如果加上分页机制,那么就需要再经过页机制的处理,也就是页表的处理才可以。页表的内容不在此实验中考虑,故暂不提及。

至于保护模式的开启,cr0为控制寄存器,0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行

movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
\# Jump to next instruction, but in 32-bit code segment.
\# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg #长跳转更新基地址

//初始化段寄存器,建立堆栈,保护模式开启完成,进入bootmain
protcseg:
# Set up the protected-mode data segment registers
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
# Set up the stack pointer and call into C. The stack region is from 0--
start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain

task4:分析bootloader加载ELF格式的OS的过程。

ELF文件结构如图:显示头部,头部会标识着若干信息,具体在数据结构出会有介绍。然后是描述
段,将ELF文件分为了若干个段,每个段都是OS的信息。每个段又分成了若干的sections。我们在
读取硬盘扇区时便按照ELF文件结构的特点设置函数并相互调用。
在这里插入图片描述

这里主要分析三个函数。

  • waitdisk:判断磁盘驱动是否空闲,为从磁盘读取OS提供帮助。

    /* waitdisk - wait for disk ready */
    static void
    waitdisk(void) {
      while ((inb(0x1F7) & 0xC0) != 0x40)
      	//0x1F7可以标志驱动器准备好/忙 
          /* do nothing */;
    }
    

    readsect:读取小节,实际上是读取扇区。这里大量的使用了内联编程的方式。

    /* readsect - read a single sector at @secno into @dst */
    static void
    readsect(void *dst, uint32_t secno) {
    	//secno为编号 
        // wait for disk to be ready
        waitdisk();
    
         outb(0x1F2, 1);                         // count = 1
         // 往0X1F2地址中写入要读取的扇区数,由于此处需要读一个扇区,因此参数为1
         // 0x1F2是要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
     	outb(0x1F3, secno & 0xFF);
         outb(0x1F4, (secno >> 8) & 0xFF);
         outb(0x1F5, (secno >> 16) & 0xFF);
         outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
         // 0x1F3-6是LBA参数 
         outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors
     	// 开读!
     	 
         // wait for disk to be ready
         waitdisk();
    
         // read a sector
    	insl(0x1F0, dst, SECTSIZE / 4);
    }
    

    readseg:读取段,实际上调用了readsect,对节多次读取。

    /* *
    
    * readseg - read @count bytes at @offset from kernel into virtual address @va,
    
    * might copy more than asked.
    
    * */
     static void
     readseg(uintptr_t va, uint32_t count, uint32_t offset) {
      uintptr_t end_va = va + count;
    
      // round down to sector boundary
      va -= offset % SECTSIZE;
    
      // translate from bytes to sectors; kernel starts at sector 1
      uint32_t secno = (offset / SECTSIZE) + 1;
    
      // If this is too slow, we could read lots of sectors at a time.
      // We'd write more to memory than asked, but it doesn't matter --
      // we load in increasing order.
      for (; va < end_va; va += SECTSIZE, secno ++) {
          readsect((void *)va, secno);
      }
     }
    
    

而bootloader加载OS的过程是读取ELF之后发生的,是上述三个函数经过迭代操作后的结构。加载OS实际上也是伴随着这个过程的。因此不对bootmain函数进行二次拆分。具体加载方式和跳转到入口过程见注释分析。

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    //先读入ELF头 
    //ELFHDR是宏,替换为一个elfhdr的数据结构,该结构如下:
	/*
	struct elfhdr {
	  uint magic;  // must equal ELF_MAGIC
	  uchar elf[12];
	  ushort type;
	  ushort machine;
	  uint version;
	  uint entry;  // 程序入口的虚拟地址
	  uint phoff;  // program header 表的位置偏移
	  uint shoff;
	  uint flags;
	  ushort ehsize;
	  ushort phentsize;
	  ushort phnum; //program header表中的入口数目
	  ushort shentsize;
	  ushort shnum;
	  ushort shstrndx;
	};
	*/ 
	//第一步操作为先读入调用readseg读入OS的ELF的文件头
	//特别注意:在进入保护模式之后,寻址方式为段寻址 
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    //第二步为判断判是否合法 
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;
	/*
	struct proghdr {
	  uint type;   // 段类型
	  uint offset;  // 段相对文件头的偏移值
	  uint va;     // 段的第一个字节将被放到内存中的虚拟地址
	  uint pa;
	  uint filesz;
	  uint memsz;  // 段在内存映像中占用的字节数
	  uint flags;
	  uint align;
	};
	*/ 
    // load each program segment (ignores ph flags)
    // ph是文件描述符表的头部
	// eph为尾部(ph加上了描述符表的表项数)
	//第三步为一段一段的从磁盘读入OS 
    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);
    }

    // call the entry point from the ELF header
    // note: does not return
    //最后一步:跳转到OS的入口,将控制权转交给OS 
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

task5:实现函数调用堆栈跟踪函数 (需要编程)

这里面主要需要了解的原理就是堆栈寄存器。涉及到的寄存器有三个:EIP、EBP、ESP。
EIP的内容为下一条指令的地址。
EBP是基址寄存器,因为可以通过EBP加上偏移量来访问栈中的不同内容。例如,EBP+1(这里是加一字,即四个字节)就可以得到EIP。
ESP则是栈顶寄存器,始终指向栈顶的位置。如果调用函数,那么ESP则会指向调用函数后的栈顶。总之,ESP一定是指向当前栈的栈顶的,也就是低位地址。
除此之外,我们还需要知道栈先入后出的特性,以及栈的生长方向是从高位地址向低位地址生长。即,低位地址的位置才是栈顶。
在这里插入图片描述
实际上,图中的EIP并不是一直是寄存器的值。在函数调用之前(调用函数的第一步是将EBP压栈,因此参数和EIP的内容都是在调用函数之前的),需要将返回地址放入图中的EIP的地方,而EBP的位置存的也是调用者的EBP的地址。这其实也很好理解,因为EIP的值不断在变化,EIP寄存器存储的是下一条指令的地址 ,所以EIP不可能一直指向一个位置。但是可以通过提前写入EIP的值(放在EBP的高位),再通过EBP加上偏移值的方法,就可以得到返回地址。因此,你可以认为图中的EIP的值是“暂存的”。EBP的内容是上一个EBP的地址,EBP寄存器存的是指针,指向当前调用函数的EBP的位置。不过在简单理解堆栈的过程而不涉及到具体的C或者assembly代码时,按照图上的理解就可以,即,EBP是基址,可以通过基址加上偏移值得到返回地址,根据返回地址就可以回到调用当前函数的函数。不需要知道更细致的内容。

对于一个函数来说,入栈的顺序是固定的:先进入的是传入的参数,从右向左进入。例如:

void fun(var1,var2)
{
	//op
}

那么先进入栈的就是var2这个参数,再进入的是var1,紧接着是EIP(返回地址)、EBP(基址)然后是函数的局部变量等存储在栈内的内容。最后则一定是ESP,栈顶寄存器。

当main函数调用其他函数的时候(main一定先于其他函数入栈,因为任何函数的调用一定和main有关系,不是直接调用就是间接调用),最先做的操作往往是将EBP进行压栈(如上面所说,EIP在调用函数之前就已经入栈)。汇编代码如下:

push ebp
mov ebp,esp

即,开拓区域存储EBP的值,将EBP的值放入栈中,并且将ESP的值给EBP。注意,ESP的值是当前的栈顶地址,那此时EBP寄存器的值就是栈顶的地址,在ESP不断生长的过程中,EBP自然就成为了当前函数栈的栈底。
在这里插入图片描述

在这里插入图片描述

而出栈也大同小异:

mov esp, ebp
pop ebp

即将ESP指向当前函数的栈底(由于函数出栈,也可以理解为栈顶),再将EBP的内容pop出栈,完成对函数的出栈。

当然。本实验要求的内容并没有这么多,主要理解EBP是基址寄存器,且使用EBP+偏移可以得到返回地址(EIP)、args等内容即可。

task6:中断

1.中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

由段寻址的方式可以得知,中断服务程序的地址是有offset+base得到的,而base是源于段选择子的。故处理代码的入口应该为gd_ss的部分,也就是16-31位。

2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

extern uintptr_t __vectors[]; //声明中断入口,__vectors定义于vector.S中
    uint32_t i;
    for (i = 0; i < (sizeof(idt) / sizeof(struct gatedesc)); i++)
    {
    // 该idt项为内核代码,所以使用GD_KTEXT段选择子
    // 中断处理程序的入口地址存放于__vectors[i]
    // 特权级为DPL_KERNEL
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); //为中断程序设置内核态权限
    }
    SETGATE(idt[T_SYSCALL], 0, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER); //为T_SYSCALL设置用户态权限
    lidt(&idt_pd); //使用lidt指令加载中断描述符表

传入 SETGATE 的参数:

第一个参数 gate 是中断描述符表
第二个参数 istrap 用来判断是中断还是 trap
第三个参数 sel 的作用是进行段的选择
第四个参数 off 表示偏移
第五个参数 dpl 表示中断的优先级
代码分析:题目要求我们为每个中断设置权限,只有 T_SYSCALL 是用户 态权限(DPL_USER),其他都为内核态权限(DPL_KERNEL)。首先通过 _vectors[]获得所有中断的入口,再通过循环为每个中断设置权限(默 认为内核态权限),为 T_SYSCALL 设置用户态权限,最后通过 lidt 将 IDT 的起始地址装入 IDTR 寄存器即可。

3.请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

太简单了,看提示吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值