一、中断是什么
- 中断定义:当CPU知道计算机发生了某些事,于是暂停正在执行的程序,转而去处理该事件。当该事件处理完毕后,CPU继续执行刚才的程序。
- 因为有了中断,系统才能并发运行。
- 并发和并行:
- 并发是单位时间内的累积工作量;并行是同时进行的工作量。
- 单核CPU谈并发,多核CPU谈并行。
- OS是个死循环,它是中断驱动的。
while(1){ 操作系统代码 }
- 死循环本身做不了什么,仅仅是保证OS可以周而复始地运行下去,而运行的目的是等待某事的发生。所以OS是事件驱动的,而这个事件就是以中断的形式通知OS的。
- 本书讨论的内容都是关于单核CPU的。
二、中断分类
1. 外部中断
- 定义:外部中断是指来自CPU外部的中断,而外部的中断必须来源于某个硬件,所以外部中断又称为硬件中断。
- 外部中断通过两根信号线通知CPU的:INTR和NMI。INTR引脚收到的中断都是不影响运行的,可以随时处理,甚至不处理;NMI引脚收到的中断都是致命错误,CPU也没有运行下去的必要了。
- 中断处理程序
- 上半段
- 中断处理程序中需要立即执行的部分。
- 在关中断(不被打扰)情况下执行。
- 如:中断答复、硬件复位。
- 下半段
- 中断处理程序中不紧急的部分。
- 在开中断情况下执行。
- 如果有新中断发生,旧中断的下半段可以下CPU,先执行新中断的上半段。
- 上半段
- 可屏蔽中断
- 可屏蔽中断通过INTR引脚进入CPU,外设发出的中断都是可屏蔽中断。
- 不可屏蔽中断
- 不可屏蔽中断通过NMI引脚进入CPU,它表示系统中发生了致命的错误。
- 等同于宣判“计算机运行到此结束”,因为用软件解决不了,所以是什么原因也不重要了。所以不可屏蔽中断的中断向量号统一为2。
2. 内部中断
- 软中断
- 定义:由软件主动引起的中断。
- 中断指令:
- “int 8位立即数”:8位立即数可以表示256种中断。
- “int3”:调试断点指令,它所触发的中断向量号为3。
- 断点的本质是指令的地址,int3指令的机器码为0xcc。调试的本质是调试器fork了一个子进程,子进程用于运行被调试的程序。调试的原理是父进程修改了子进程的指令,将其用int3指令替换,从而子进程调用了int3指令触发中断。
- 流程为:调试器(父进程)将被调试进程(子进程)断点起始地址的第一个字节备份好后,在原地将第1个字节修改为0xcc。当指令执行到断点处时,会去执行机器码为0xcc的int3指令,该指令会触发3号中断,从而去执行该中断对应的中断处理程序。由于中断处理程序运行时要用到寄存器,为了保存调试进程的线程,该中断处理程序必须先将当前的寄存器和相关内存单元压栈保存,用户在查看寄存器和变量时就是从栈中获取的。当恢复执行所调试的进程时,中断处理程序需要将之前的1字节还原至断点处,然后恢复各寄存器和内存单元的值,修改返回地址为断点地址,用iret指令退出中断,返回到用户进程继续执行。
- into:中断溢出指令,中断向量号为4。eflags的OF = 1时才引发中断。
- bound:检查数组索引越界指令,中断向量号为5。用于检测数组的索引下标是否在上下边界之间,指令格式为:bound 16/32位寄存器。
- ud2:未定义指令,中断向量号为6。表示指令无效,CPU无法识别。
- 除了"int 8位立即数"之外,其余的几种又可以称为异常。因为它们兼具软中断的“主动”和异常的“错误”结果。
- 异常
- 定义:在指令执行期间由CPU内部产生的错误引起的。
- 分类:
- Fault:故障。可以再被执行一次。当发生此类异常时,CPU将机器状态恢复到异常之前的状态,之后调用中断处理程序,CPU将返回地址指向导致fault发生的指令。通常中断处理程序会将问题修复,待中断处理程序返回后便能重试。例如:缺页中断page fault。
- Trap:陷阱。此异常通常用在调试中,比如int3指令。为了让中断处理程序返回后可以继续指向下去,CPU将中断处理程序的返回地址指向导致异常的下一条指令。
- Abort:终止。由于错误无法修复,程序将无法继续运行,OS为了自保,只能将程序从进程表中去掉。如:硬件错误、某些数据结构出错。
- eflags中的IF位
- 作用:可以屏蔽中断
- 只要是导致运行错误的中断类型都会无视IF位,不受IF位的管束,如NMI、异常。
- 由于int n型的软中断用于实现系统调用功能,不能因为IF=0就不顾用户的请求,所以为了用户功能正常,软中断必须无视IF位。
- 指令:cli使IF位置0,关中断;sti使IF位置1,开中断。
- 中断向量和中断向量号
- 中断向量(Interrupt Vector)的作用类似于选择子,它专门用于中断描述符表,没有RPL字段。它是一个指向中断处理程序的地址,当发生中断时,处理器会根据中断向量找到相应的中断处理程序并执行。
- 异常和不可屏蔽中断的中断向量号是由CPU主动提供的;来自外部设备的可屏蔽中断向量号是由中断代理(8259A)提供的。
- 中断向量号是一个8位的数字,范围从0到255。中断向量号(Interrupt Vector Number)是用于唯一标识不同中断类型的数字编号。每个中断类型都会被分配一个唯一的中断向量号,这样处理器在处理中断时可以根据中断向量号来确定要执行的中断处理程序。
三、中断描述符表 IDT
1. IDT
- 中断描述符表IDT(Interrupt Descriptor Table)是保护模式下用于存储中断处理程序入口的表,当CPU接收一个中断时,需要用中断向量在IDT中检索相应的描述符,在该描述符中找到中断处理程序的起始地址,然后指向该中断处理程序。
- 中断不仅在保护模式下有,在实模式下用于存储中断处理程序入口的表叫做中断向量表IVT(Interrupt Vector Table)。
- IVT和IDT的区别?
- IDT的地址不受限;IVT必须位于最低端0~0x3ff处共1024字节。
- IDT的每个描述符是8字节;IVT的每个描述符是4字节。
- 门
- 门表示一段程序的入口。门的本质就是描述符,8字节。
- 段描述符描述的是一片内存区域;门描述符描述的是一段代码。门描述符中添加了各种属性,是进门的条件。
- 描述符的高4字节的第8~12位是固定的意义,用来表达描述符的类型。CPU通过这些位便知道该结构是哪种描述符。第8 ~11位是type位,12位是S位,用来表示是系统段/数据段。门都属于系统段,S=0,所以主要区别是type位不同。
- IDT的结构
- IDT是由“门”组成的,任务门、中断门和陷阱门。
- 一个中断源会产生一个中断向量,每个中断向量都对应IDT中的一个门描述符,通过该门描述符就找到了对应的中断处理程序。
- IDTR
- IDTR为中断描述符表寄存器
- 该寄存器分成两部分:第0~15位为表界限,即IDT大小-1;第16 ~47位是IDT的基址。
- IDT的地址要加载到IDTR中,只有IDTR指向了IDT,CPU接受到中断向量号时才能找到中断处理程序,这样中断系统才能正常运行。
- 加载IDTR的指令:lidt 48位内存数据。
2. 中断处理过程及保护
- 完整的中断过程分为CPU外和CPU内:
- CPU外:外部设备的中断由中断过程芯片接受,处理后将该中断的中断向量号发送到CPU。
- CPU内:CPU执行该中断向量号对应的中断处理程序。
- CPU内的中断处理过程:
- CPU根据中断向量号定位中断门描述符
- 中断向量号是IDT的索引,当CPU收到一个外部的中断向量号时,它用此号查询IDT对应的中断描述符,然后再去执行该中断描述符中的中断处理程序。
- 因为中断描述符是8字节,所以CPU用中断向量号*8+IDTR中的基址=中断向量号对应的中断描述符。
- CPU进行特权级检查
- 由于中断是通过中断向量号通知CPU的,中断向量号只是个整数,没有RPL,所以中断的特权级检查不涉及RPL。
- 中断门的特权检查与调用门类似:对于软中断,CPL要在门描述符的DPL与门中目标代码段的DPL之间。
- 对于软中断,CPU先检查进门的下限,即:CPL特权要>=门描述符的DPL;然后检查特权级的上限,CPL的特权要<目标代码段的DPL。除了返回指令外,特权转移只能由低到高。
- 对于外部设备的中断/异常,只检查上限即可。即:CPL特权级<目标代码段DPL。
- 执行中断处理程序
- 特权级检查通过后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP中,开始执行中断处理程序。
- 特权级检查通过后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP中,开始执行中断处理程序。
- CPU根据中断向量号定位中断门描述符
- 中断发生后的eflags变化:
- 中断发生后,eflags中的NT位和TF位都被置0。如果中断对应的门是中断门,则eflags的IF位置0,避免中断嵌套。
- 如果中断发生对应的是任务门/陷阱门,则IF位不置0。因为陷阱门主要用于调试,它允许CPU响应更高级别的中断,允许中断嵌套;任务门是执行一个新任务,任务都应该在开中断的情况下允许,否则就独占CPU资源。
- TF:TrapFlag,陷阱标志位。它用在调试环境中,当TF=0时表示禁止单步运行。在单步执行模式下,处理器在执行每条指令后会引发一个中断,以便调试器或监控程序能够监视程序的执行,并对每条指令的执行进行跟踪和分析。这对于调试程序非常有用,可以逐条指令地检查程序的状态和执行路径。
- NT:Nest Task Flag,任务嵌套标志位。
- 任务嵌套是指CPU将当前任务挂起,然后去执行新的任务,等新任务执行完后再继续执行旧任务。
- 执行新任务前CPU的准备工作:
- 将旧人物的TSS选择子写入新任务TSS中的"上一个任务TSS的指针"字段中。
- 将新任务的eflags中的NT = 1,表示新任务之所以可以执行,是因为有别的任务调用它。
- CPU把新任务执行完后通过iret指令返回去执行旧任务,iret指令有两个功能:中断返回和返回到调用自己执行的那个旧任务中。
- 当CPU执行iret时,会查看NT位。当NT=1时,说明任务是被嵌套执行的,因此会从自己的TSS中“上一个任务TSS的指针”中获取旧任务,然后去执行该任务;若NT = 0,则说明是在中断处理环境下,就执行正常的中断退出流程。
- 从中断返回的指令是iret,它从栈中弹出数据到寄存器cs、eip、eflags中,根据特权级是否改变,判断是否需要恢复旧栈。
3. 中断发生时的压栈
- 中断的过程:
- 中断发生时,CPU会获取一个中断向量号,然后在IDT中找到相应的中断门描述符。
- 门描述符中保存的是中断处理程序所在的代码段的选择子和段内偏移地址,CPU将其加载到CS:EIP中。只要段寄存器被加载,CPU都认为是换了段,所以段描述符缓存寄存器(Cache)也会更新。
- 为了从中断返回后能继续运行原程序,CPU自动将CS和EIP的当前值保存到中断处理程序使用的栈中。不同特权级下是不同的栈,而中断可能在任何特权级下发生。
- 除了要保存CS和EIP外,还需要保存标志寄存器eflags,如果涉及特权级变化,还需要保存栈的SS和ESP寄存器。
- 中断时的压栈:
- 比较CPL和中断门描述符中选择子对应的目标代码段的DPL对比,若CPL权限低,则需要保存旧栈。CPU临时保存SS_old和ESP_old,然后在TSS中找到同目标代码段DPL级别相同的栈加载到寄存器SS和ESP中,记作SS_new和ESP_new,然后将SS_old和ESP_old压入新栈备份。
- 在新栈中压入eflags寄存器。
- 由于要切换到新代码段,所以CS_old和EIP_old入栈。
- 某些异常会有错误码,错误码用于报告异常在哪个段上发生的,所以包含选择子等。错误码入栈,记作ERROR_CODE。
- 从中断返回的过程:
- 中断返回是用iret指令实现的。
- 如果有错误码,CPU不会主动跳过。需要由程序员手动跳过。
- 根据CS_old对应代码段的DPL和CS_old选择子的RPL做对比,查看特权级是否变化。弹出CS_old和EIP_old到CS和EIP寄存器中。
- 将栈中的eflags弹出到eflags寄存器中。如果特权级改变,则此时的栈指针ESP_new指向ESP_old;若特权级没有变化,则此时栈指针为ESP_old。
- 若改变特权级,则弹出SS_old和ESP_old到SS和ESP寄存器中。
- 至此,CPU回到了旧进程(被中断进程)。
4. 中断错误码
- 错误码作用:用来指明中断发生在哪个段上,所以错误码中最主要的部分是选择子。
- 结构:
位 | 作用 |
---|---|
EXT | EXTernal event,外部事件,用来指明中断源是否来自CPU外部。如果中断源为NMI,则EXT=1,否则为0。 |
IDT | 表示选择子是否指向IDT,若选择子指向IDT,则IDT=1,否则指向GDT/LDT。 |
TI | 和选择子中的TI一个意思,TI=1表示是从GDT中检索描述符,=0表示是从LDT中检索描述符。 |
3~15位 | 用来在表(IDT、GDT、LDT)中索引描述符的下标。 |
15~31位 | 全0,若错误码32位都是0,则表示为空描述符,发生错误。 |
- 通常能够压入错误码的中断都属于异常,中断向量号为0~32,而外部中断和int 软中断并不会产生错误码,通常我们也不用处理错误码。
四、可编程中断控制器8259A
1. 8259A介绍
- 中断代理:负责对中断仲裁,决定哪个中断优先被CPU受理。Intel 8259A芯片就是一种可编程的中断代理。
- 8259A的功能:用于管理和控制可屏蔽中断,它表现为屏蔽外设中断,对它们实行优先级判决,向CPU提供中断向量号等。
- 每个独立运行的外设都是一个中断源,它们发出的中断只有接在中断请求(IRQ:Interrupt ReQuest)信号线上才能被CPU知晓。
- 主片与从片:
- Intel处理器共支持256个中断,而8259A可管理8个中断,一个8259A被称为一片。
- 将n片级联在一起,一片为主片(master),其余为从片(slave),一共可以处理8(n-1)+(8-(n-1))=7n+1个中断。
- 一个主片最多级联8个从片,因为级联一个从片要占用主片的一个IRQ接口,而从片上的IRQ接口不会被占用,从片上有专门的接口用于级联。所以9个片最多可以处理64个中断。
- 来自从片的中断只能传递给主片,由主片向上传递给CPU。只有主片才能向CPU发送int中断信号。
- 8259A内部结构:
名称 | 含义 | 作用 |
---|---|---|
INT | 8259A选出优先级最高的中断请求,发信号通知CPU | |
INTA | INT ACknowledge,中断响应信号 | INTA接受来自CPU的INTA接口的中断响应信号 |
IMR | Interrupt Mask Register,中断屏蔽寄存器 | 宽度8位,用来屏蔽某个外设的中断 |
IRR | Interrupt Request Register,中断请求寄存器 | 宽度8位。接收来自IMR寄存器过滤后的中断信号并锁存,相当于未处理的中断信号队列 |
PR | Priority Resolver,优先级仲裁器 | 当多个中断同时发生/有新中断发生时,将它与当前正在处理的中断做对比,找出优先级最高的中断 |
ISR | In-Service Register,中断服务寄存器 | 宽度8位。当某个中断正在被处理时,保存在此寄存器中 |
- 8259A中的寄存器都是8位,因为8259A共8个IRQ接口,可以用8位寄存器中的每一位代表8259A的每个IRQ接口。
- 8259A的工作流程:
- 当某外设发出一个中断时,该中断被送入8259A。8259A首先检查IMR寄存器是否屏蔽了该中断,若IMR寄存器中的位=1,则中断屏蔽(丢弃该中断),=0则中断放行。
- 若IMR中断放行,则中断信号被送入IRR寄存器,该IRQ接口所在的IRR寄存器中对应的BIT置1.
- 在某个恰当时机,PR会从IRR寄存器中挑选一个优先级最大的中断,IRQ接口号越低,优先级越大。
- 8259A在控制电路中通过INT接口向CPU发生INTR信号,信号被送入CPU 的INTR接口后,CPU知道有新的中断来了。当CPU执行完手中的指令后,通过自己的INTA接口向8259A的INTA接口回复一个中断响应信号,表示CPU已准备完毕。
- 8259A收到中断响应信号后,将刚才选出的优先级最大的中断在ISR寄存器中对应的BIT置1,同时在IRR中将该中断去掉(BIT置0)。
- CPU再次发送INTA信号给8259A,想要获取中断向量号。8259A用“起始中断向量号+IRQ接口号”当作该设备的中断向量号。8259A将该中断向量号通过系统数据总线发送给CPU。CPU接收到中断向量号后,在IDT中找到中断描述符,去处理相应的中断处理程序。
- 优先级更高的中断:若有优先级更高的中断到来,则原本ISR中中断的BIT清0,IRR中的BIT=1;随后在ISR中将优先级跟高的新中断的BIT置1,然后将此中断发送给CPU。
- CPU中的中断处理的框架:
- 构造好IDT。
- 提供中断向量号
2. 8259A的编程
-
中断向量号是逻辑上的东西,它在物理上就是8259A的IRQ接口号。
-
8259A内部有两组寄存器
- 一组是初始化命令寄存器,用来保存初始化命令字ICW,ICW共4个:ICW1~4。
- 另一组寄存器是操作命令寄存器组,用来保存操作命令字OCW,OCW共3个:OCW1~3。
-
8259A的编程:
- 初始化:用ICW做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是往8259A的端口发送一系列ICW,也许后面的设置要依赖前面的ICW,所以必须依次写入ICW1~4。
- 操作:用OCW来操作控制8259A。中断屏蔽和中断结束,就是通过往8259A端口发送OCW实现的,发送顺序不固定。
-
ICW:
-
ICW1:用来初始化8259A的连接方式(单片/多片)和中断信号的触发方式(中断请求信号是电平触发/边沿触发)。
位 英文 含义 =1 =0 IC4 是否需要写入ICW4 需要写入 不需要写入 SNGL Single 单片/级联 单片 级联 ADI call address interval 用来设置8085的调用时间间隔,x86不需要设置 LTIM level/edge triggered mode 设置中断检测方式 电平触发 边沿触发 第4位的1 ICW1的标志 -
ICW2:设置起始中断向量号,是IRQ接口到逻辑中断向量号的映射。由于IRQ接口是顺序排列,所以只需要设置IRQ0映射到的中断向量号即可,后面的IRQ接口对应的中断向量号会自动顺下去。所以我们只需要写T3~ T7,ID0~ 2不需要管。
-
ICW3:
- 仅在级联状态下需要,用来设置主片和从片用哪个IRQ接口互连。
- ICW3需要写入主片的0x21端口&从片的0xA1端口。
- 主片:ICW3=1的那一位对应的IRQ接口用于连接从片,为0用于连接外设。
- 从片:从片中有专门用于级联主片的接口不是IRQ。所以从片的高5位为0,不需要。
-
ICW4:
位 英文 含义 =1 =0 SFNM Special Fully Nested Mode 特殊全嵌套模式 特殊全嵌套模式 全嵌套模式 BUF 本8259A芯片是否工作在缓冲模式 工作在缓存模式 工作在非缓存模式 M/S Master/Slave 规定本8259A是主片还是从片 主片 从片 AEOI Auto End Of Interrupt 是否让8259A自动把中断结束(因为8259A在收到中断结束信号时才能处理下一个中断) 自动 非自动 μPM microprocessor 微处理器类型 x86处理器 8080/8085处理器
-
-
OCW:
-
OCW1:
- 用来屏蔽连接在8259A上的外部设备的中断信号,实际上就是把OCW1写入IMR寄存器。
- 屏蔽是指是否把来自外设的中断信号发送给CPU。
- 由于外设的中断都是可屏蔽中断,所以最终要看eflags中的IF位,若IF=0,则就算8259A把外设的中断信号发送过来,CPU也不理会。
- OCW1要写入主片的0x21或从片的0xA1端口。
- M0~ M7对应的是IRQ0~ 7,若某位=1,则屏蔽;=0,则中断放行。
-
OCW2:
- OCW2用来设置中断结束方式和优先级方式。
- OCW2要写入主片的0x20 & 从片的0xA0端口。
位 英文 作用 =1 =0 R Rotation 表示是否按照循环方式设置中断优先级 优先级自动循环 采用固定优先级方式 SL Special Level 是否指定优先等级,等级用低3位指定。SL表示是否开启L2 ~L0位 低三位有效 低三位无效 EOI End Of Interrupt 终端结束命令位,在ICW4中AEOI位为0(非自动结束中断)时有效 ISR中对应位清0,即将当前中断结束 00 OCW2的标识 L2~L0 用来确定优先级的编码,一种用于EOI表示被中断的优先级别;一种用于优先级循环,指定最低优先级别 - 中断结束:
- SL:可以针对某个特定优先级的中断进行操作。
- 发送EOI信号表示结束中断。若SL=1,则用L2 ~L0来指定位于ISR的某个中断被终止;若SL=0,则8259A会自动将正在处理的中断结束。
- 设置优先级控制方式:
- R=0:表示固定优先级方式,即IRQ接口号越低,优先级越高。
- R=1:表示用循环优先级方式,这样优先级会在0 ~7内循环,当某级别的优先级被处理后,它的优先级变为最低。
- 也可以打开SL开关,使SL=1,通过L2 ~L1指定最低优先级是哪个LRQ接口。
-
OCW3:
- 用来设定特殊屏蔽方式和查询方式。
- OCW3要写入主片的0x20端口 / 从片的0xA0端口。
位 英文 作用 =1 =0 7位 用不到 ESMM和SMM Enable Special Mask Mode 和 Special Mask Mode 用来启用/禁用特殊屏蔽模式,ESMM是特殊屏蔽模式允许位,是个开关;SMM是特殊屏蔽模式位。只有启用ESMM时SMM才有效 ESMM和SMM都=1,则正式工作在特殊屏蔽模式下 ESMM=0则SMM无效;ESMM=1,SMM=0表示为工作在特殊屏蔽模式 01 OCW3标识 P Poll command 查询命令 设置8259A为中断查询方式,这样可以通过读取寄存器如IRS来查看当前的中断处理情况 RR Read Register 读取寄存器命令。和RIS位配合使用 可以读取寄存器 RIS Read Interrupt register Select 读取中断寄存器选择位,选择待读取的寄存器;能读取的前提是RR=1 选择ISR寄存器 选择IRR寄存器 -
-
8259A的两个端口地址如何识别4个ICW和3个OCW?
- ICW1和OCW2、OCW3是用偶地址端口0x20(主片)或0xA0(从片)写入。
- ICW2~ICW4和OCW1是用奇地址端口0x21(主片)或0xA1(从片)写入。
- 4个ICW要按照次序写入,这样8259A就知道写入端口的数据是什么了。
- OCW的写入顺序无关,且ICW1和OCW2、OCW3的写入端口是一致的,8259A怎么辨别呢?通过各控制字中的第4~3位标识位。
- OCW1是怎么确定的?OCW是在初始化后才有效的,所以初始化后写入奇地址端口的数据便被认为是OCW1。
-
8259A的编程就是写入ICW和OCW,步骤为:
- OCW的初始化必须最先完成:
- 物理8259A是否级联,ICW1和ICW2必须有,并且要按顺序写入。
- 只有当ICW1中的SNGL=0时,才表示级联。需要设置主片和从片,在主片和从片中各写入ICW3。
- 只有当ICW1中的IC4=1时,才需要写入ICW4.x86中的ICW4必须为1。
- 初始化8259A后才可以用OCW对它操作。
- OCW的初始化必须最先完成:
五、编写中断处理程序
1. 最简单的中断处理程序
- 开启中断的流程:
- init_all函数用来初始化所有的设备和数据结构,我们在主函数中调用它来完成初始化工作。
- init_all首先调用idt_init,它用来初始化中断相关的内容。
- idt_init调用pic_init和ide_desc_init来完成初始化。pic_init用来初始化可编程中断控制器8259A。ide_desc_init用来完成初始化IDT。
- idt_init完成后便可以加载IDT了,到此打开中的条件准备好了。
- 宏
- 宏属于预处理指令,预处理指令是编译器为用户提供的、仅能被编译器中的预处理器支持的符号,故属于伪指令。
- 宏,即Macro,是用来替代重复性输入,是一段代码的模板。
- 如果定义单行的宏,可以用%define指令实现,和C语言中的define用法一致。
- 多行的宏要用%macro实现:
%macro 宏名字参数个数 宏代码体 %endmacro
- "宏名字"是调用宏时用的。"参数个数"是告诉预处理器,此宏支持的参数个数。在"宏代码体"中,如果想引用某个参数,就用%数字的方式来引用,从左边起第一个参数为%1。
//宏定义 %macro mul_add 3 mov eax,%1 add eax,%2 add eax,%3 %endmacro //调用方式 mul_add 45,24,33
--------------------------代码-------------------------------
- 用汇编语言实现中断处理程序
- 在这个初始化过程中,最核心、最底层的就是ide_desc_init,在该函数中我们要填充中断处理程序的地址到IDT中。
- 当中断发送时,CPU要在目标栈中保存被中断的部分寄存器环境,这是CPU自动完成的。
- “/home/lily/OS/boot/kernel/kernel.S”
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了错误码,为保持栈中格式同一,这里不做操作
%define ZERO push 0 ;若在相关异常中CPU没有压入错误码,为了格式统一,就手工压入一个0
extern put_str ;声明外部函数
section .data ;代码段开始
intr_str db "interrupt occur!", 0xa, 0 ;0xa 是换行符,0 是字符串的结束符
global intr_entry_table ;intr_entry_table位于data段,链接时会和宏中的data段放在一起
intr_entry_table: ;构造intr_entry_table数组,只是构造数组,并不调用中断,中断由硬件触发,目前由时钟触发
;宏开始
%macro VECTOR 2 ;定义了一个叫VECTOR的宏,接收两个参数。一个参数是中断向量号,第二个参数也是个宏
section .text
intr%1entry: ;%1表示第一个参数,不是英文L。中断处理程序的起始地址;每个中队处理程序都要压入中断向量号,所以一个中断类型一个向量号。自己知道自己的向量号是多少
;intr%1entry 表示一个带有参数的标签名。在宏被展开时,%1 将被实际的参数替换,从而形成一个具体的标签名。如果我们调用 VECTOR 0x20, ZERO,宏展开后的结果将是:intr0x20entry:
%2 ;调用宏的第二个参数
push intr_str
call put_str ;中断就是打印字符串intr_str
add esp,4 ;跳过参数
;如果是从片中进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ;中断结束命令EOI。OCW2的第5位是EOI位,其余都是0,所以是0x20。我们要向8259A发送结束标志,不然手动结束的8259A不知道中断结束了。
out 0xa0,al ;向主片发送
out 0x20,al ;向从片发送
add esp,4 ;跳过error_code,就算没有error_code,ZERO中也压入了一个0
iret ;从中断返回,32位下=iretd
section .data ;表示上一个代码段结束
dd intr%1entry ;dd用来定义数组元素的宽度,元素值为intr%lentry;存储各个中断入口程序的地址,形成intr_entry_table数组。这样每个宏调用都会在数组中产生新的地址元素
%endmacro
;宏结束
VECTOR 0x00,ZERO ;预处理时会将其展开为宏,intr0x00entry为一个符号(符号就是地址),function为.text,该符号会被写入intr_entry_table数组中。
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
- 创建中断向量表IDT,安装中断处理程序
- “/home/lily/OS/boot/kernel/interrupt.c”
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //目前共支持的中断数,33
#define PIC_M_CTRL 0x20 //主片的控制端口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
/*中断门描述符结构体*/
struct gate_desc{
uint16_t func_offset_low_word; //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
uint16_t selector; //低32位——16~31位:目标CS段选择子
uint8_t dcount; //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
uint8_t attribute; //高32位——8~15位:P+DPL+S+TYPE
uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};
//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //IDT是中描述符表,实际上是中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //声明引用定义在kernel.S中的中断处理函数入口数组
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址
//中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
/*完成所有有关中断的初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
pic_init(); //初始化8259A
/*加载idt*/
// uint64_t idt_operand = ((sizeof(idt)-1) | (uint64_t)((uint32_t)idt << 16)); //书上是错的
uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)(uint32_t)idt << 16)); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
//put_int(idt_operand);
asm volatile("lidt %0" : : "m" (idt_operand)); //加载IDT,IDT的0~15位是表界限,16~47位是表基址
put_str("idt_init done\n");
}
- “/home/lily/OS/boot/kernel/interrupt.h”
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
- “/home/lily/OS/boot/kernel/global.h”
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)
/*IDT描述符属性*/
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32位的门
#define IDT_DESC_16_TYPE 0x6 //16位的门,不会用到
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)
#endif
- 用内联汇编实现端口I/O函数
- “/home/lily/OS/boot/lib/kernel/io.h”
/*机器模式
b -- 输出寄存器QImode名称,即寄存器的低8位:[a-d]l
w -- 输出寄存器HImode名称,即寄存器中2字节的部分,如[a-d]x
HImode
"Half-Integer"模式,表示一个两字节的整数
QImode
"Quarter-Integer"模式,表示一个一字节的整数
*/
#ifndef __LIB_KERNEL_IO_H
#define __LIB_KERNEL_IO_H
#include "stdint.h"
/*向端口port写入一个字节*/
static inline void outb(uint16_t port,uint8_t data){
/*对端口指定N表示0~255,d表示用dx存储端口号,%b0表示对应al,%w1表示对应dx*/
//outb:将一个字节的数据从CPU输出到指定的I/O端口
asm volatile("outb %b0,%w1" :: "a"(data),"Nd"(port));
}
/*将addr处起始的word_cnt个字写入端口port*/
static inline void outsw(uint16_t port,const void* addr,uint32_t word_cnt){
/*+表示此限制既做输入,又做输出 outsw是把ds:esi处的16位内容写入port端口,我们在设置段描述符时,已经把ds,es,ss段的选择子都设置为相同的值了,此处不用担心数据混乱*/
asm volatile("cld;rep outsw":"+S"(addr),"+c"(word_cnt):"d"(port));
}
/*将从端口port读入一个字节返回*/
static inline uint8_t inb(uint16_t port){
uint8_t data;
asm volatile("inb %w1,%b0":"=a"(data):"Nd"(port)); //inb:从指定的I/O端口读取一个字节的数据,并将其加载到CPU的寄存器中
return data;
}
/*将从端口port读入的word_cnt个字写入addr*/
static inline void insw(uint16_t port,void* addr,uint32_t word_cnt){
/*insw是将端口port处读入的16位内容写入es:edi指向的内存
因为ds,es,ss处我们设置了数据相同,所以此处不需要担心紊乱*/
asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
}
#endif
- 设置8259A
- 加载IDT,开启中断
- “/home/lily/OS/boot/kernel/init.c”
#include "init.h"
#include "print.h"
#include "interrupt.h"
/*负责初始化所有模块*/
void init_all(){
put_str("init_all\n");
idt_init(); //初始化中断
}
- “/home/lily/OS/boot/kernel/init.h”
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
- “/home/lily/OS/boot/kernel/main.c”
#include "print.h"
#include "init.h"
void main(void){
put_str("I am kernel\n");
init_all();
asm volatile("sti"); //为演示中断,在此临时开中断,sti的作用是将IF=1
while (1);
}
- 编译链接
- [main.o在链接时必须放在第一个,链接时,调用在前,实现在后(P274)
nasm -f elf -o build/print.o lib/kernel/print.S
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o
- 写入磁盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/build/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
- 启动bochs
bin/bochs -f bochsrc.disk
- 查看idt
info idt
2. 改进中断处理程序
- 改进方案
- 为防止kernel.S冗长,我们选择在C中编写中断处理程序,在汇编中调用它。
- 在汇编中的intrXXentry中调用C的中断处理函数;在C中建立目标中断处理函数数组idt_table,数组元素是C的中断处理函数,供汇编intrXXentry调用。
- 对于C的idt_table[i],汇编调用它是用call [idt_table + i*4],因为idt_table是函数数组,里面的元素为函数指针,即函数的地址。在32位系统中函数地址为32位,所以是4字节。
- 保持环境的入栈情况:
--------------------------代码-------------------------------
- “/home/lily/OS/boot/kernel/interrupt.c”
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //目前共支持的中断数,33
#define PIC_M_CTRL 0x20 //主片的控制端口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
/*中断门描述符结构体*/
struct gate_desc{
uint16_t func_offset_low_word; //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
uint16_t selector; //低32位——16~31位:目标CS段选择子
uint8_t dcount; //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
uint8_t attribute; //高32位——8~15位:P+DPL+S+TYPE
uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};
//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //IDT是中描述符表,实际上是中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //指针格式应该与数组类型一致,这里intr_entry_table中的元素类型就是function,就是.text的地址。所以用intr_handle来引用。声明引用定义在kernel.S中的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //idt_table为函数数组,里面保持了中断处理函数的指针
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址
//中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
/*通用的中断处理函数,一般用在异常出现时的处理*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){ //IRQ7和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char('\n');
}
/*初始化idt_table*/
static void exception_init(void){
//将idt_table的元素都指向通用的中断处理函数,名称为unknown
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
idt_table[i] = general_intr_handler; //默认为general_intr_handler,以后会由register_handler来注册具体函数
intr_name[i] = "unknown";
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
//intr_name[15]是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/*完成所有有关中断的初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
exception_init();
pic_init(); //初始化8259A
/*加载idt*/
// uint64_t idt_operand = ((sizeof(idt)-1) | (uint64_t)((uint32_t)idt << 16)); //书上是错的
uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)idt << 16)); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
asm volatile("lidt %0" : : "m" (idt_operand)); //加载IDT,IDT的0~15位是表界限,16~47位是表基址
put_str("idt_init done\n");
}
- “/home/lily/OS/boot/kernel/kernel.S”
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了错误码,为保持栈中格式同一,这里不做操作
%define ZERO push 0 ;若在相关异常中CPU没有压入错误码,为了格式统一,就手工压入一个0
extern put_str ;声明外部函数
extern idt_table
section .data ;代码段开始
global intr_entry_table ;intr_entry_table位于data段,链接时会和宏中的data段放在一起
intr_entry_table: ;构造intr_entry_table数组,只是构造数组,并不调用中断,中断由硬件触发,目前由时钟触发
;宏开始
%macro VECTOR 2 ;定义了一个叫VECTOR的宏,接收两个参数。一个参数是中断向量号,第二个参数也是个宏
section .text
intr%1entry: ;%1表示第一个参数,不是英文L。中断处理程序的起始地址;每个中队处理程序都要压入中断向量号,所以一个中断类型一个向量号。自己知道自己的向量号是多少
%2 ;压入错误码,中断若有错误码会压入到eip后
;保存上下文环境,保存ds,es,fs,gs和8个32位通用寄存器
push ds
push es
push fs
push gs
pushad
;发送EOI信号的目的是告知硬件中断控制器8259A,当前中断已经处理完成,可以继续处理其他中断请求。
;如果是从片中进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ;中断结束命令EOI。OCW2的第5位是EOI位,其余都是0,所以是0x20。我们要向8259A发送结束标志,不然手动结束的8259A不知道中断结束了。
out 0xa0,al ;向主片发送
out 0x20,al ;向从片发送
;处理中断,即调用中断处理函数
push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量号
call [idt_table + %1*4]
;中断结束,返回原进程
jmp intr_exit
section .data ;表示上一个代码段结束
dd intr%1entry ;dd用来定义数组元素的宽度,元素值为intr%lentry;存储各个中断入口程序的地址,形成intr_entry_table数组。这样每个宏调用都会在数组中产生新的地址元素
%endmacro
;宏结束
section .text
global intr_exit
intr_exit:
;跳过中断号
add esp,4
;恢复上下文环境
popad
pop gs
pop fs
pop es
pop ds
;跳过error_code
add esp,4
;返回主程序,iretd用于从中断服务例程(ISR)返回到被中断的程序。
iretd
VECTOR 0x00,ZERO ;预处理时会将其展开为宏,intr0x00entry为一个符号(符号就是地址),function为.text,该符号会被写入intr_entry_table数组中。
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
- 编译链接
- [main.o在链接时必须放在第一个,链接时,调用在前,实现在后(P274)
nasm -f elf -o build/print.o lib/kernel/print.S
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o
- 写入磁盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/build/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
- 启动bochs
bin/bochs -f bochsrc.disk
3. 调试:CPU进入中断时压栈出栈完整过程
- 找到中断发生的指令数——0x00018240345
- 看入栈情况
六、可编程计数器/定时器 8253简介
- 第六章用到的不多。
- 凡是用到计数器/定时器的地方,我们都用定时计数器来代替。
1. 时钟——给设备打拍子
- 时钟并不是计算机处理速度的衡量,而是一种使设备间相互配合而避免发生冲突的节拍。
- 各种设备都有自己的时钟,CPU和外设的时钟 肯定不是一个数量级。
- 时钟分类:
- 内部时钟
- 内部时钟是指CPU中内部元件的工作时许,主要用于控制、同步内部工作过程的步调。
- 内部时钟是由晶体振荡器产生晶振,经过分频后便是主板的外频。Intel处理器将外频乘上某个倍数称为主频,CPU取指令、执行指令所消耗的时钟周期便是基于主频的。
- 内部时钟是由CPU固件结构决定的,出厂时就设定好的,通常以纳秒 (ns) 为单位。
- 外部时钟
- 外部时钟是指 CPU与外设/外设之间 通信时采用的一种时序。
- 单位一般是毫秒 (ms) 或秒 (s) 级的。
- 内部时钟
- 定时计数器
- 为了保证运行在不同时钟节拍下的设备能够同步通信,应以CPU的内部时钟为依据来设计外部时钟,既要符合CPU内部运行时序的规定,又要满足外设工作时序的要求。
- 定时计数器就是用来解决时序配合问题的。
- 晶振的频率过高,必须送到定时计数器来分频才能产生所需要的各种信号。
- 外部定时的实现
- 软件实现
- 例如让CPU执行9w次空循环,通过这种延时来达到定时作用。浪费CPU时间。
int cycle_cnt = 90000; while(cycle_cnt-->0);
- 硬件实现
- 硬件实现的称为定时器,作用是发送信号。
- 过程:当到达了所计数的时间,计数器可以自动发出一个输出信号,可以用该信号向CPU发出中断,这样CPU可以去执行相应的中断处理程序。
- 硬件定时器是独立的,可以同CPU并行工作,可以节省CPU时间。
- 定时器分为可编程和不可变成两种,常用的可编程定时器有Intel 8253/8254/82C54A等,我们用到的是8253。
- 软件实现
- 节拍:计数器的时钟脉冲信号,也称为计数脉冲信号。每一种时钟脉冲信号到来时就会修改计数值。
- 定时器的计数方式:
- 正计时:每一次时钟脉冲发生时,计数值++,直到与设定的目标终止值相等时,提示时间已到。
- 倒计时:先设定好计数器的值,每一次时钟脉冲发生时,计数值–,直到0时提示时间已到。8253是倒计时。
2. 8253入门
- 8253结构
- 8253内部有3个独立的计数器,分别是计数器0~2,端口分别为0x40 ~0x42。
- 各个计数器都要一套自己的寄存器资源,包括一个16位的计数初值寄存器、一个计数器执行部件和一个输出锁存器。
- 计数器执行部件是核心,本质是一个减法器。
寄存器 | 位数 | 作用 |
---|---|---|
计数初值寄存器 | 16位 | 对8253初始化时写入的计数初始值就保存在此 |
计数器执行部件 | 16位 | 减法计数器 |
输出锁存器 | 16位 | 为了让外界可以随时获得当前计数值 |
- 引脚
引脚 | 信号 | 作用 |
---|---|---|
CLK | 时钟输入信号 | 计数器自己的时钟频率,每当该引脚收到一个信号时,计数器 - - |
GATE | 门控输入信号 | 在某些工作方式下用于控制计数器是否开始计数 |
OUT | 计数器输出信号 | 当定时结束时,该引脚输出相应信号,用来通知CPU或外设定时结束 |
- 工作过程:计数开始之前,计数初值保存在计数初值寄存器中,计数器执行部件(减法计数器)将初值载入后,计数器CLK引脚每收到一个信号,碱法计数器数值 - -,同时将当前数值保存到输出锁存器中。当计数器==0是,通过OUT引脚发出信号向CPU发出中断请求/直接启动某外设。
- 计数器0的作用是产生时钟信号,这个时钟是指连接到主片IRQ0引脚上的那个时钟,也就是说计数器0决定时钟中断信号的发生频率。
- 时钟中断流程:计数器0计时到期后就会发出时钟中断信号,8259A就会感知引脚IRQ有中断信号到来。
3. 8253控制字
- 控制字寄存器
- 端口号0x43
- 是8位大小的寄存器
- 也成为控制模式寄存器
- 控制字寄存器中保存的内容称为控制字,控制字用来设置所指定的计数器(通道)的工作方式、读写格式和数制。
- 控制模式:工作方式、读写格式和数制
- 三个计数器是独立的,每个计数器都要明确自己的控制模式才能知道该怎样去工作,他们各自的控制模式都要以控制字的形式在同一个控制字寄存器中设定。
- 控制字的结构
位 | 名称 | 作用 |
---|---|---|
SC1和SC0 | 选择计数器位 | 用来选择待操作的是哪个计数器 |
RW1和RW0 | 读/写/ 锁存操作位 | 用来设置待操作计数器的读写和锁存方式 |
M2~M0 | 工作方式(模式)选择位 | 每个计数器有6中不同的工作模式 |
BCD | BCD码计数位 | 表示该计数方式是BCD还是二进制。BCD是用4位二进制表示1位10进制数 |
4. 8253工作方式
- 计数器计时的条件
- GATE为高电平,即GATE=1,由硬件控制。
- 计数初值已经写入减法计数器中,由软件out指令控制。
- 启动方式:
- 软件启动:GATE=1,只需要CPU用out指令向减法计数器中写入初值,计数器便开始计数。工作方式0、2、3、4都是软件启动。
- 硬件启动:减法计数器中已有初值,当GATE引脚迎来上升沿后,计数器便开始工作。工作方式1、5都是硬件启动。
- 终止方式:
- 强制终止:工作方式2和3中,计数器是循环计数。只能通过GATE置0让它终止。
- 自动终止:工作方式0、1、4、5中,计时器是单次计数的。如果想在执行过程中终止,需要将GATE置0。
- 8253初始化步骤:
- 向控制字寄存器端口0x43中写入控制字
- 在所指定使用的计数器端口写入计数初值
七、提高时钟中断的频率
- 如何提高时钟中断频率?
- IRQ0引脚上的时钟中断信号频率是由8253的计数器0设置的。
- 时钟发出的中断不能只有一次,需要循环。我们选择方式2,是标准的分频方式。
- 计数器发出输出信号的频率是由计数初值决定的,所以要向计数器0中赋予合适的计数初值。
--------------------------代码-------------------------------
- “/home/lily/OS/boot/device/timer.c”
#include "timer.h"
#include "io.h"
#include "print.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY/IRQ0_FREQUENCY
#define COUNTER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
/*把操作的计数器counter_no、读写锁属性rw1、计数器模式counter_mode写入模式控制寄存器中并赋予初始值counter_value*/
static void frequency_set(uint8_t coutner_port,uint8_t counter_no,uint8_t rw1,uint8_t counter_mode,uint16_t counter_value){
/*向控制字寄存器端口0x43写入控制字*/
outb(PIT_CONTROL_PORT,(uint8_t)(counter_no << 6 | rw1 << 4 | counter_mode << 1));
/*先写入counter_value的低8位*/
outb(coutner_port,(uint8_t)counter_value);
/*再写入counter_value的高8位*/
outb(coutner_port,(uint8_t)(counter_value >> 8));
}
/*初始化PIT8253*/
void timer_init(){
put_str("timer_init start\n");
/*设置8253的定时周期,也就是发中断的周期*/
frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH,COUNTER_MODE,COUNTER0_VALUE);
put_str("timer_init done\n");
}
- “/home/lily/OS/boot/device/timer.h”
#ifndef __DEVICE_TIMER_H
#define __DEVICE_TIMER_H
#include "stdint.h"
void timer_init();
#endif
- “/home/lily/OS/boot/kernel/init.c”
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
/*负责初始化所有模块*/
void init_all(){
put_str("init_all\n");
idt_init(); //初始化中断
timer_init(); //初始化PIT
}
- 编译链接
- [main.o在链接时必须放在第一个,链接时,调用在前,实现在后(P274)
nasm -f elf -o build/print.o lib/kernel/print.S
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc -m32 -I lib/kernel -c -o build/timer.o device/timer.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o build/timer.o
- 写入磁盘
dd if=/home/lily/OS/boot/mbr.bin of=/home/lily/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/lily/OS/boot/loader.bin of=/home/lily/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
dd if=/home/lily/OS/boot/build/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
- 启动bochs
bin/bochs -f bochsrc.disk