异常控制流

引言:异常控制流,之前对异常的理解还是肤浅了

异常控制流

控制流与异常控制流

CPU读取PC的地址运行程序,PC会存储一系列地址的值,如果这一串序列排成一串,那么这就是控制流

比如:a0,a1,a2,a3...an-1

  • ak -> ak+1的过程就称为控制转移(平滑的变化过程)
  • am -> an的过程(m,n不相邻),这就称为突变(这种情况有可能是执行了程序的跳转、调用等等指令,也有可能是出现了异常)

为什么要进行异常处理?

程序要继续执行,系统就必须能对出现的任何状况进行响应

  • 对于正常的状况,直接执行即可;
  • 对于正常的突变,也可以正常执行;
  • 但是如果出现与程序执行无关的一些突变,也需要做出响应(比如)
    • 一个硬件定时器定期产生信号
    • 包传到网卡后,必须放入内存

这样的突变(与程序执行无关的一些突变)就称为异常控制流(Exceptional Control Flow,ECF)

异常

注意:本节所讲述的都是硬件级别的异常(不是Java中那种catch到的应用级别异常)

异常的处理

异常:是异常控制流ECF的突变,一部分由硬件实现,一部分由OS实现

硬件和OS内核(软件)一同实现了这个功能,所以不同的处理器,它的异常处理过程也是不同的,但是原理基本一致

异常处理的关键结构

  • 异常号:每种异常都有一个非负整数的异常号,异常号分为两部分

    • 一部分是CPU的设计者分配的(比如除零、缺页、内存访问违例、断点以及算数运算溢出
    • 一部分是OS内核的设计者分配的(比如系统调用、外部的I/O信号
  • 异常表:计算机启动时,OS就会分配和初始化一张称为异常表的跳转表

    • 这个表的结构如下,序号其实就是异常号,异常号就是遍历这个表的索引值

      1
      2
      3
      4
      5
      6
      7
      8
      0 |-----------------|
      |处理异常程序0的代码 |
      1 |-----------------|
      |处理异常程序1的代码 |
      2 |-----------------|
      |处理异常程序2的代码 |
      ....
      n |------------------|
  • 异常处理程序:可以看到异常表内有处理对应异常的代码,他们就是异常处理程序

  • 异常表基址寄存器:此寄存器存放了异常表的基址,通过基址寄存器+异常号,就能快速的找到处理异常的代码

寻址

异常处理流程

如何检测到异常?

CPU有很多状态,这些状态表现为不同的位和信号

如果状态发生了变化,就称为事件,CPU可以检测到这些事件的发生

这些事件的发生后,如果其有对应的异常号,那么就是一个异常

硬件与OS是如何协同处理一个异常的?

  1. 运行时,如果发生了一个事件(引起了CPU状态(有些位或信号)的改变),就会去查是否有对应的异常号
  2. 如果存在对应的异常号,处理器就会触发异常
  3. 通过异常表基址寄存器+异常号,就可以锁定到对应处理该异常的代码位置,处理对应的异常

异常处理的特点

异常的处理类似于过程调用,但是有些区别:

(PS:过程调用是相对于系统调用而言的,过程调用不需要切换到内核态,过程调用有可能涉及到磁盘的一些操作等等)

  1. 过程调用会将返回地址压入栈中;而异常会根据不同的类型压入当前指令的返回地址或是下一个指令的返回地址
  2. 处理器会把额外的一些状态也压入栈(可能重新开始执行后会需要这些状态)
  3. 异常处理程序运行在内核模式下(意味着,他们对所有的系统资源都有完全的访问权限)

异常的分类

异常主要分为四大类(对于OS而言,不讨论应用级别的异常)

类别 原因 异步/同步 返回行为
中断 来自IO设备 异步 总是返回下一条指令
陷阱 有意的异常 同步 总是返回下一条指令
故障 潜在的可恢复的错误 同步 返回当前指令/不返回(终止)
终止 不可恢复的错误 同步 不会返回

下面会详细介绍这些异常

中断

中断:来自I/O设备的信号引起的异常

(IO设备:并不单独指磁盘,像是网卡、定时器、鼠标、键盘都属于IO设备)

注意:不同的书籍,对于中断的解释不一样,这点要注意区别,这里列一下具体的区别

不同体系下中断所指的内容会有所区别

中断的特点

中断最大的特点就是异步

中断是异步的,具体怎么理解?

指信号是由IO设备发出的,并不是CPU处理指令造成的;其他的三种异常,都是CPU执行的过程中出现的问题,所以称为同步的异常

中断的处理流程

如何判断发生了中断?

CPU有很多引脚,如果发生中断,CPU可以感知到中断引脚的电压升高

处理流程

然后会进行一系列的步骤对中断进行处理:

  1. 感知到中断后,从系统总线读取异常号
  2. 通过异常表基址寄存器+异常号,调用对应的中断处理程序
  3. 当处理程序返回时,它就将控制返回给下一条指令

陷阱

陷阱:陷阱的唯一作用就是在用户态与内核态之间提供一个像过程一样的接口(即系统调用

为什么需要有陷阱(系统调用)?

内核提供了很多服务:读文件、创建进程、终止进程等,都属于内核态的功能,用户态没有也不能有这些权限(很危险,如果用户进程都可以随意的操作你的OS服务的话,难以想象,你的电脑还是你的电脑吗)

如何执行系统调用

处理器提供了syscall n指令,用户程序如果需要服务n,那么就可以执行这条指令来使用系统调用

陷入的过程

(与其他异常的过程大同小异)

  1. 用户程序需要系统调用,执行syscall n指令
  2. 通过异常表基址寄存器+异常号,调用对应的异常处理程序(此处为陷阱)
  3. 当处理程序返回时,它就将控制返回给下一条指令

陷阱的特点

陷阱需要与其他异常区别的就是目的不一样,陷阱的目的是切换用户态与内核态的,是为了进行系统调用的

故障

故障:由错误情况引起,可能被修复,可能修复不了

如果故障可以修复,那么在执行完处理程序后,就会返回本条指令,继续执行(比如说缺页异常

如果故障不能修复,那么就调用abort历程,终止引起故障的应用程序

常见的故障

  • 除法错误:比如除零错误、或是除的结果对于目标操作数来说太大
  • 一般保护故障:试图修改一个只读的文本段(Linux为段错误,也叫吐核)
  • 缺页

终止

终止一般代表不能回复的错误,一般为硬件的错误,比如DRAM或是SRAM位损坏发生奇偶错误,将不会返回控制给应用进程,调用abort历程,终止引起故障的应用程序

常见的终止有:机器检查(硬件异常)

内核态与用户态(未完成)

内核态

为什么有内核态与用户态?

​ 为了保证安全内核必须无懈可击,防止用户程序恶意破坏,所以处理器提供了一种机制,限制一个应用可以执行的指令(特权指令)以及它可以访问的地址空间范围

OS是如何实现的内核态?

CPU某个控制寄存器中有一位作为模式位,此位为1就运行在内核态,为0就运行在用户态

内核态的特点

  • 可以访问内存的任何位置
  • 可以执行特权指令

用户程序只能通过系统调用来间接的访问内核的代码和数据

参考资料

  1. 《CSAPP》
  2. 《汇编语言》王爽著