ucore lab1

练习1


在终端输入make V=,分别生成了bin/kernel和bin/bootblock,两者一同构成了ucore.img。首先是生成bin/kernel,需要数个源文件,将各个源文件编译,如下图所示:


部分用到的编译参数解释如下:

-c:只编译不链接,产生.o文件

-ggdb:生成可供gdb使用的调试信息

-m32:  生成适用于32位环境的代码。

-gstabs:  生成stabs格式的调试信息。这样要ucore的monitor可以显示出便于开发者阅读的函数调用栈信息。至于什么是stabs格式…

-nostdinc:  不使用标准库,标准库是给应用程序用的。

-fno-stack-protector  不生成用于检测缓冲区溢出的代码

Os  为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节

-I   添加搜索头文件的路径                            //好像不需要空格?编译信息里都是-Ilibs/这种。。

-Wall: 显示编译后所有警告

-fno-builtin:不使用C语言的内建函数,建议好好看一下什么是内建函数。

-nostdinc, 编译器不在系统缺省的头文档目录里面找头文档,一般和-I 联合使用,明确限定头文档的位置

        然后这些目标文件链接生成bin/kernel:


        这里用到的参数的说明:

-m   这里后接参数elf_i386,模拟为i386上的连接器

-T   让连接器使用指定的脚本

         同样的,生成bin/bootblock,经历相同的过程:


        一些参数说明:

-N: 设置代码段和数据段均可读写

-e <entry>: 指定入口

-Ttext:  制定代码段开始位置

         最后,这两个文件共同构成ucore.img


首先生成一个有10000个块的文件,每个块默认512字节,用0填充,然后,把bootblock写到第一个块,而从第二个块开始写kernel中的内容。

2.

一个被系统认为是符合规范的硬盘主引导扇区其大小不能超过512KB,且最后两个字节是0x55和0XAA

练习2


1

首先一个终端打开qemu


        参数说明:

-s:  qemu在端口1234监听gdb的调试连接

-S:  让qemu启动后暂停,等待gdb的连接

         然后可以通过终端操作qemu,输入x/10i $pc,查看近十条指令:注意到第一条指令是一个long jump


这里有一个问题:

Q:为什么第一条指令是在0Xfffffff0: 刚开机不是实模式16位运行吗,怎么就寻址到这么高了

A:实模式确实是16*CS Segment Selector + EIP,但这种方式启用是在CS值第一次被改变后,刚开机时还没有改变CS值,此时寻址是Base Address+ EIP,而Base Address就是0Xffff0000,加上EIP的0xfff0便是开机后的第一条指令地址了。


为了调试ucore,新开一个终端打开gdb连接上这个虚拟机


我这里反汇编显示是错误的,还没找到解决办法,但是单步走的话其实程序运行是正确的


2 在初始化位置0x7c00设置实地址断点,测试断点正常


        注意到练习1中bootblock的编译过程:

        这里的-Ttext 0x7c00,表示代码段偏移了0x7c00,所以为了直接定位到这里,需要在这里加一个断点:b* 0x7c00。为了方便调试,可以在gdb里输入layout split

        修改gdbinit脚本如下:

    同样的,在一个终端打开QEMU,输入

再打开一个终端,输入make debug,得到如下界面:

断点正常,over。

3 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较

        bootasm.S的部分截图如下:

        bootblock.asm的部分截图如下:

        结合之前2中的反汇编指令,三者基本一样

4  自己找一个bootloader或内核中的代码位置,设置断点并进行测试

         自己随便设一个,略

练习3 


        首先介绍一些前置知识

        对于Intel 80386的体系结构而言,PC机中的系统初始化软件由BIOS和BOOTLOADER(在ucore中位bootasm.S和bootmain.c)组成。

        计算机加电后,CPU从物理地址0xFFFFFFF0开始执行,在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点,BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行,即开始执行bootloader。

        bootloader完成的工作主要包括:

1 切换到保护模式,启用分段机制

2 读磁盘中ELF执行文件格式的ucore操作系统到内存

3 显示字符串信息

4 把控制权交给ucore操作系统

        对应其工作的实现文件在lab1中的boot目录下的三个文件asm.h、bootasm.S和bootmain.c

        这里还要讲下实模式和保护模式,在bootloader接手BIOS的工作后,当前的PC系统处于实模式(16位模式)运行状态。8086时,地址线是16位的但是要寻址20位空间。实模式寻址是由CS:IP来寻址,CS和IP值都来自16位寄存器,于是最大寻址能力是2^20+2^16=1088KB。所以当寻址到超过1MB的内存时,会发生“回卷”(不会发生异常)。后面,地址线宽度变宽,为了保持完全的向下兼容性,IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿以上的回绕特征,于是出现了A20 Gate。他们的方法就是把A20地址线控制和键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽\禁止)。

        一开始时A20地址线控制是被屏蔽的(总为0),直到系统软件通过一定的IO操作去打开它(参看bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0--1M、2-3M、4-5M......,这显然是不行的,所以在保护模式下,这个开关也必须打开。

        只有在保护模式下,80386的全部32根地址线有效,可寻址高达4G字节的线性地址空间和物理地址空间,可访问64TB(有2^14个段,每个段最大空间为2^32字节)的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制。

        保护模式下,有两个段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一张段表可以包含8192(2^13)个描述符,因而最多可以同时存在 2^14个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。

        编译器把源程序编译成执行程序时用到的代码段、数据段、堆和栈等概念在这里可以与段联系起来,二者在含义上是一致的

        分段机制涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)

        转换逻辑地址(Logical Address,应用程序员看到的地址)到物理地址(Physical Address, 实际的物理内存地址)分以下两步:

[1] 分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址等于物理地址。

[2] 分页地址转换,这一步中把线性地址转换为物理地址。(注意:这一步是可选的,由操作系统决定是否需要。在后续试验中会涉及。

        每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)

        全局描述符表 全局描述符表的是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。全局描述符表中第一个段描述符设定为空段描述符

        由于GDT 不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段(不能我记我自己)

         段选择子(段寄存器)结构如下:

1 索引(Index):在描述符表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。

2 表指示位(Table Indicator,TI):选择应该访问哪一个描述符表。0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。

3 请求特权级(Requested Privilege Level,RPL):保护机制

        数据段选择子的整个内容可由程序直接加载(如MOV)到各个段寄存器(如SS或DS等)当中。这些内容里包含了RPL字段,然而,代码段寄存器(CS)的内容不能由装载指令(如MOV)直接设置,而只能被那些会改变程序执行顺序的指令(如JMP、INT、CALL)间接地设置。而且CS拥有一个由CPU维护的当前特权级字段(Current Privilege Level,简称CPL)。二者结构如下图所示:


        CPU会在两个关键点上保护内存:当一个段选择符被加载时,以及,当通过线性地址访问一个内存页时。因此,保护也反映在内存地址转换的过程之中,既包括分段又包括分页。当一个数据段选择符被加载时,就会发生下述的检测过程:

CPL:当前特权级(Current Privilege Level) 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别)

DPL:描述符特权(Descriptor Privilege Level) 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身真正的特权级。

RPL:请求特权级RPL(Request Privilege Level) RPL保存在选择子的最低两位。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置,并不一定RPL>=CPL,但是当RPL<CPL时,实际起作用的就是CPL了,因为访问时的特权检查是判断:max(RPL,CPL)<=DPL是否成立,所以RPL可以看成是每次访问时的附加限制,RPL=0时附加限制最小,RPL=3时附加限制最大。

        这三个特权级,其实就是分别代表当前进程的,段描述符里的,段选择子里申请的特权级,确保当前进程不会越级。

        接下来结束实验部分

TASK1 为何开启A20,以及如何开启A20

        为何已经说了,现在说如何开启

        首先在0x7c00处断点,进入调试:

        cli和cld指令,它们的作用分别是:

cli:清除标志寄存器的中断允许标志位IF,这样CPU无法响应外部中断

cld:清除标志寄存器的方向标志位DF,在字串操作中使变址寄存器SI或DI的地址指针自动增加,字串处理由前往后,反之std就是置其为1,使字串处理由后往前

        接下里是对各个段寄存器置0,然后是打开A20 GATE,其打开需要向键盘控制器8042发送一个命令。键盘控制器8042将会将它的的某个输出引脚的输出置高电平,作为 A20 地址线控制的输入。

        8042芯片,其有两个端口地址,分别是0x60和0x64。0x64用来发送一个键盘控制命令,0x60用来传递参数

        在传命令前需要确认这个port是否是busy的,而测试其是否空闲的代码便是如下

        若非busy,便可顺序执行下去

        这里对0x64 port发送了0xd1指令,表示要write这个port,然后又测试了一次是否busy,我们单步走,还是没有跳转,这代表8042仍然不busy,很nice,然后将0xdf通过0x60 port发送过去,也就打开了A20

TASK2 如何初始化GDT表

        其实就是一条指令:

        这里就是加载GDT表,但是我的显示和网上帖子的不一样,它们都是这种更直观的形式:

        但是无所谓,接下来需要进入保护模式

TASK3 如何使能和进入保护模式

        这三步的意思就是把CR0第一位置1。CR0寄存器是控制寄存器,其图如下:

        将PE置1后便是开启了保护模式,至此保护模式已经开启。上图中第四条指令是一个long jump,它将我们带到这里:

        各个段寄存器得到设立,栈被建立起。这里call 0x7d0c代表call bootmain

PS:初始化GDT表还是有点模糊,就只有一条指令,详细还是另找资料吧

练习4 分析bootloader加载ELF格式的OS的过程

TASK1 bootloader如何读取硬盘扇区的?

        对于bootloader,之前的是bootasm.S里的工作,下面讲bootmain.c的工作。下面先贴出bootmain.c的代码:

        bootloader需要将我们的kernel从硬盘读到内存中,

        由指导书可知,其访问硬盘一般都是LBA的PIO方式。一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的。一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现

        读一个扇区的流程(可参看boot/bootmain.c中的readsect函数实现)大致如下:

1 等待磁盘准备好

2 发出读取扇区的命令

3 等待磁盘准备好

4 把磁盘扇区数据读到指定内存

        下面对源码分析,首先:

        这个函数的意思是从kernel的0地址处开始的SECTSIZE*8个字节读到虚拟地址ELFHDR处,ELFHDR这里是地址0x10000,而SECTSIZE是512,可能386定义的page size是512字节吧,现在一般是4KB。

        这个readseg函数的具体如下

        由于每次只能read一个SECTSIZE,所以在这里分开数次传送,每次调用了readsect()函数。kernel初始是sector1(sector 0放的就是我们正在讨论的bootloader),从sector1开始依次读入。readsect()函数的介绍略。

TASK2 bootloader是如何加载ELF格式的OS?

        如之前所说,bootloder首先将kernel的前4096字节加载到了虚拟地址0x10000处

        首先检查这个ELF格式的kernel是否是个合法的ELF文件,即检查其magic number是否正确。

        ELF头的文件格式,如下:

        而对于之后的代码,这里涉及到了program header:

        program header描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。

        这里ph是program header表的首项,而eph是末项,由此通过循环,将各个段加载。

        最后调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值(这里不太懂)

练习5  实现函数调用堆栈跟踪函数 (需要编程)

         练习五能增强对函数调用和栈的了解。函数调用是通过栈实现的,其参数入栈顺序有多种规定,这里以最常用的CDECL来示例:

        esp寄存器指向栈顶,ebp寄存器指向栈底,栈是从高地址往低地址增加。

        这里举例,比如fun1调用了fun2,首先入栈的是fun2需要用到的参数,即fun1传给他的参数,然后接着就是fun1的返回地址,fun2完成他的任务后,PC需要回到fun1原来的地方,需要注意,一直到这里都是fun1的栈帧,这些东西是属于fun1的。然后创建fun2的栈帧,首先栈底保存fun1的栈底地址,因为我们后面要更新ebp为fun2的栈底,即现在fun2的栈底处保存的值是fun1的栈底值,稍微有点绕但影响不大,再往后就是fun2中自己创建的局部变量等了,fun2开始进行他的工作。这就是函数调用的大概的流程

        print_stackframe()函数的官方实现如下:

        需要注意,这里read_ebp()必须是inline的,不然每次都是打印自己的栈底了

        执行make qemu后终端显示的信息如下:

        对于最后一项信息

        此时ebp为kernel的入口,即kern_init的栈底,由练习3:

        最开始esp设为0x7c00,是bootmain的栈顶,这里kern_init的栈底为0x7bf8,两者相差8字节,说明bootmain在调用kern_init时没有任何传参,因为为了创建kern_init的栈帧需要在bootmain的栈顶push 返回地址和push ebp。而打印的4个参数,其实bootmain()在调用kern_init时没有用任何参数,这四个其实没用。

练习6  完善中断初始化和处理 

        还是先对中断作一些简要介绍,这里说的中断是interrupt, exception, trap的合称。

        中断描述符表中的entry又叫作门描述符(Gate Descriptor),有多种Gate Descriptor,但在这个lab主要介绍和处理Interrupt Gate Descriptor,Trap Gate Descriptor。

        IDT和IDTR寄存器的结构和关系如下图所示:

        Interrupts/Exceptions应该使用Interrupt Gate和Trap Gate,它们之间的唯一区别就是:当调用Interrupt Gate时,Interrupt会被CPU自动禁止;而调用Trap Gate时,CPU则不会去禁止或打开中断,而是保留它原来的样子。

        这里所谓的自动禁止意思是,当调用Interrupt Gate时,会将标志寄存器(EFLAGS)压栈,然后清除(clear)其的IF位以避免重复中断。当然我们也可以在interrupt handler中再set IF位以达到嵌套中断,但需要注意保存好需要的context。此外,Trap Gate不会clear IF也很好理解,因为用户进程理应不能禁止中断。

        下图显示了80386的中断门描述符、陷阱门描述符的格式

        中断处理中硬件负责完成的工作:

        下面介绍中断处理的流程,其中有2个过程是由硬件完成的

(起始)从CPU收到中断事件后,打断当前程序或任务的执行,根据某种机制跳转到中断服务例程去执行的过程。其具体流程如下:

1.  CPU执行完每次指令后,都会去确认是否有中断请求到达,若有,CPU便会在总线上读取中该请求对应的中断向量。

2.  CPU通过中断向量在IDT中找到对应的中断描述符,中断描述符里保存着中断服务例程的段选择子

3.  CPU通过得到的段选择子在GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址

4.   CPU会根据CPL (当前进程的权限级别Current Privilege Level) 和中断服务例程的段描述符的DPL确认是否发生了特权级的转换,若发生了特权级的转换,CPU会从当前进程的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈位置,并将进程使用的栈切换到内核栈,这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的用户态的ss和esp压到新的内核栈中保存起来

5.   CPU利用内核栈保存上下文,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息

6.   CPU利用中断服务例程的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务例程,中断服务程序正式开始工作

(结束):每个中断服务例程在有中断处理工作完成后需要通过iret(或iretd)指令恢复被打断的程序的执行。CPU执行IRET指令的具体过程如下:

1.   首先pop出上下文信息,

2.   若发生特权级转换,切换回用户栈

3.   如果此次处理的是带有错误码(errorCode)的异常,CPU在恢复先前程序的现场时,并不会弹出errorCode。这一步需要通过软件完成,即要求相关的中断服务例程在调用iret返回之前添加出栈代码主动弹出errorCode(这里不是很懂)

        下图显示了从中断向量到GDT中相应中断服务程序起始位置的定位方式:

中断处理的特权级转换:

        特权级转换是通过门描述符(gate descriptor)和相关指令来完成的。中断门描述符和陷阱门描述符几乎是一样的。中断发生时实施特权检查的过程如下图所示:

        门中的DPL和段选择符一起控制着访问,同时,段选择符结合偏移量(Offset)指出了中断处理例程的入口点。内核一般在门描述符中填入内核代码段的段选择子。产生中断后,CPU一定不会将运行控制从高特权环转向低特权环,特权级必须要么保持不变(当操作系统内核自己被中断的时候),或被提升(当用户态程序被中断的时候)。无论哪一种情况,作为结果的CPL必须等于目的代码段的DPL。如果CPL发生了改变,一个堆栈切换操作(通过TSS完成)就会发生。如果中断是被用户态程序中的指令所触发的(比如软件执行INT n生产的中断),还会增加一个额外的检查:门的DPL必须具有与CPL相同或更低的特权。这就防止了用户代码随意触发中断。如果这些检查失败,会产生一个一般保护异常(general-protection exception)。

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

        一个表项占8字节,由之前的中断门描述结构:

        16-31位是段选择子,而offset由0-15和48-63共同构成,分别代表低16位和高16位。

TASK2

        直接给官方答案吧,task3也可以看官方答案

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容