当前位置: 首页 > Linux

ucore操作系统实验笔记-Lab1

时间:2023-04-06 23:59:08 Linux

最近一直在学习清华大学的操作系统课程。本课程最大的特点是有一系列实用的操作系统实验。这些实验一共有8个,我在这里记录下实验中的一些经验和结论。Task1的任务主要是熟悉Makfile以及如何生成操作系统的镜像文件。Makefile可以用,不需要深入了解。Task2主要是熟悉GDB和操作系统的启动过程。以下是调试BIOS的一些步骤。首先修改gdbinit为:setarchitecturei8086targetremote:1234definehook-stopx/i$pcend然后输入makedebug输入x/i$csx/i$eip我们可以得到$cs和$eip的当前值。其中,$cs=0xf000$eip=0xfff0在实模式下,这个地址是$cs<<4|$eip=0xffff0我们也可以看到这个地址的指令是什么x/2i0xffff0得到的结果是0xffff0:ljmp$0xf000,$0xe05b也就是说BIOS的起始地址应该是$cs<<4|0xe05b=0xfe05b此时,我们设置断点到0x7c00:b*0x7c00/*注意,对于绝对地址,需要加上*用作地址*/程序运行时会停在地址0x7c00.引导加载程序存储在这里。Task3的Taks是这5个Taks中最重要的。通过这个Task我们可以了解到:如何开启A20;CPU如何从实模式切换到保护模式;如何初始化和使用GDT表。如何开启/关闭A20实模式下的内存访问在开启A20之前,先说说i8086中CPU是如何访问内存空间的。i8086时代,CPU的数据总线是16bit,地址总线是20bit,寄存器是16bit,所以CPU只能访问1MB以内的空间。因为数据总线和寄存器都只有16bit,如果我们需要得到20bit的数据,还需要做一些额外的操作,比如移位。实际上,CPU将段移位后与偏移量形成了一个20位的地址(每个段大小恒定64K)。这个地址就是实模式访问内存的地址:address=segment<<4|offset理论上一个20bit的地址可以访问1MB的内存空间(0x00000-(2^20-1=0xFFFFF))。但在实模式下,这20bit地址理论上可以访问0x00000-(0xFFFF0+0xFFFF=0x10FFEF)的内存空间。也就是说,理论上我们可以访问1MB以上的内存空间,但是越过0xFFFFF后,地址又会回到0x00000。以上特性在i8086没有问题(因为最多只能访问1MB的内存空间),但是在i80286/i80386之后,CPU有了更宽的地址总线、数据总线和寄存器,这会出现一个问题:在实模式下,我们可以访问1MB以上,但是我们只想访问1MB以内的内存。为了解决这个问题,在CPU中加入了一个可以控制A20地址线的模块。通过这个模块,我们在实模式下限制第20位地址线为0,这样CPU就不能访问超过1MB的空间。进入保护模式后,我们通过这个模块来解除对A20地址线的限制,这样我们就可以访问1MB以上的内存空间了。A20打开/关闭的过程目前是CPU通过键盘控制器8042来控制A20的地址线。默认情况下,A20地址线是关闭的(第20位地址线被限制为0),所以在进入保护模式之前(需要访问1MB以上的内存空间),我们需要打开A20地址线(第20位)位地址线可以是0或1)。A20的启动过程请参考bootasm.S文件。CPU如何从实模式切换到保护模式非常简单。我们需要在打开A20地址线后,将$CR0(控制寄存器0)的PE(bit0)设置为1。具体代码请参考bootasm.S文件。如何初始化和使用GDT表GDT详解在使用GDT之前,我们需要了解什么是GDT。GDT的全称是GlobalDescriptorTable,即全局描述符表。在保护模式下,我们通过设置GDT将内存空间依次划分为段(这些段可以重叠),这样就可以实现不同的程序访问不同的内存空间。这与实模式下的寻址方式不同。在实模式下,我们只能使用address=segment<<4|offset用于寻址(虽然也是segment+offset,但是在实模式下我们实际上不会被分段)。在这种情况下,任何程序都可以访问整个1MB空间。在保护模式下,程序无法通过分段访问整个内存空间。下面引用一段ucore实验报告:【补充】在保护模式下,有两个段表:GDT(GlobalDescriptorTable)和LDT(LocalDescriptorTable),每个段表可以包含8192(2^13)Descriptor[1],所以最多可以同时存在2*2^13=2^14个段。虽然在保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段不能扩展物理地址空间,每个段的地址空间有很大程度的重叠。目前所谓的64TB(2^(14+32)=2^46)逻辑地址空间是理论值,没有实际意义。在32位保护模式下,真正的物理空间仍然只有2^32字节那么大。注意:ucorelab中只使用了GDT,没有使用LDT。参考:[1]3.5.1SegmentDescriptorTables,Intel?64andIA-32ArchitecturesSoftwareDeveloper'sManual除了GDT,我们还需要了解其他几个术语:段描述符(segmentdescriptor)和段选择器(segmentselector).段描述符是GDT中的元素,段选择符是访问GDT的索引。段选择符在实模式下,逻辑地址由段选择符和段选择符偏移量组成。其中,段选择符为16bit,段选择符偏移量为32bit。下面是段选择器的示意图:在段选择器的child中,INDEX[15:3]是GDT的索引。TI[2:2]用于选择表的类型,1为LDT,0为GDT。RPL[1:0]用于选择请求者的权限级别,00为最高,11为最低。段描述符段描述符的形式比较复杂(为了兼容不同版本的CPU),这里我只给出示意图,具体内容请参考手册。这里用到的最重要的是段基数和段限制:GDT访问有了上面的知识,我们就可以看到如何通过GDT获取需要访问的地址了。我们通过这个示意图来说明:我们是根据CPU给定的逻辑地址来分离段选择符的。使用此段选择器来选择段描述符。线性地址是通过段描述符中的基地址和段选择器的偏移量相加得到的。这个地址就是我们需要的。GDT的初始化和使用因为我们需要在保护模式下使用分段内存空间,所以在进入保护模式之前需要对GDT进行初始化。下面是一些代码来说明如何初始化和使用GDT。下面是GDT初始化的代码:#defineSEG_NULLASM\.word0,0;\.byte0,0,0,0#defineSEG_ASM(type,base,lim)\.word(((lim)>>12)&0xffff),((base)&0xffff);\.byte(((base)>>16)&0xff),(0x90|(type)),\(0xC0|(((lim)>>28)&0xf)),(((base)>>24)&0xff)gdt:/*有一种特殊的选择器叫做空(Null)选择器,它的Index=0,TI=0,RPL字段可以是任意值。空选择器有特定的用途,当使用空选择器进行内存访问时会导致异常。emptyselector是专门定义的,它不对应全局描述符表GDT中的第0个描述符,所以处理器中的第0个描述符是永远不会被处理器访问到的,一般设置为全0。*/SEG_NULLASM#nullseg/*在Lab1中,代码段和数据段都可以访问整个内存空间*/SEG_ASM(STA_X|STA_R,0x0,0xffffffff)#引导加载程序和内核的代码段SEG_ASM(STA_W,0x0,0xffffffff)#datasegforbootloaderandkernelsgdtdesc:/*lgdt需要先载入GDT的大小,再载入gdt的地址*/.word0x17#sizeof(gdt)-1.longgdt#addressgdt理论上GDT可以存放在memory任意位置,但是这里我们以实模式初始化GDT,所以GDT应该存在于最低的1MB内存空间。CPU通过lgdt指令读取GDT的地址,然后我们就可以使用GDT了。.setPROT_MODE_CSEG,0x8.setPROT_MODE_DSEG,0x10/*加载GDT*/lgdtgdtdesc/*从实模式切换到保护模式*/movl%cr0,%eaxorl$CR0_PE_ON,%eaxmovl%eax,%cr0#ljmp,#%cs←imm1#%ip←imm2/*设置%cs(codesegment)的值为0x8*/ljmp$PROT_MODE_CSEG,$protcseg...protcseg:#设置保护模式数据段registers/*设置数据段的值*/movw$PROT_MODE_DSEG,%ax#我们的数据段选择器movw%ax,%ds#->DS:DataSegmentmovw%ax,%es#->ES:ExtraSegmentmovw%ax,%fs#->FSmovw%ax,%gs#->GSmovw%ax,%ss#->SS:StackSegmentTask4通过这个Task,我们可以了解到OS是如何加载ELF镜像文件的。ELF文件格式和使用方法我这里就不仔细研究了。Task5的任务就是让我们理解函数调用和栈的关系。关于函数调用的细节,我在之前的文章中已经写过了。具体可以参考C函数调用过程原理和函数栈帧分析。这里我们主要分析代码,源码在kern/debug/kdebug.c文件中。/*栈底方向的高地址......参数3参数2参数1返回地址上层[ebp]<------[esp/currentebp]局部变量低地址*/voidprint_stackframe(void){uint32_tcur_ebp,cur_eip;uint32_t参数[4];cur_ebp=read_ebp();cur_eip=read_eip();/*假设最多有20层函数调用*/for(intstack_level=0;stack_level