操作系统实验1

本文档详细介绍了操作系统实验的内容,包括实验的目的、设计思想、流程和主要数据结构。实验涉及从实模式切换到保护模式、加载ELF执行文件、中断处理流程、函数调用堆栈跟踪以及中断向量表的初始化。实验要求学生理解和实现中断服务例程、函数调用堆栈的追踪以及时钟中断的处理。通过对ucore操作系统源码的分析和编程,加深了对CPU中断机制、内存管理和外设控制的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

HUNAN  UNIVERSITY

 

 

 

 

操作系统

实验报告

 

 

 

 

 

 

 

  目:

实验1

 

学生姓名:

周思宇

 

学生学号:

201608030201

 

专业班级:

计科1601

 

完成日期:

2018.11.22

目  录

一、内容

二、目的

三、实验设计思想和流程

四、主要数据结构及符号说明

五、实验环境以及实验过程与结果分析

六、实验体会和思考题

附录(源代码及注释)

一、内容

lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS。

 

二、目的

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作。为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。lab1提供了一个非常小的bootloader和ucore OS,整个bootloader执行代码小于512个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS,读者可以了解到:

 

计算机原理

  • CPU的编址与寻址: 基于分段机制的内存管理
  • CPU的中断机制
  • 外设:串口/并口/CGA,时钟,硬盘

 

Bootloader软件

  • 编译运行bootloader的过程
  • 调试bootloader的方法
  • PC启动bootloader的过程
  • ELF执行文件的格式和加载
  • 外设访问:读硬盘,在CGA上显示字符串

 

ucore OS软件

  • 编译运行ucore OS的过程
  • ucore OS的启动过程
  • 调试ucore OS的方法
  • 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
  • 中断管理:与软件相关的中断处理
  • 外设管理:时钟

 

三、实验设计思想和流程(需要编程的题)

第五题:

代码设计步骤:

  1. 调用read_ebp()以获得ebp.类型为(uint32_t);
  2. 调用read_eip()以获得eip的值。类型为(uint32_t);
  3. 从0.。。。到stackdepth

ebp、eip的值打印出来

uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]

输出("n");

调用print_debuginfo(eip-1)来打印c语言调用函数名和行号等。

弹出调用堆栈帧

注意:

调用函数的返回加法器eip=ss:[ebp+4]

调用函数的ebp=ss:[ebp]

 

第六题:

代码设计步骤:

首先确定每个中断服务例程的entry addrs在哪里?

存储在vectors中。

uintptr_t_vectors[]在哪里?

_vectors[]位于kern/trap/vector.S中,它是由tools/vector.c生成的。

可以使用“extern uintptr_t __vectors[]”定义这个extern变量。

然后,在中断描述表(IDT)中设置ISR的条目。

然后使用“lidt”指令让CPU知道IDT在哪里。

Google libs/x86.h看一下具体是啥意思

题目中也给过,lidt的参数是idt_pd。

 

然后是设置100tick的输出,更简单:

在定时器中断之后,使用全局变量记录

每个TICK_NUM周期,都可以使用函数打印print_ticks()。

 

第七题:

代码设计步骤:

指针得弄懂,堆栈也得彻底弄懂才能看懂

在从内核态,通过中断,切换为用户态时:

首先要执行sub 0x8,esp 这个语句

然后执行int T_SWITCH_TOU表示发生这个中断,按照之前的叙述,此时的执行过程是:中断向量--查找中断向量表--查找入口地址:发现此时CPL并没有发生切换

所以并不把当前的ss和esp入栈,直接把eflags,cs,eip入栈,然后进入vector规定的地址后,继续把errorno和trapno入栈,然后进入__alltraps,把ds es gs ss 入栈,pushal,当前esp入栈,执行trap,执行trapdispatch,执行相应中断向量号case处的代码:

当前不是用户态,要不要切换呢?此时需要对堆栈进行一些操作。现在是内核栈,我们原来从用户到内核态转换时,是通过TSS查到内核态的ss和esp的,但是这里似乎并没有从TSS查用户态的ss和esp?我们需要自己建立一个堆栈给他使用,这个就是这里的switchk2u变量所对应的地址。这个变量是全局变量,所以它具体在哪存着?

所以至少有一个意识:用户态的堆栈和内核态的堆栈不在一个地方

看具体代码实现

这里首先把tf所指的内容复制过来到switchk2u所对应的地址上,然后设置switchk2u这个变量的cs段,ds,es,ss等为用户数据段选择。

为啥设置esp呢?

原来的trapframe结构的esp保存的是如果发生了权限切换,那么保存原来那个特权级的esp,便于之后恢复。

他的意思是tf结构体esp所在的位置。然而真正给esp赋值的地方,是在中断结束返回后,手动把当前的ebp的值给esp。

之后设置eflags,因为用户态要实现IO,需要把eflags寄存器中的IOPL标志位设置为3,这样CPL<=IOPL是恒成立的,用户态也可以实现IO了

switchk2u是与内核栈不同的一个地址,我们要把它作为新的用户栈,并且还要保证在iret恢复寄存器时,要从switchl2u所规定的这个栈中恢复。

那该如何实现iret恢复寄存器时,是从switchk2u这里恢复而不是从之前的tf这里恢复呢????

trapentry.S在call trap后第一句执行的语句是什么?

popl esp

这个popl esp就是我们修改用户栈指针的地方。如果把popl esp这个语句原来要弹出的内容,换成switchk2u的地址,那么就可以把esp指针设置为switchk2u了。要时后来根据esp恢复寄存器,就会从switchk2u这块恢复。

具体实现过程在后面。这一部分只说思路。

 

四、主要数据结构及符号说明(需要编程的题)

第五题:

数据结构:函数堆栈

一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈结构

这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

ss:[ebp+4]处为返回地址

ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存)

ss:[ebp-4]处为第一个局部变量

ss:[ebp]处为上一层ebp值

由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

内联函数Read Type()可以告诉我们当前EBP的值。以及

非内联函数Read Type()是有用的,它可以读取当前EIP的值,

因为在调用这个函数时,read_eip()可以读

容易堆叠。

函数说明:

内联函数read_ebp()可以告诉我们当前的ebp。

非内联函数read_eip()可以读取当前eip的值,因为在调用这个函数时,read_eip()可以轻松地从堆栈中读取调用方的eip。

在print_debuginfo()中,函数debuginfo_eip()可以获得关于调用链的足够信息。

print_stackframe()将跟踪并打印它们以进行调试。

在boot/bootasm.S中,在跳转到内核条目之前,ebp的值被设置为零,这就是边界。

 

第六题:

数据结构的解释:

__vectors[i]

这个数组是干什么的?

保存了每个中断向量的入口地址

而这些入口地址,就是当中断发生时,中断描述符中所对应的那个offset,所以一旦中断发生,中断处理程序首先是会跳到vector[i]所对应的代码

idt_init就是初始化中断向量表:vector.S规定了每个中断处理例程的代码偏移,然后idt_init通过这些偏移,设置好idt表,然后再通过lidt,把idt表的初始地址保存到idtr寄存器中,这样中断相关的数据结构初始化完毕了

 

第三小问的数据结构:

vector.S规定了中断的入口地址

vector.S文件,有两部分,第一部分是代码段,定义了vector0到vector255这256个标号所对应的代码段的起始位置,每个标号后的代码无非是两种:要么是压入0和中断向量,要么就不压入0,只压入中断向量。然后是jmp __alltraps

TICK_NUM之前都设置过了

每轮100次就调用print函数即可,没什么特别复杂的数据结构。

 

第七题:

   lab1_print_cur_status();当前状态:是内核态还是用户态?

   lab1_switch_to_user(); 转换去用户态

   lab1_switch_to_kernel();转换去内核态

五、实验环境以及实验过程与结果分析

练习1:理解通过make生成执行文件的过程。(要求在报告中写出对下述问题的回答)

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。

在此练习中,大家需要通过静态分析代码来了解:

操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

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

       

实验分析:

lab1的整体目录结构如下所示

.

├── boot

│   ├── asm.h

│   ├── bootasm.S

│   └── bootmain.c

├── kern

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值