程序异常与中断¶
进程与进程的上下文切换¶
- CPU所执行的指令的地址序列称为CPU的控制流,通过按顺序执行和通过过CALL/RET/Jcc/JMP等指令跳转到转移目标地址处执行两种方式得到的控制流为正常控制流。
控制流¶
异常控制流¶
- CPU会因为遇到内部异常或外部中断等原因而打断程序的正常控制流,转去执行操作系统提供的针对这些特殊事件的处理程序。
- 由于某些特殊情况引起用户程序的正常执行被打断所形成的意外控制流称为异常控制流。
- 原因
- 硬件层面
- 内部异常(缺页、越权、越级、整除0、溢出等)
- 外部中断(Ctrl-C、打印缺纸、DMA结束等)
- 进程的上下文切换(发生在操作系统层)
- 一个进程直接发送信号给另一个进程(发生在应用软件层)
逻辑控制流¶
- 对于确定的数据集,某进程指令执行地址序列是确定的 。称为进程的逻辑控制流。
- 对于单处理器系统,进程会轮流使用处理器,即处理器的物理控制流由多个逻辑控制流组成。
- 逻辑控制流不会因被其他进程打断而改变, 还能回到原被打断的“断点”处继续执行
- 在时间上有交错或重叠的情况被称为并发,并行是并发的特例,两个同时执行的进程是并行的(运行在不同的处理器核)
程序和进程¶
- 程序指按某种方式组合形成的代码和数据集合,代码 即是机器指令序列,因而程序是一种静态概念。
- 进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程
- 一个可执行目标文件(即程序)可被加载执行多次,也即,一个程序可能对应多个不同的进程。
- 进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或 终止)而消亡,它所占用的资源也随着进程的终止而释放。
- 操作系统(管理任务)以外的都属于“用户”的任务。
- 引入进程的原因
- 每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器
- 每个进程拥有一个私有的虚拟地址空间,使得程序员 以为自己的程序在执行过程中独占使用存储器
- 进程”的引入简化了程序员的编程以及语言处理系统的处理 ,即简化了编程、编译、链接、共享和加载等整个过程。
进程与上下文切换¶
- OS通过处理器调度让处理器轮流执行多个进程。实现不同进程中指令交替执行的机制称为进程的上下文切换
- 因此感觉到的运行时间比真实执行时间要长
-
处理器调度等事件会引起用户进程正常执行被打断,因而形成异常控制流。 进程的上下文切换机制很好地解决了这类异常控制流,实现了从一个进程安全切换到另一个进程执行的过程。
-
如在命令行中运行hello.c
##### 上下文
- 寄存器只有一套,因此更换进程时寄存器上下文要保存到系统级上下文中
进程的存储器映射¶
- 建立虚拟地址空间与磁盘的映射
-
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 将指定(磁盘)文件fd中偏移量offset开始的长度为length个字节的一块信息映射到虚 拟空间中起始地址为start、长度为length个字节的一块区域,得到vm_area_struct 结构的信息,并初始化相应页表项,建立文件地址和区域之间的映射关系。进程运行时,第一次访问时会发生缺页,需要进行缺页处理(磁盘读取)
- 私有<->共享
- 普通<->匿名(请求0,不需要从此磁盘进行读取)
- 由于进程很多时,主存中可能不存在空闲的页框,当一个进程需要装入新的页时就需要淘汰某个进程的页面,如果被淘汰的页面被修改过,就需要将其从主存页框保存到指定的交换文件中,以后再次访问时从交换文件中调入。
-
- 共享库代码在内存和硬盘都只需要一个副本
-
- 采用写时拷贝技术(写时出错了再拷贝)
- 只读代码区一样(只有一份)(减少空间占用),可读可写数据 区开始也一样(修改后不一样,需要进行拷贝),但属于私有对象
-
磁盘上elf->运行->进程
- 通过虚拟地址与主存上的物理地址进行映射
程序的加载与运行¶
- UNIX/Linux系统中,可通过调用execve()函数来启动加载器。
- execve()函数的功能是在当前进程上下文中加载并运行一个新程序。
- 构造参数->fork创建进程->execve装载映射->运行main
- 加载器根据可执行文件 的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷 贝”到存储器中(实际上不会真正 拷贝,仅建立一种映像)
- 加载后,将PC(EIP)设定指向 Entry point (即符号_start处),最终 执行main函数,以启动程序执行。
用户态与内核态¶
- 为了区分处理器运行的是用户代码还是内核代码,必须有一 个状态位来标识,这个状态位称为模式位
- 处理器模式分用户模式(用户态)和内核模式(核心态)
- 用户模式(也称目态、用户态)下,处理器运行用户进程, 此时不允许使用特权指令
- 内核模式(有时称系统模式、管理模式、超级用户模式、管 态、内核态、核心态)下处理器运行内核代码,允许使用特权指令,例如:停机指令、开/关中断指令、Cache冲刷指 令等
异常和中断¶
- 程序执行过程中CPU会遇到一些特殊情况,使正在执行的程序被“中断”
- CPU中止原来正在执行的程序,转到处理异常情况或特殊事件的程序去 执行,结束后再返回到原被中止的程序处(断点(发生异常的下一条指令的位置))继续执行。
程序异常¶
- 发生异常(exception)和中断(interrupt)事件后,系统将进入OS内核态对相应事件进行处理,即改变处理器状态(用户态→内核态)
- 内核控制路径:在当前进程上跑内核的处理程序而不是新开一个进程
异常的分类¶
- 故障(fault) :执行指令引起的异常事件,如溢出、非法指令、缺页、 访问越权等。 “断点”为发生故障指令的地址
- 因为该指令出现了故障无法正常执行,因此需要解决后返回重新执行
- 自陷(Trap) :预先安排的事件(“埋地雷”),如单步跟踪、断点、 系统调用(执行访管指令) 等。是一种自愿中断。 “断点”为自陷指令下条指令地址
- 终止(Abort) :硬故障事件,此时机器将“终止”,调出中断服务程 序来重启操作系统。 “断点”是什么?随便!
故障-页故障¶
- 执行每条指令都要访存(取指令、取操作数、存结果) 在保护模式下,每次访存都要进行逻辑地址向物理地址转换 ,在地址转换过程中会发现是否发生了“页故障”!
- 如越界、越权、缺页等
- &&
- 它们都位于起始地址为0x08048000(是一个4KB页面的起 始位置)的同一个页面,执行这三条指令之前,该页已经调 入内存。因为没有其他进程在系统中运行,所以不会因为执 行其他进程而使得调入主存的页面被调出到磁盘。因而都不 会在取指令时发生缺页。
- 对a[10](地址0x8049028)的访问是对所在页面(首 址为0x08049000)的第一次访问,故不在主存,缺页处理结束后,再回到这条movl指令重新执行,再访问数据就没有问题了。
- 对a[1000](地址0x8049fa0)的访问是对所在页面 (首址为0x08049000)的第2次访问,故在主存,不 会发生缺页。但a[1000]实际不存在,只不过编译器未 检查数组边界,0x8049fa0处可能是x的地址,故该 指令执行结果可能是x被赋值为3
- 地址0x8052c40偏离数组首址0x8049000已达4×10000 +4=40004个单元,即偏离了9个页面,很可能超出可读写 区范围,故执行该指令时可能会发生保护违例。页故障处理 程序发送一个“段错误”信号(SIGSEGV)给用户进程,用 户进程接受到该信号后就调出一个信号处理程序执行,该信 号处理程序根据信号类型,在屏幕上显示“段故障 (segmentation fault)”信息,并终止用户进程
陷阱自陷¶
- 陷阱也称自陷或陷入,执行陷阱指令(自陷指令)时,CPU调出特定程序(主动中断用户程序执行,转到内核程序进行进一步的处理) 进行相应处理,处理结束后返回到陷阱指令下一条指令执行。
- 陷阱系统为用户程序和内核之间提供了一个过程一样的接口(系统调用),用户程序利用这个接口可以方便地使用操作系统提供的一些服务
-
int指令:调用内核指令进行处理
-
opening file
-
程序调试
程序终止¶
程序中断¶
- 外设通过中断请求信号线向CPU提出“中断”请求,不由指令引起,故中断 也称为异步异常。
- 每执行完一条指令,CPU就查看中断请求引脚,若引脚的信号有效,则进行 中断响应:将当前PC(断点)和当前机器状态保存到栈中(push),并“关中断”(防止处理过程中再次被中断导致保存的数据被破坏), 然后,从数据总线读取中断类型号,根据中断类型号跳转到对应的中断服务 程序执行。中断检测及响应过程由硬件完成。
中断的分类¶
- 可屏蔽中断:通过INTR 向CPU请求,可通过设置屏蔽字来屏蔽 请求,若中断请求被屏蔽(关中断),则不会被送到CPU。
- 不可屏蔽中断:非常紧急的硬件故障,如:电源掉电,硬件线路 故障等。通过NMI 向CPU请求。一旦产生,就被立即送CPU, 以便快速处理。这种情况下,中断服务程序会尽快保存系统重要 信息,然后在屏幕上显示相应的消息或直接重启系统。(无论是否关中断都要处理)
中断的响应过程¶
- 关中断(“中断允许位”清0):使CPU处于“禁止(可屏蔽)中断”状态,以防止 新中断破坏断点(PC)、程序状态(PSW)和现场(通用寄存器)。
- 保护断点(返回地址676)和程序状态:将断点和程序状态保存到栈或特殊寄存器中
- PC→栈或EPC(专门存放断点的寄存器)
- PSWR →栈或EPSWR (专门保存程序状态的寄存器)
- PSW(Program Status Word):程序状态字
- PSWR(PSW寄存器):如IA-32中的的EFLAGS寄存器
- 识别中断事件
- 软件识别
- 设置一个异常状态寄存器(MIPS中为Cause寄存器),用于记录异常 原因。操作系统使用一个统一的异常处理程序,该程序按优先级顺序 查询异常状态寄存器,识别出异常事件。
- 硬件识别(向量中断)(IA-32采用)
- 用专门的硬件查询电路(中断控制器)按优先级顺序识别异常,得到“中断类型号” ,根据此号,到中断向量表中读取对应的中断服务程序的入口地址。
- 每个中断都有相应的“中断服务程序”,可根据中断类型号找到中断服务程序的入口地址
向量中断方式的实现¶
-
对于IDT表,是异常与中断处理共用的
-
每个异常和中断都有唯一编号,称之为中断类型号(也称向量号)。 如类型0为“除法错”,类型2为“NMI中断”,类型14为“缺页”
- 每个异常和中断有与其对应的异常处理程序或中断服务程序,其入口 地址放在一个专门的中断向量表或中断描述符表中。
- 实模式下,用中断向量表描述
- 保护模式下,用中断描述符表描述
- 前32个类型(0~31)保留给CPU使用,剩余的由用户自行定义(这 里的用户指机器硬件的用户,即操作系统)
- 通过执行INT n(指令第二字节给出中断类型号n,n=32~255)使 CPU自动转到OS给出的中断服务程序执行
- 中断类型
- 用户(这里的用户是指操作系统)自定义类型号为 32~255,部分用于可屏 蔽中断,部分用于软中断
- 可屏蔽中断通过CPU的 INTR 引脚向CPU发出中 断请求
- 给外设使用
- 软中断(由软件/操作系统触发的)指令INT n 被设 定为一种陷阱异常,例如 ,Linux通过
int \$0x80
指令将128号设定为系统 调用,而Windows通过int \$0x2e
指令将46号设定为系统调用。- 软中断是由软件程序显式地触发的,而陷阱是由程序执行期间发生的特定条件触发的。
- 软中断通常用于操作系统内部的功能调用和服务请求。陷阱通常用于处理异常情况,如非法操作、错误或其他不正常情况。
- 实地址模式下的中断向量表
- 实地址模式是Intel为80286及其之后的处理器提供的一种 8086兼容模式。寻址空间1MB,指令地址=CS<<4+IP。中断向量表位于 0000H~03FFH。共256组,每组占四个字节CS:IP (共1KB)。
- 开机后系统首先在实地址模式下工作(只有1MB的寻址空间)
- 开机过程中,需要先准备在实模式下的中断向量表和中断服务程序。通 常,由固化在主板上一块ROM芯片中的BIOS程序完成
- BIOS是基本输入/输出系统的简称, 是针对具体主板设计的,与安装的操作系统无关。
- BIOS包含各种基本设备驱动程序,通过执行BIOS程序,基本设备驱动程 序以中断服务程序的形式被加载到内存,以提供基本I/O系统调用。
- 一旦进入保护模式,就不再使用BIOS。
- BIOS程序检测显卡、键盘、内存等,并在00000H~003FFH区建立中 断向量表,在中断向量所指主存区建立相应的中断服务程序
- BIOS利用INT指令执行特定的中断服务程序把OS从磁盘加载到内存中。 例如,BIOS可通过执行int 0x19指令来调用中断向量0x19对应的中断 服务程序,将启动盘上的0号磁头对应盘面的0磁道1扇区中的引导程序装入内存
- 保护模式下的中断描述符表
- 保护模式下,通过中断描述符表获异常处理或中断服务程序入口地址
- 中断描述符表(Interrupt Descriptor Table,IDT)是OS内核中的 一个表,共有256个表项,每个表项占8个字节,IDT共占用2KB
- IDTR中存放IDT在内存的首地址
- 每一个表项是一个中断门描述符、陷阱门描述符或任务门描述符
- 中断门(Interrupt Gate): 中断门用于处理硬件中断或外部设备触发的异步事件(处理中断)。当硬件中断或外部事件发生时,处理器通过中断向量号找到相应的中断门,然后跳转到中断处理程序执行相应的操作。中断门可以在内核模式和用户模式之间进行切换,以提供对内核代码的特权访问。中断门使用的特权级由其所在的代码段描述符决定。
- 通过中断门进入到一个中断服务程序时,CPU 会清除EFLAGS 寄存器中 的IF 标志,即关中断;
- 内部异常支持嵌套处理
- 陷阱门(Trap Gate): 陷阱门类似于中断门,用于处理同步的异常事件(处理异常)。当执行特定的指令(如INT指令)或发生特定的条件时,处理器通过陷阱向量号找到相应的陷阱门,并跳转到陷阱处理程序执行相应的操作。与中断门不同,陷阱门不会改变特权级,即可以在当前特权级下执行。陷阱门通常用于异常处理和系统调用。
- 不会关中断
- 外部异常不支持嵌套处理
- 任务门(Task Gate): 任务门用于实现任务切换。任务切换是指在多任务操作系统中,从一个任务切换到另一个任务的过程。任务门包含了指向任务状态段(TSS)的指针,通过任务门可以实现任务的切换。当任务切换发生时,处理器会自动保存当前任务的上下文并加载新任务的上下文。任务门通常用于任务切换和多任务管理。
- 不包含偏移地址,只包含TSS段选择符
- Linux中,将类型号为8的双重故障(#DF)用任务门实现,而且是唯一 通过任务门实现的异常。
IA-32¶
基本过程¶
IDT初始化¶
- 中断门:DPL=0,TYPE=1110B。激活所有中断
- 外部中断
- 系统门:DPL=3,TYPE=1111B。激活4、5和128三个陷阱异常,分 别对应指令into、bound和int $0x80三条指令。因DPL为3,CPL≤DPL ,故在用户态下可使用这三条指令
- 是由正在执行的程序自己调用的
- 有条件陷阱(特定情况时触发)
- 系统中断门:DPL=3,TYPE=1110B。激活3号中断(即调试断点), 对应指令int 3。因DPL为3,CPL≤DPL,故用户态下可使用int 3指令。
- 陷阱门:DPL=0,TYPE=1111B。激活所有内部异常,并阻止用户程 序使用INT n(n≠128或3)指令模拟非法异常来陷入内核态运行。
- 用户程序模拟非法异常的方式可以是通过使用INT指令传递一个不被允许的软中断号,以期望触发非法异常并进入内核态。然而,由于操作系统限制了用户程序对特定软中断号的访问权限,这样的模拟行为通常会被拒绝或导致系统产生错误。
- 任务门:DPL=0,TYPE=0101B。激活8号中断(双重故障)。
- Linux内核在启用异常和中断机制之前,先设置好IDT 的每个表项,并把 IDT 首址存入 IDTR。系统初始化时,Linux完成对 GDT、GDTR、IDT 和IDTR 等的设置,以后一旦发生异常或中断,CPU就可通过异常和中断 响应机制调出异常或中断处理程序执行。
- 门的DPL的影响
- 当用户态代码执行系统门(DPL=3)指令时,处理器会检查系统门描述符,并根据其中的入口地址跳转到内核中相应的系统调用处理程序。在内核态中,系统调用处理程序会执行特定的系统调用逻辑,完成请求的操作。处理完系统调用后,控制权会返回到用户态代码,继续执行原始的用户态指令。与执行中断门不同,不会发生特权级别的转换。相反,系统门提供了一种特权级别之间的跳转机制,允许用户态代码直接跳转到特定的系统调用处理程序。
- 而跳转是改变代码执行的位置,以便执行其他代码。特权级别转换是一种特权管理机制,用于控制特权级别之间的访问和执行,而跳转是一种程序控制机制,用于改变代码的执行路径。
- 系统门中的是由用户执行int指令进入内核态,中断门则是出现13号错误后强制转化
中断响应过程¶
- 确定中断类型号i,从IDTR 指向的IDT 中取出第i 个表项IDTi。
- 根据IDTi 中段选择符,从GDTR 指向的GDT 中取出相应段描述符,得到 对应异常或中断处理程序所在段的DPL、基地址等信息。Linux下中断门和陷 阱门对应的即为内核代码段,所以DPL为0,基地址为0。
- 若CPL<DPL或编程异常(即n≠128或3)IDTn 的DPL<CPL,则发生13号异常(立刻切换到内核态)。Linux下, 前者不会发生。后者用于防止恶意程序模拟INT n 陷入内核进行破坏性操作。
- 由内核/硬件发现异常并调用中断处理,而用户不被允许直接调用
- 若CPL≠DPL,则从用户态换至内核态,以使用内核栈。切换栈的步骤:
- 当发生特权操作或需要访问受保护的内核资源时,用户程序需要切换到内核态。这是因为内核态具有更高的权限和更广泛的系统资源访问权限,可以执行特权指令、操作系统服务和访问内核数据结构。而用户态则受到限制,无法直接执行特权操作或访问内核资源。
- 通过切换到内核态,操作系统可以保护关键的内核数据结构和系统资源,防止用户程序对其进行非法访问或破坏。同时,内核态也提供了操作系统执行系统调用、处理中断和异常等核心功能的环境。
- 读TR 寄存器,以访问正在运行的用户进程的TSS段;
- 将TSS段中保存的内核栈的段选择符和栈指针分别装入寄存器SS 和ESP, 然后在内核栈中保存原来用户栈的SS 和ESP
- 内核中的TSS段记录了 每个进程的状态信息, 例如,每个进程对应的 页表、task和mm等结 构信息、内核栈的栈顶 指针SS:ESP 等
- 若是故障,则将发生故障的指令的逻辑地址写入CS 和EIP,以使处理后回到 故障指令执行。其他情况下,CS 和EIP 不变,使处理后回到下条指令执行。
- 在当前栈中保存EFLAGS、CS 和EIP 寄存器的内容(断点和程序状态)。
- 若异常产生了一个硬件出错码,则将其保存在内核栈中。
- 将IDTi中的段选择符装入CS,IDTi中的偏移地址装入EIP,它们是异常处理 程序或中断服务程序第一条指令的逻辑地址(Linux中段基址=0)。
- 下个时钟周期开始,从CS:EIP所指处开始执行异常或中断处理程序!
返回过程¶
- (中断或异常处理程序最后一条指令是IRET)
- 从栈中弹出硬件出错码(保存过的话)、EIP、CS和EFLAGS
- 检查当前异常或中断处理程序的CPL是否等于CS中最低两位,若是 则说明异常或中断响应前、后都处于同一个特权级(说明发生异常之前执行的是内核程序,因此不需要回到用户态),此时,IRET指令 完成操作;否则,再继续完成下一步工作。
- 从内核栈中弹出SS和ESP,以恢复到异常或中断响应前的特权级进程所使用的栈。
- 检查DS、ES、FS和GS段寄存器的内容,若其中有某个寄存器的段 选择符指向一个段描述符且其DPL小于CPL,则将该段寄存器清0。这 是为了防止恶意应用程序(CPL=3)利用内核以前使用过的段寄存器 (DPL=0)来访问内核地址空间。
- 执行完IRET指令后,CPU回到原来发生异常或中断的进程继续执行
异常处理¶
- 不可以恢复的异常
- 异常处理程序发送相应的信号给发生异常的当前进程,以通知当前进程 中止运行。
- 采用向发生异常的进程发送信号的机制实现异常处理,可尽快完成在 内核态的异常处理过程,因为异常处理过程越长,嵌套执行异常的可 能性越大,而异常嵌套执行会付出较大的代价。
- 可以恢复的异常
- 会进行故障恢复,然后返回断点执行
- 若只是缺页,则页故障处 理程序负责把所缺失页面从磁盘装入主存,然后返回到发生缺页故障 的指令继续执行
处理过程¶
- 准备阶段:在内核栈保存通用寄存器内容(称为现场信息),这部分大多 用汇编语言程序实现。
- 处理阶段:采用C函数进行具体处理。函数名由do_前缀和处理程序名组 成,如do_overflow 为溢出处理函数。
- 大部分函数的处理方式:保存硬件出错码(如果有的话)和异常类型号,然后,向当前进程发送一个信号。
- 当前进程接受到信号后,若有对应信号 处理程序,则转执行;若 没有,则调用内核abort例程执行,以终 止当前进程。
- 恢复阶段:恢复保存在内核栈中的 各个寄存器的内容,切换到用户态并 返回到当前进程的断点处继续执行。
中断处理¶
- 中断不是由目标进程引起的,因此给目标破进程发送信号是不能解决问题的
- cpu输入到中断屏蔽字寄存器
- 中断请求寄存器的内容来自于io设备
处理过程¶
- 准备阶段:在内核栈中保存各通用寄存器的内容(称为现场信息) 以及所请求IRQi 的值等,并给PIC回送应答信息,允许其发送新的 中断请求信号。
- 处理阶段:执行IRQi 对应的中断服务例程ISR(Interrupt Server Routine)。中断类型号为32+i
- 恢复阶段:恢复保存在内核栈中的各个寄存器的内容,切换到用户 态并返回到当前进程的逻辑控制流的断点处继续执行。
系统调用处理¶
-
操作系统提供了中断指令int 0x80来主动进入内核,这是用户程序发起的调用访问内核代码的唯一方式
-
系统调用(陷阱)是特殊异常事件,是OS为用户程序提供服务的手段。
- 通常,系统调用被封装成用户程序能直接调用的函数,如exit()、read() 和open(),这些是标准C库中系统调用对应的封装函数。
- Linux中系统调用所用参数通过寄存器传递,传递参数的寄存器顺序依次 为:EAX(调用号)、EBX、ECX、EDX、ESI、EDI和EBP,除调用号以 外,最多6个参数
- 封装函数总是若干条传送指令后跟一条陷阱指令。传送指令用来传递系统调用 的参数,陷阱指令(如int $0x80)用来陷入内核进行处理。
- 当应用程序执行
int 0x80
指令时,处理器会触发一个软中断,控制权转移到操作系统的中断处理程序。操作系统会根据中断号和参数来确定执行的具体系统调用
- 不是所有的陷阱都用于系统调用。陷阱还可以用于其他目的,如调试、异常处理等。因此,陷阱并不等同于系统调用,它是一种通用的异常处理机制,可以被广泛应用于操作系统的各个方面。
-
系统调用号是系统调用跳转表索引值,跳转表给出系统调用服务例程首址
- int 0x80执行过程
- 发生13号异常说明权限不足,是一种可恢复异常,系统会自动从用户态切换到内核态进行处理