没有合适的资源?快使用搜索试试~ 我知道了~
PA4-中断与IO1
需积分: 0 0 下载量 100 浏览量
2022-08-03
16:49:17
上传
评论
收藏 1.58MB PDF 举报
温馨提示
试读
35页
实验简介(请认真阅读以下内容,若有违反,后果自负)预计平均耗时/代码量: 30 小时/300 行实验内容: 本次实验的阶段性安排如下:阶段 2: 移植打字小游戏
资源详情
资源评论
资源推荐
实验四(PA4)中断与 I/O
进行本实验前,请在工程目录下执行以下命令进行分支整理,否则将影响成绩:
git commit --allow-empty -am "before starting PA4"
git checkout master
git merge pa3
git checkout -b pa4
普通阅读,无需提交。
必做任务,需要提交
思考题,需要提交
实验简介,请仔细阅读。
实验简介(请认真阅读以下内容, 若有违反, 后果自负)
预计平均耗时/代码量: 30 小时/300 行
实验内容: 本次实验的阶段性安排如下:
⚫ 阶段 1: 在 NEMU 中运行使用 printf()输出的 Hello 程序
⚫ 阶段 2: 移植打字小游戏
⚫ 阶段 3:移植仙剑奇侠传
提交说明:见这里
注意事项:实验部分内容需要 cmov 指令的支持,但 cmov 指令在 i386 手册中
没有涉及。提供的 PA3 答案中已经实现了所有的 cmov 指令,请大家根据 nemu
运行时提示的 opcode 信息,将对应的 cmov 指令填入的操作码表即可。
评分依据:代码实现占 70%,实验报告占 30%。代码实现中完成阶段 1 占 30%,
阶段 2 占 40%,阶段 3 占 30%。
满足合理的需求
在之前的 PA 中, 用户进程都只能安分守己地在运行在 NEMU 上, 除了"计
算"之外, 什么都做不了。但像"输出一句话"这种合理的需求, 内核必须想办法满
足它。举一个银行的例子, 如果银行连最基本的取款业务都不能办理, 是没有客
户愿意光顾它的。但同时银行也不能允许客户亲自到金库里取款, 而是需要客户
按照规定的手续来办理取款业务。同样地, 操作系统并不允许用户进程直接操作
显示器硬件进行输出, 否则恶意程序就很容易往显示器中写入恶意数据, 让屏幕
保持黑屏, 影响其它进程的使用。因此, 用户进程想输出一句话, 也要经过一定
的合法手续, 这一合法手续就是系统调用。
我们到银行办理业务的时候, 需要告诉工作人员要办理什么业务, 账号是什
么, 交易金额是多少, 这无非是希望工作人员知道我们具体想做什么。用户进程
执行系统调用的时候也是类似的情况, 要通过一种方法描述自己的需求, 然后告
诉操作系统内核。用来描述需求最方便的手段就是使用通用寄存器了, 用户进程
将系统调用的参数依次放入各个寄存器中(第 1 个参数放在 %eax 中,第二个参
数放在%ebx 中...)。为了让内核注意到用户进程提交的申请, 系统调用通常都会
触发一个异常, 然后陷入内核。这个异常和非法操作产生的异常不同, 内核能够
识别它是由系统调用产生的。在 GNU/Linux 中, 这个异常通过 int $0x80 指令
触发。
我们可以在 GNU/Linux 下编写一个程序, 手工触发一次 write 系统调用:
用户进程执行上述代码, 就相当于告诉内核:帮我把从 str 开始的 14 字节写到 1
号文件中去。其中"写到 1 号文件中去"的功能相当于输出到屏幕上。
虽然操作系统需要为用户进程服务, 但这并不意味着操作系统需要把所有
信息都暴露给用户程序。有些信息是用户进程没有必要知道的, 也永远不应该知
道, 例如 GDT, 页表。因此, 通常不存在一个系统调用用来获取 GDT 这些操作系
统私有的信息。
事实上, 你平时使用的 printf , cout 这些库函数和库类, 对字符串进行格式
化之后, 最终也是通过系统调用进行输出。这些都是"系统调用封装成库函数"的
例子。系统调用本身对操作系统的各种资源进行了抽象, 但为了给上层的程序员
提供更好的接口(beautiful interface), 库函数会再次对部分系统调用再次进行
抽象。例如 fopen 这个库函数用于创建并打开一个新文件, 或者打开一个已有的
文件, 在 GNU/Linux 中, 它封装了 open 系统调用。另一方面, 系统调用依赖于
具体的操作系统, 因此库函数的封装也提高了程序的可移植性, 在 windows 中,
fopen 封装了 CreateFile 系统调用, 如果在代码中直接使用 CreateFile 系统调
用,把代码放到 GNU/Linux 下编译就会产生链接错误, 即使链接成功, 程序运行
的时候也会产生运行时错误。
并不是所有的库函数都封装了系统调用, 例如 strcpy 这类字符串处理函数
就不需要使用系统调用. 从某种程度上来说, 库函数的抽象确实方便了程序员,
使得他们不必关心系统调用的细节。
穿越时空的旅程
异常是指 CPU 在执行过程中检测到的不正常事件, 例如除数为零, 无效指
令, 缺页等。IA-32 还向软件提供 int 指令, 让软件可以手动产生异常, 因此上文
提到的系统调用也算是一种异常。那触发异常之后都发生了些什么呢?我们先来
对这一场神秘的时空之旅作一些简单的描述。
为了方便叙述, 我们称触发异常之前用户进程的状态为 A。触发异常之后,
CPU 将会陷入内核, 跳转到操作系统事先设置好的异常处理代码, 处理结束之
后再恢复 A 的执行。可以看到, A 的执行流程被打断了, 为了以后能够完美地恢
复到被打断时的状态,CPU 在处理异常之前应该先把 A 的状态保存起来, 等到异
常处理结束之后, 根据之前保存的信息把计算机恢复到被打断之前的状态。
哪些内容表征了 A 的状态?在 IA-32 中, 首先当然是 EIP(instruction
pointer)了, 它指示了 A 在被打断的时候正在执行的指令(或者下一条指令);然
后就是 EFLAGS(各种标志位)和 CS(代码段, CPL)。由于一些特殊的原因, 这三个
寄存器的内容必须由硬件来保存。此外, 通用寄存器(GPR, general propose
register)的值对 A 来说还是有意义的, 而进行异常处理的时候又难免会使用到
寄存器。但硬件并不负责保存它们, 因此需要操作系统来保存它们的值。
要将这些信息保存到哪里去呢?一个合适的地方就是进程的堆栈。触发异常
时, 硬件会自动将 EFLAGS, CS, EIP 三个寄存器的值保存到堆栈上。此外, IA-32
提供了 pusha / popa 指令, 用于把通用寄存器的值压入/弹出堆栈, 但你需要注
思考题 1:异常和函数调用
我们知道进行函数调用的时候也需要保存调用者的状态:返回地址, 而进行
异常处理之前却要保存更多的信息. 尝试对比它们, 并思考两者保存信息不
同是什么原因造成的。
意压入的顺序, 更多信息请查阅 i386 手册。
等到异常处理结束之后, CPU 将会根据堆栈上保存的信息恢复 A 的状态, 最
后执行 iret 指令。iret 指令用于从中断或异常处理代码中返回, 它将栈顶的三个
元素来依次解释成 EIP, CS, EFLAGS, 并恢复它们。这样用户进程就可以从 A 开
始继续运行了, 在它看来, 这次时空之旅就好像没有发生过一样。
神奇的传送门
我们在上文提到, 用户进程触发异常之后将会陷入内核。"陷入内核"究竟是
怎么样的一个过程?具体要陷入到什么地方去? 要回答这些问题, 我们首先要
认识 IA-32 中断机制。
在 IA-32 中, 异常事件的入口地址是通过门描述符(Gate Descriptor)来指
示的. 门描述符有 3 种:
⚫ 中断门(Interrupt Gate)
⚫ 陷阱门(Trap Gate)
⚫ 任务门(Task Gate)
关于中断/异常会在下文作进一步的解释, 而任务门在实验中不会用到。中
断门和陷阱门的结构如下:
由于 IA-32 分段机制的存在, 我们必须通过段和段内偏移来表示跳转入口。
因此在中断门和陷阱门中, selector 域用于指示目标段的段描述符, offset 域用
于指示跳转目标在段内的偏移。这样, 如果能找到一个门描述符, 就可以根据门
描述符中的信息计算出跳转目标了。
和分段机制类似, 为了方便管理各个门描述符, IA-32 把内存中的某一段数
据专门解释成一个数组, 叫 IDT(Interrupt DescriptorTable, 中断描述符表), 数
组的一个元素就是一个门描述符。为了找到一个门描述符, 我们还需要一个索引.
对于 CPU 异常来说, 这个索引由 CPU 内部产生(例如缺页异常为 14 号异常), 或
者由 int 指令给出(例如 int $0x80)。最后, 为了找到 IDT, IA-32 中使用 IDTR
寄存器来存放 IDT 的首地址和长度。操作系统需要事先把 IDT 准备好, 然后通过
一条特殊的指令把 IDT 的首地址和长度装载到 IDTR 中, IA-32 中断处理机制就
可以正常工作了。
现在是万事俱备, 等到异常的东风一刮, CPU 就会按照设定好的 IDT 跳转到
目标地址:
1. 依次将 EFLAGS, CS, EIP 寄存器的值压入堆栈
2. 从 IDTR 中读出 IDT 的首地址
3. 根据异常(中断)号在 IDT 中进行索引, 找到一个门描述符
4. 把门描述符中的 selector 域装入 CS 寄存器
5. 根据 CS 寄存器中的段选择符, 在 GDT 或 LDT 中进行索引, 找到一个段描述
符, 并把这个段的一些信息加载到 CS 寄存器的描述符 cache 中
6. 在段描述符中读出段的基地址, 和门描述符中的 offset 相加, 得出入口地址
7. 跳转到入口地址
需要注意的是, 这些工作都是硬件自动完成的, 不需要程序员编写指令来完
成相应的内容。事实上, 这只是一个大概的过程, 在真实的计算机上还会遇到很
剩余34页未读,继续阅读
白小俗
- 粉丝: 32
- 资源: 302
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功
评论0