Songqian Li's Blog

去历史上留点故事

7.1 中断是什么,为什么要有中断

运用中断能够显著提升并发,从而大幅提升效率。

7.2 操作系统是中断驱动的

略

7.3 中断分类

把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部中断。

外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断两种。

内部中断按中断是否正常来划分,可分为软中断和异常。

7.3.1 外部中断

外部中断是指来自 CPU 外部的中断,而外部的中断源必须是某个硬件,所以外部中断又称为硬件中断。

外部硬件的中断通过两根信号线通知 CPU,一个是 INTR(Interrupt Require),一个是 NMI(Non Maskable Interrupt)。

CPU 通过不同的引脚对两种中断加以区分,只要从 INTR 引脚收到的中断都是不影响系统运行的,可以随时处理,甚至 CPU 可以不处理;而从 NMI 引脚收到的中断,那基本都是硬伤,CPU 没有运行下去的必要了。

可屏蔽中断

可屏蔽中断是从 INTR 引脚进入 CPU 的,可屏蔽的意思是此外部设备发出的中断,CPU 可以不理会,因为它不会让系统宕机,所以可以通过 eflags 寄存器的 IF 位将所有这些外部设备的中断屏蔽。

Linux 中把可屏蔽中断分为上半部和下半部分开处理。中断处理程序是需要完整执行的,不能光为了提高中断响应效率而只执行部分中断处理程序。于是将中断处理程序中需要立即执行的部分划分到上半部,通常只完成中断应答或硬件复位等重要紧迫的工作。将不紧急的部分推迟到下半部完成。所以上半部是在关中断下执行,下半部是在开中断下执行。

不可屏蔽中断

不可屏蔽中断时从 NMI 引脚进入 CPU 的,它表示系统中发生了致命的错误,它等同于宣布:计算机运行结束了。

因为不可屏蔽中断表示计算机出现了致命问题,用软件解决不了了,故每种原因对于软件工程师来说意义不大,就没必要再细分原因,统统为导致宕机的各种原因分配一个中断向量号2就够了。

CPU 收到中断后,得知道发生了什么事情才能执行相应的处理办法。这是通过中断向量表(实模式下)或中断描述符表(保护模式下)来实现的。事先为每一种中断分配一个中断向量号,中断向量号就是一个整数,他就是中断向量表或中断描述符表中的索引下标,用来索引中断项。中断发起时,响应的中断向量号通过 NMI 或 INRT 引脚被传入 CPU,CPU 根据中断向量号在表中检索对应的中断处理程序去执行。

7.3.2 内部中断

内部中断分为软中断和异常。

软中断

软中断就是由软件主动发起的中断,它是主观上的,不是客观上的某种内部错误。

可以发起软中断的指令有:

  • int 8位立即数,8 位可表示 256 种中断,这与处理器支持的中断数是吻合的。

  • int3,调试断点指令,其所触发的中断向量号是 3。我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,子进程用于运行被调试的程序。调试器中设置的断点,原理就是父进程修改了子进程的指令,将其用 int3 指令替换。

  • into,中断溢出指令,触发的中断向量号是 4。能否引发 4 号中断要看 eflags 标志寄存器中的 OF 位是否为 1,为 1 才会引发中断。

  • bound,检查数组索引越界指令,中断向量号 5。指令格式:bound 16/32为寄存器 16/32位内存。寄存器存储待检测的数组下标值,内存存数组下表的下边界和上边界,若下标处于数组索引的范围之外,则会触发 5 号中断。

  • ud2,未定义指令,中断向量号 6。该指令表示指令无效,CPU 无法识别。主动使用它发起中断,常用于软件测试中,无实际用途。

异常

异常是另一种内部中断,是指令执行期间 CPU 内部产生的错误引起的。

异常分为:

  1. Fault,称为故障。这种错误是可以被修复的一种类型,属于最轻的一种异常。

  2. Trap,称为陷阱。此异常通常用在调试中,比如 int3 指令便引发此类异常,为了让中断处理程序返回后能够继续向下执行。

  3. Abort,称为终止。

中断类型:

表中 Error code 字段中,如果值为 Y,表示相应中断会由 CPU 压入错误码。

7.4 中断描述符表

中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表(实模式下是中断向量表,IVT),当 CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断程序。

中断描述符表中有中断描述符、任务门描述符和陷阱门描述符。所以中断描述符表中的描述符有自己的名称——门。

中断描述符表和中断向量表的区别在于:

  1. IDT 地址不受限制,而 IVT 位于 0~0x3ff 共 1024 个字节

  2. IDT 每个描述符 8 字节,IVT 每个向量 4 字节

IDT 同 GDT 一样,硬件上提供了存储其位置的寄存器,中断描述符表寄存器 IDTR:

0~15 位共 16 位是表界限,表示最大范围是 0xffff,即 64KB。故可容纳的描述符个数是 64KB/8=8K=8192 个,但 CPU 只支持 256 个中断,其余的描述符不可用。16~47 位共 32 位是 IDT 线性基地址。

特别注意:

  1. GDT 中的第 0 个段描述符不可用,但 IDT 第 0 个门描述符可用。

  2. 门描述符有个 P 位,将来构建 IDT 时要把 P 位置 0,表示门描述符中的中断处理程序不在内存中;

  3. 同加载 GDTR 一样,加载 IDTR 也有个专门指令lidt,格式:lidt 48位内存数据

7.4.1 中断处理过程及保护

完整的中断过程分为 CPU 外和 CPU 内两部分:

  • CPU 外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到 CPU;

  • CPU 内:CPU 执行该中断向量号对应的中断处理程序。

CPU 外在下一节中提到。

CPU 内的过程:

  1. 处理器更加中断向量号定位中断门描述符。中断向量号*8+IDTR的中断描述符表地址得到中断向量号对应的中断描述符

  2. 处理器进行特权级检查。

    1. 如果是由软件发起的中断,要求特权级满足:门描述符 DPL≤ CPL ≤ 目标代码段 DPL,数值上则反过来。
    2. 如果是由外部设备和异常发起的中断,要求特权级: CPL < 目标代码单 DPL 即可。
  3. 执行中断处理程序。将门描述符目标代码段代码选择子加载到代码段寄存器 CS 中,把门描述符中中断处理程序的偏移地址加载到 EIP,开始执行中断处理程序。

以上过程如图所示:

中断发生后, eflags 中的 NT 位和 TF 位会被置 0。如果中断对应的门描述符是中断门,标志寄存器 eflags 中的 IF 位被自动置 0,避免中断嵌套。

  • NT 位,即 Nest Task ,意为任务嵌套标志位。 当一个任务中又嵌套调用了另一个任务(进程)时,此 NT 位为 1 。

  • TF 位,即 Trap Flag,意为陷阱标志位。在调试环境中,当 TF 为 0 时表示禁止单步执行。

CPU 提供了专门用于控制 IF 位的指令,使用cli使 IF 位为 0,称为关中断;使用sti使 IF 位为 1,称为开中断。当然,不可屏蔽中断不受 IF 控制。

这里说一下任务嵌套调用(或 NT 位的作用):

任务嵌套调用是指 CPU 将当前正执行的旧任务挂起,转去执行另外的新任务,待新任务执行完后,CPU 再回到旧任务继续执行。CPU 在执行新任务前做了两件准备工作:

  1. 将就任务 TSS 选择子写到了新任务 TSS 中的“上一个任务 TSS 的指针”字段中;

  2. 将新任务标志寄存器 eflags 中的 NT 位置 1,表示新任务之所以能够执行,是因为有别的任务调用了它。

CPU 把新任务执行完后通过iret指令返回到旧任务。这时就用到 NT 位。若 NT 位为 1,说明当前任务是嵌套执行的,因此 CPU 会从自己 TSS 中“上一个任务 TSS 的指针”中获取旧任务,然后去执行该任务。若 NT 位为 0,表示当前是在中断处理环境下,于是就执行正常的中断退出流程。

7.4.2 中断发生时的压栈

寄存器入栈情况及顺序:

  1. CPU 根据 CPL 和中断门描述符选择子对应的目标代码段 DPL 比对,若 CPL 权限<DPL,表示向高特权级转移,需要切换到高特权级栈,故将寄存器 SS、ESP 的值临时保存,然后在 TSS 中找到同目标代码段 DPL 级别相同的栈加载到 SS、ESP 寄存器中,并将之前临时保存的 SS 和 ESP 寄存器的值压栈,如图 7-8 A 所示。

  2. 新栈中压入 eflags 寄存器,如图 7-8 B 所示。

  3. 切换代码段是段间转移,需要备份 CS 和 EIP,如图 7-8 C 所示。

  4. 某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,所以错误码中包含选择子等信息,这里也需要将错误码入栈,如图 7-8 D 所示。

若不涉及特权级转移,就不会到 TSS 中寻找新栈,栈中数据就不会有 SS 和 ESP 的值,如图 7-9 所示。

中断执行完中断处理程序后,用iret指令返回到被中断的进程。返回过程是进入中断的逆过程,并且根据 CS_old 和 EIP_old 判断特权级是否有变化,从而判断是否需要从栈中弹出 SS_old 和 ESP_old。

如果中断又错误码,处理器不会主动跳过它的位置,必须手动跳过,也就是说用 iret 指令返回时,当前 esp 必须指向栈中的 EIP_old 位置。

7.4.3 中断错误码

错误码和选自自的格式很像,只是低 2 位不再是 RPL,而是 EXT 和 IDT。

  • EXT 表示 EXTernal event,即外部事件,用来致命中断源是否来自处理器外部。如果中断源是不可屏蔽中断 NMI 或外部设备,EXT 为 1,否则为 0.

  • IDT 表示选择子是否指向中断描述符表 IDT,IDT 为 1 表示指向中断描述符表,否则指向 GDT 或 LDT。

  • TI 和选择子中 TI 是一个意思,为 0 时用来致命选择子是从 GDT 中检索描述符,为 1 时是从 LDT 中检索。

当全 0 的错误码出现时,表示中断的发生与特定的段无关,或者引用了一个空描述符,引用描述符就是往段寄存器中加载选择子时处理器发现选择子指向的描述符是空的。

通常能够压入错误码的中断属于中断向量号 0~32 之内的异常,而外部中断(32~255)和 int 软中断并不会产生错误码。

7.5 可编程中断控制器 8259A

7.5.1 8259A 介绍

中断代理负责对所有中断仲裁,决定哪个中断年优先被 CPU 受理,这里使用的中断代理就是 Intel 8259A 芯片。

8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。

一片 8259A 可以管理 8 个中断,n 片 8259A 通过级联可支持 7n+1 个中断源,最多可级联 9 片,即最多支持 64 个中断。级联时只能有一片 8259A 做主片 master,其余的为从片 slave。来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向 CPU 发送 INT 中断信号。个人电脑中只有两片 8259A 芯片,最多 15 个中断,如图 7-11 所示。

8259A 的内部结构逻辑

  • INT:8259A 选出优先级最高的中断请求后,发信号通知 CPU

  • INTA:INT ACknowledge,中断响应信号。位于 8259A 中的 INTA 接收来自 CPU 的 INTA 接口的中断响应信号。

  • IMR:Interrupt Mask Register,中断屏蔽寄存器,用来屏蔽某个外设中断。

  • IRR:Interrupt Request Register,中断请求寄存器。作用是接受经过 IMR 寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断。

  • PR:Priority Resolver,优先级仲裁器。

  • ISR:In-Service Register,中断服务寄存器。当某个中断正在被处理时,保存在此寄存器中。

中断处理流程
  1. 8259A 接收到中断信号后,首先检查 IMR 中是否已经屏蔽来自该 IRQ 接口的中断信号(IMR 中的位为 1 表示中断屏蔽,0 表示中断放行)。

  2. 未屏蔽则将中断信号送到 IRR,将该 IRQ 接口在 IRR 中对应的 bit 置 1。

  3. 在某个恰当实际,PR 会从 IRR 中挑选一个优先级最高的中断(IRQ 借口好越低,优先级越高),然后 8259A 在控制电路中通过 INT 接口向 CPU 发送 INTR 信号。

  4. CPU 通过自己的 INTA 接口向 8259A 的 INTA 接口回复一个中断响应信号。

  5. 8259A 接收到 CPU 的信号后,立即将刚才选出来的中断在 ISR 中对应的 bit 置 1,同时将该中断从 IRR 中去掉,即将对应 bit 位置 0。

  6. CPU 再次发送 INTA 信号给 8259A,意为获取中断向量号。

  7. 8259A 用起始中断向量号+IRQ接口号作为该设备的中断向量号通过系统数据总线发送给 CPU。

  8. CPU 从数据总线拿到中断向量号后,去 IVT 或 IDT 中找到响应的中断处理程序并执行。

  9. 若 8259A 的“EOI(End Of Interrupt)通知”被设置为自动模式,则在接收到 CPU 第二个 INTA 信号后(即步骤 6),8259A 会自动将此中断在 ISR 中对应 bit 位置 0;若 EOI 为半自动模式(手动模式),则中断处理程序结束处必须有向 8259A 发送 EOI 的代码,8259A 接收到 EOI 后,将 ISR 对应 bit 位置 0。

注意,进入 ISR 后的中断,在未被送到 CPU 前,还有可能被新产生的优先级更高的中断换下来。

7.5.2 8259A 的编程

中断向量号是逻辑上的东西,它在物理上是 8259A 上的接口号。8259A 上 IRQ 号的排列顺序是固定的,但其对应的中断向量号是不固定的,这其实是一种由硬件到软件的映射,通过设置 8259A 可以将 IRQ 接口映射道不同的中断向量号。

8259A 内部有两组寄存器:

  • 一组是初始化命令寄存器组,用来保存初始化命令字(Initalization Command Words,ICW),ICW 共 4 个,ICW1~ICW4.

  • 另一组是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW 共 3 个,OCW1~OCW3

所以对 8259A 的编程也分为初始化和操作两部分:

  1. 一部分用 ICW 做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。这部分必须依次写入 ICW1~ICW4。

  2. 另一部分是用 OCW 来操作控制 8259A,通过往 8259A 端口发送 OCW 实现中断屏蔽和中断结束,OCW 发送顺序不固定。

ICW1

ICW1 用来初始化 8259A 的连接方式和中断信号的触发方式。

连接方式是指用单片工作还是用多片级联工作。

触发方式是指中断请求信号是电平触发,还是边沿触发。

ICW1 需要写入到主片的 0x20 端口和从片的 0xA0 端口,如图 7-13 所示。

  • IC4 表示是否要写入 ICW4。为 1 表示需要写入,0 为不需要。x86 系统 IC4 必须为 1。

  • SNGL表示 single。若为 1 则为单片模式,为 0 表示级联模式。

  • ADI表示 call address interval,用来设置 8085 的调用时间间隔,x86 不用设置。

  • LTIM表示 level/edge triggered mode,用来设置中断检测方式。为 0 表示边沿触发,为 1 表示电平触发。

  • 第 4 位的 1 是固定值,这是 ICW1 的标记。

  • 第 5~7 位专用于 8085 处理器,x86 不需要,置 0 即可。

ICW2

ICW2 用来设置起始中断向量号,就是前面所说的硬件 IRQ 接口到逻辑中断向量号的映射。

ICW2 需要写入到主片的 0x21 端口和从片的 0xA1 端口。

由于咱们只需要设置 IRQ0 的中断向量号, IRQl ~ IRQ7 的中断向量号是 IRQ0 的顺延,所以,咱们只

负责填写高 5 位 T3 ~ T7,ID0~ID2 这低 3 位会根据 8 个 IRQ 接口的排列位次自行导入。

ICW3

ICW3 仅在级联的方式下才需要(如果 ICWl 中的 SNGL 为 0),用来设置主片和从片用哪个 IRQ 接口互连。

ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。

由于主片和从片的级联方式不一样,对于这个 ICW3,主片和从片都有自己不同的结构。

对于主片来说,接从片的 IRQ 接口对应那一位置 1,接外部设备的置 0。

对于从片来说,在中断响应时,主片会发送与从片做级联的 IRQ 接口号,所有从片用自己的 ICW3 的低 3 位和它对比,若一致则认为是发给自己的,故从片只用低 3 位 ID0~ID2 即可,其他位置 0。

ICW4

ICW4 用于设置 8259A 的工作模式,当 ICW1 中的 IC4 为 1 时才需要 ICW4。

ICW4 需要写入主片的 0x21 端口及从片的 0xA1 端口。

ICW4 中有些低位基于高位,故从高位开始介绍:

  • 第 5~7 位未定义,置 0 即可。

  • SFNM表示特殊全嵌套模式(Special Fully Nested Mode),若 SFNM 为 0,表示全嵌套模式,若为 1 表示特殊全嵌套模式。

  • BUF表示本芯片是否工作在缓冲模式。为 1 表示工作在缓冲模式,为 0 则为非缓冲模式。

  • M/S用来规定在缓冲模式下,本 8259A 是主片还是从片,为 1 是主片,0 是从片。若在非缓冲模式下则无效。

  • AEOI表示自动结束中断(Auto End Of Interrupt)。为 1 表示自动结束中断,为 0 表示手动结束中断,需在中断处理程序中或主函数中手动向 8259A 的主、从片发送 EOI 信号。

  • μPM表示微处理器类型,此项是为了兼容老处理器。为 0 表示 8080 或 8085 处理器,为 1 表示 x86 处理器。

OCW1

OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,实际上就是把 OCW1 写入了 IMR 寄存器。

OCW1 要写入主片的 0x21 或从片的 0xA1 端口。

  • M0~M7 对应 IRQ0~IRQ7,某位为 1,对应端口的中断信号就被屏蔽,为 0 则放行。

OCW2

OCW2 用来设置中断结束方式和优先级模式。OCW2 的配置比较复杂,各种属性位要配合在一起,组合出 8259A 的各种工作模式。

OCW2 要写入主片的 0x20 或从片的 0xA0 端口。

  • R,即 Rotation,表示是否按照循环方式设置中断优先级。为 1 表示优先级循环,为 0 表示固定优先级方式。

  • SL,即 Specific Level,表示是否指定优先等级。为 1 表示低 3 位有效,为 0 表示无效。

  • EOI,即 End Of Interrupt,为中断结束命令位。为 1 会令 ISR 寄存器中断相应位清 0。为 0 则需手动结束中断(如果中断来自主片,只需要向主片发送 EOI,若中断来自从片,除了向从片发送 EOI,还要向主片发送 EOI)。

  • 第 3~4 位是 OCW2 的标识。

  • L0~L2 用来确定优先级的编码。

具体有以下几种工作方式:

OCW3

OCW3 用来设定特殊屏蔽方式及查询方式。

OCW3 要写入主片的 0x20 端口或从片的 0xA0 端口。

  • RIS,即 Read Interrupt register Select,读取中断寄存器选择位。为 1 表示读取 ISR 寄存器,为 0 表示读取 IRR 寄存器。

  • RR,即 Read Register,读取寄存器命令。它和 RIS 位是配合在一起使用的。RR 为 1 时才可以读取寄存器。

  • P,即 Poll command,查询命令。当 P 为 1 时,设置 8259A 为中断查询方式,这样就可以通过读取寄存器,如 IRS,来查看当前的中断处理情况。

  • 第 3~4 位是 OCW3 的标识,8259A 通过这两位判断是哪个控制字。

  • ESMM(Enable Special Mask)和SMM(Special Mask Mode)是组合一起用的,用来启用或禁用特殊屏蔽模式。ESMM是特殊屏蔽模式允许位,SMM是特殊屏蔽模式位。只有ESMM和SMM都为 1,才表示工作在特殊屏蔽模式下。

  • 第 7 位未用到。

ICW1 和 OCW2, OCW3 是用偶地址端口 0x20 (主片〉或 0xA0 (从片)写入,利用控制字标识识别。

ICW2 ~ ICW4 和 OCW1 是用奇地址端口 0x21 (主片)或 0x21 (从片)写入,由于 OCW 是在初始化后有效,故在初始化后写入奇地址端口的数据便被认为是 OCW1。

7.6 编写中断处理程序

7.6.1 从最简单的中断处理程序开始

Intel 8259A 芯片位于主板上的南桥芯片中,不需要单独安装。

程序流程如图所示:

init_all():初始化所有的设备及数据结构

  • idt_init():初始化中断相关内容

    • pic_init():初始化 8259A(PIC 是 Programmable Interrupt Controller 的简称)
    • idt_desc_init():初始化 IDT
  • 加载 IDT

用汇编实现中断处理程序

汇编中用%macro,%endmacro来定义多行宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[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
global intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号,所以1个中断类型1个处理程序,自己知道自己的中断号是多少
%2
push intr_str
call put_str
add esp, 4

;如果从片上进入中断,除了往片上发送 EOI 外,还要往主片上发送 EOI,因为后面要在 8259A 芯片上设置手动结束中断,所以这里手动发送 EOI
mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;往从片发送
out 0x20, al ;往主片发送

add esp, 4
iret

section .data ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了
dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组
%endmacro

VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ERROR_CODE
VECTOR 0x09, ZERO
VECTOR 0x0a, ERROR_CODE
VECTOR 0x0b, ERROR_CODE
VECTOR 0x0c, ZERO
VECTOR 0x0d, ERROR_CODE
VECTOR 0x0e, ERROR_CODE
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ERROR_CODE
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ERROR_CODE
VECTOR 0x19, ZERO
VECTOR 0x1a, ERROR_CODE
VECTOR 0x1b, ERROR_CODE
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODE
VECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO

代码定义了 33 个中断处理程序,每个中断处理程序都一样,都是打印字符串“interrupt occur!”,之后退出中断。由于中断向量 0~19 为处理器内部固定的异常类型,20~31 是 Intel 保留的,所以咱们可用的中断向量号最低是 32,将来设置 8259A 时会把 IR0 的中断向量号设置为 32。

错误码是由 CPU 压入的,如果没有错误码,咱们手动压入一个 32 位数,这样无论中断是否会压入错误码,栈顶指针都是一样的。

编译器会将属性相同的 section 合并到同一个大的 segment 中,而且在 kemel.S 中对所有的数据 section 都用了同一个名字.data,所以编译后所有中断处理程序的地址作为数组元素地址会是连续的。

创建中断描述符表 IDT,安装中断处理程序

这里提示一下中断门结构:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "print.h"
/*略*/

#define IDT_DESC_CNT 0x21 //目前总共支持的中断数量

/**
* 中断门描述符结构体
*/
struct gate_desc{
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项位双字计数字段,是门描述符第四字节,是固定值
uint8_t attribute;
uint16_t func_offset_high_word;
};

extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用在 kernel.S 中的中断处理函数入口数组
static struct gate_desc idt[IDT_DESC_CNT]; //idt 本质上就是个中断门描述符数组

/*略*/

/**
* @description 创建中断门描述符
* @param p_gdesc 中断描述符
* @param attr 属性
* @param function 中断处理函数地址
*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr, intr_handler function){
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE; //global.h里定义的
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}

/**
* @description 初始化中断描述符表
*/
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]); //IDT_DESC_DPL0在global.h定义的
}
put_str(" idt_desc_init done\n");
}

/**
* @description 完成有关中断的所有初始化工作
*/
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)));
asm volatile("lidt %0"::"m"(idt_operand));
put_str("idt_init done\n");
}
1
2
3
4
5
6
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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位的门,不用,定义它只为和32位门区分
#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 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**************	 机器模式   ***************
b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。
w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。
HImode
“Half-Integer”模式,表示一个两字节的整数。
QImode
“Quarter-Integer”模式,表示一个一字节的整数。
*******************************************/

#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"

/* 向端口port写入一个字节*/
static inline void outb(uint16_t port, uint8_t data) {
/*********************************************************
a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号,
%b0表示对应al,%w1表示对应dx */
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));
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

因为 io.h 里的函数不是普通的函数,它们都是对底层硬件端口直接操作的,通常由设备的驱动程序来调用,为了快速响应,函数调用上需要更加高效。为了让处理器更多地为用户程序服务,操作系统(包括硬件驱动程序)必须减少自己占用处理器的时间,所以对硬件端口的操作要求——快。

io.h 定义了 4 个函数:

  1. 一次写入 1 个字节的 outb 函数。

  2. 一次写入多个字的 outsw 函数,以 2 字节为单位。

  3. 一次读入 1 个字节的 inb 函数。

  4. 一次读入多个字的 insw 函数,以 2 字节为单位。

介绍一下汇编指令insw,该指令的功能是从 dx 寄存器指定的端口处读入的 16 位数据写入 es:edi 指向的内存。所以咱们需要把 addr 的值写入 edi 寄存器,把端口 port 写入 dx。insw 一次只能传输两字节数据,要传输多字节数据就要用到重复指令cld,这里用 cx 寄存器来控制循环次数,故将 word_cnt 的值写入 cx 寄存器。insw 每执行完一次 ,edi 自动加 2,cx 自动减 1,故 edi 和 cx 的约束需要可读可写,故用“+”。

设置 8259A

8259A 的编程就是写入 ICW 和 OCW。

  • ICW1 和 OCW2, OCW3 是用偶地址端口 0x20 (主片〉或 0xA0 (从片)写入。

  • ICW2 ~ ICW4 和 OCW1 是用奇地址端口 0x21 (主片)或 0xA1 (从片)写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//第一个“略”处
#include "io.h"

#define PIC_M_CTRL 0x20 //主片
#define PIC_M_DATA 0x21
#define PIC_S_CTRL 0xA0 //从片
#define PIC_S_DATA 0xA1

//第二个“略”处
/* 初始化可编程中断控制器 8259A */
static void pic_init(void){
//初始化主片
outb(PIC_M_CTRL, 0x11); //ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4
outb(PIC_M_DATA, 0x20); //ICW2: 0010 0000 ,起始中断向量号为 0x20(0x20-0x27)
outb(PIC_M_DATA, 0x04); //ICW3: 0000 0100 ,IR2 接从片
outb(PIC_M_DATA, 0x01); //ICW4: 0000 0001 ,8086 模式,正常EOI

//初始化从片
outb(PIC_S_CTRL, 0x11); //ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4
outb(PIC_S_DATA, 0x28); //ICW2: 0010 1000 ,起始中断向量号为 0x28(0x28-0x2f)
outb(PIC_S_DATA, 0x02); //ICW3: 0000 0010 ,设置连接到主片的 IR2 引脚
outb(PIC_S_DATA, 0x01); //ICW4: 0000 0001 ,8086 模式,正常EOI

//打开主片上的 IR0 也就是目前只接受时钟产生的中断
//eflags 里的 IF 位对所有外部中断有效,但不能屏蔽某个外设的中断了
outb (PIC_M_DATA, 0xfe); //OCW1: 1111 1110
outb (PIC_S_DATA, 0xff); //OCW1: 1111 1111

put_str(" pic init done\n");
}
加载 IDT,开启中断

这是开启中断的最后一个环节:把中断描述符表 IDT 的信息加载到 IDTR。

IDTR 的结构:低 16 位是 IDT 大小-1,高 32 位是 IDT 的线性基地址。

1
2
3
4
5
6
static struct gate_desc idt[IDT_DESC_CNT];   // idt是中断描述符表,本质上就是个中断门描述符数组
//...............................//
/*加载 idt*/
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)((uint32_t)idt << 16)));// 指针只能转换相同大小的精度,故只能先转换为32位数再转换为64位数
asm volatile("lidt %0"::"m"(idt_operand));
put_str("idt_init done\n");
启动初始化

在 kernel 下定义 init.c

1
2
3
4
5
6
7
8
9
#include "init.h"
#include "print.h"
#include "interrupt.h"

/* 负责初始化所有模块 */
void init_all(){
put_str("init_all\n");
idt_init(); // 初始化中断
}
1
2
3
4
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif //__KERNEL_INIT_H
1
2
3
4
5
6
7
8
9
#include "print.h"
#include "init.h"

void main() {
put_str("I am kernel\n");
init_all();
asm volatile("sti"); //开启中断
while (1);
}

这里为了让中断程序运行,需要打开中断才行,打开中断时用 sti 指令,它将 eflags 寄存器的 IF 位置 1,。

运行结果:

还可以使用info idt来查看 IDT 内容:

7.6.2 改进中断处理程序

本节将实现具体的中断处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
char* intr_name[IDT_DESC_CNT];		     // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; // 定义中断处理程序数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序

/**
* @description 通用的中断处理函数,一般用在异常出现时的处理
* @param vec_nr 中断向量号
*/
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}

/**
* @description 完成一般中断处理函数注册及异常名称注册
*/
static void exception_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为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] 第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";
}

general_intr_handler是默认的通用中断处理函数,若相关中断没有定义具体的中断处理函数时调用该函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[bits 32]
%define ERROR_CODE nop ;为了栈中格式统一,如果 CPU 在异常中已经自动压入错误码,这里不做操作
%define ZERO push 0 ;为了栈中格式统一,如果 CPU 在异常中没有自动压入错误码,这里填充 0

extern idt_table ;idt_table是C中注册的中断处理程序数组

section .data
global intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号,所以1个中断类型1个处理程序,自己知道自己的中断号是多少
%2 ; 中断若有错误码会压在eip后面
;保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

;如果从片上进入中断,除了往片上发送 EOI 外,还要往主片上发送 EOI,因为后面要在 8259A 芯片上设置手动结束中断,所以这里手动发送 EOI
mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;往从片发送
out 0x20, al ;往主片发送

push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ;调用idt_table中的C版本中断处理函数
jmp intr_exit

section .data ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了
dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组
%endmacro

section .text
global intr_exit
intr_exit:
;恢复上下文环境
add esp, 4 ;跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ;跳过错误码
iretd

VECTOR 0x00, ZERO
;略,同之前的代码
VECTOR 0x20, ZERO

代码解析:

  • 第 16~20 行是保护进程上下文。因为在此汇编文件中药调用 C 程序,一定会使当前寄存器环境破坏,所以要保护当前所使用的的寄存器环境。这里是要把 4 个段寄存器 ds、es、fs、gs 和 8 个通用寄存器保护起来。这 4 个寄存器虽然是 16 位,但在 32 位下入栈后要占 4 字节(位于其他寄存器和内存中的值若为 16 位,则入栈后只占 2 字节)。栈中情况如图所示:

  • 第 28 行调用 C 语言写的中断处理程序。

  • 第 40 行popad时不会将栈中 esp 的值赋给 esp 寄存器。

7.6.3 调试实战:处理器进入中断时压栈出栈完整过程

这里只记录调试指令:

  • print-stack查看栈;

  • show int有中断发生时,控制台打印中断信息;

  • s单步执行;

  • n单步执行(遇到函数跳过);

  • r查看通用寄存器;

  • sreg查看段寄存器;

7.7 可编程计数器/定时器 8253 简介

7.7.1 时钟——给设备打牌子

时钟信号表达的意思是设备自己的工作节拍、频率。计算机中的时钟大致可以分为两大类:内部时钟和外部时钟。

内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频后就是主板的外频。Intel 处理器将此外频乘以某个倍数之后便称为主频。

外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序,比如 IO 接口和处理器之间。

定时计数器是用来解决处理器与外部设备同步数据时的时序配合问题的,它分为不可编程定时器及可编程定时器两种。我们要接触的是可编程定时器 PIC,Programmable Interval Timer。常用的可编程定时计数器有 Intel 8253/8254/ 82C54A 等。

硬件定时器一般有两种计时方式:

  1. 正计时:每次时钟脉冲发生时,将当前计数器加 1,直到与设定目标终止值相等时提示时间已到。

  2. 倒计时:先设定号计数器的值,每发生一次时钟脉冲计数器减 1,直到为 0 时提示时间已到。

8253 是倒计时方式。

7.7.2 8253 入门

8253 内部有三个独立的计数器,分别是计数器 0~计数器 2,端口分别是 0x40 ~ 0x42。每个计数器都有自己的一套寄存器资源,工作时互相独立,互不干涉。

寄存器资源包括:

  • 16 位的计数初值寄存器,保存计数器初始值

  • 16 位减法计数器,从初值寄存器中取值后自减计数

  • 16 位输出锁存器,保存当前减法计数器的值,用于外界获取当前计数值

每个计数器有三个引脚:

  • CLK,表示时钟输入信号,每当此引脚收到一个时钟信号,减法计数器就减 1。

  • GATE,表示门控输入信号,用于控制计数器是否可以开始计数。

  • OUT,宝石计数器输出信号,定时完成后发出信号通知处理器或设备。

每个计数器都有自己的用途:

7.7.3 8253 控制字

控制字寄存器端口是0x43,它是 8 位大小的寄存器。

  • SC1和SC0位是选择计数位,即 Select Counter,或者选择通道位,即 Select Channel。

  • RW1和RW0是读/写/锁存操作位,用来设置待操作计数器(通道)的读写及锁存方式。

  • M2~M0这三位是工作方式选择位。

  • BCD,即 Binary-Coded Decimal,称为“二进制码的十进制数”,用来指示计算器的计算方式是 BCD 码还是二进制数。

    • 为 1 表示用 BCD 码计数,计数范围是 0~0x9999,表示十进制范围是 0~9999,0 值表示十进制 10000;
    • 为 0 表示用二进制计数,计数范围是 0~0xFFFF,表示十进制范围是 0~65535,0 值表示十进制 65536。

7.7.4 8253 工作方式

8253 有 6 种工作方式:

计数器开始计数有两个条件:

  1. GATE 为高电平,即 GATE 为 1,这是由硬件控制的;

  2. 计数初值已写入计数器中的减法计数器,这是由软件 out 指令控制的。

当这两个条件具备后,计数器将在下一个时钟信号 CLK 的下降沿开始计数。

按照“哪个未完成”来划分启动类型:

  • 软件启动:硬件条件已经完成,由软件条件来控制启动,工作方式 0/2/3/4

  • 硬件启动:软件条件已经完成,由硬件条件来控制启动,工作方式 1/5

停止类型:

  • 强制终止:将 GATE 信号置 0

  • 自动终止:单次计数完之后自动停止,工作方式 0/1/4/5

7.7.5 8253 初始化步骤

  1. 往控制字寄存器端口0x43中写入控制字

  2. 在所执行使用的计数器端口中写入计数初值

7.8 提高时钟中断频率

本节会对 8253 进行编程,使得时钟 1s 内发 100 次中断信号,即中断频率为 100Hz。

所以本节的工作:

  1. IRQ0 引脚上的时钟中断信号频率是由 8253 的计数器 0 设置的,故要使用 8253 的计数器 0;

  2. 时钟发出的中断信号必须是周期性发出,也就是要采用循环技术方式,这里选用方式 2;

  3. 计数器发出输出信号的频率是由计数初值决定的,要为计数器 0 赋予合适的初值。

8253 计数器的工作频率是 1.19318Mz,要发出 100 次中断信号,计数初值就需要设为 1193180/100≈11932。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "timer.h"
#include "io.h"
#include "print.h"

#define IRQ0_FREQUENCY 1
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

/**
* @description 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value
* @param counter_port 控制字寄存器端口
* @param counter_no 计数器号
* @param rwl 读写锁属性
* @param counter_mode 计数器模式
* @param counter_value 计数器初值
*/
static void frequency_set(uint8_t counter_port,
uint8_t counter_no,
uint8_t rwl,
uint8_t counter_mode,
uint16_t counter_value) {
// 往控制字寄存器端口0x43中写入控制字
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
// 先写入counter_value的低8位
outb(counter_port, (uint8_t) counter_value);
// 再写入counter_value的高8位
outb(counter_port, (uint8_t) counter_value >> 8);
}

/**
* @description 初始化PIT8253
*/
void timer_init() {
put_str("timer_init start\n");
// 设置8253的定时周期,也就是发中断的周期
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done\n");
}
1
2
3
4
5
6
7
8
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H

#include "stdint.h"

void timer_init(void);

#endif

然后将timer_init函数加到 kernel/init.c 中的init_all函数中:

1
2
3
4
5
6
7
8
9
10
11
#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();
}

编译生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建生成中间文件目录
mkdir -p build/lib/kernel/
mkdir -p build/kernel/
mkdir -p build/device/
# 编译汇编
nasm -f elf -o build/lib/kernel/print.o lib/kernel/print.S
nasm -f elf -o build/kernel/kernel.o kernel/kernel.S
# 编译C程序
gcc -m32 -I lib/kernel/ -fno-stack-protector -c -o build/device/timer.o device/timer.c
gcc -m32 -I lib/kernel/ -fno-stack-protector -c -o build/kernel/main.o kernel/main.c
gcc -m32 -I lib/kernel/ -I kernel/ -c -fno-builtin -fno-stack-protector -o build/kernel/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel/ -I kernel/ -c -fno-builtin -fno-stack-protector -o build/kernel/init.o kernel/init.c
# 链接目标文件
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin \
build/kernel/main.o build/kernel/init.o build/kernel/interrupt.o build/lib/kernel/print.o build/kernel/kernel.o build/device/timer.o
# 写入虚拟机磁盘
dd if=build/kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc

可能存在的问题:

相关文章
评论
分享
  • 《操作系统真象还原》:第十章 输入输出系统

    上一章中我们遇到的字符混乱和 GP 异常问题,根本原因是由于临界区代码的资源竞争,这需要一些互斥的方法来保证操作的原子性。 10.1 同步机制——锁 10.1.1 排查 GP 异常,理解原子操作 多线程执行刷屏时光标值越界导致 G...

    《操作系统真象还原》:第十章 输入输出系统
  • 《操作系统真象还原》:第九章 线程

    线程和进程将分两部分实现,本章先讲解线程。 9.1 实现内核线程 9.1.1 执行流 在处理器数量不变的情况下,多任务操作系统采用多道程序设计的方式,使处理器在所有任务之间来回切换,这称为“伪并行”,由操作系统中的任务调度器决定当前处...

    《操作系统真象还原》:第九章 线程
  • 《操作系统真象还原》:第八章 内存管理系统

    8.1 makefile 简介 这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html 8.1.1 makefile 是什么 makefile 是 Linux ...

    《操作系统真象还原》:第八章 内存管理系统
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。stdcall是被调用者清理参数所占的栈空间。举例来说: 12int subtract(int a, ...

    《操作系统真象还原》:第六章 完善内核
  • 《操作系统真象还原》:第五章 保护模式进阶——加载内核

    5.3 加载内核 5.3.1 用 C 语言写内核 第一个 C 语言代码: 1234int main(void) { while(1); return 0;} 这个内核文件什么都没做,通过while(1)这个死循环一...

    《操作系统真象还原》:第五章 保护模式进阶——加载内核
  • 《操作系统真象还原》:第五章 保护模式进阶——内存分页机制

    从这一刻起,我们才算开始了真正的操作系统学习之旅 5.1 获取物理内存容量 5.1.1 Linux 获取内存的方法 在 Linux 2.6 内核总是用detect_memory函数来获取内存容量的。其函数本质上是通过调用 BIOS...

    《操作系统真象还原》:第五章 保护模式进阶——内存分页机制
  • 《操作系统真象还原》:第四章 保护模式入门

    4.1 保护模式概述 在本章大家会见到全局描述符表、中断描述符表、各种门结构,这是 CPU 提供给应用的,咱们用好就行。 保护模式强调的是“保护”,它是在 Intel 80286 CPU 中首次出现,这是继 8086 之后,Intel...

    《操作系统真象还原》:第四章 保护模式入门
  • 《操作系统真象还原》:第三章 完善MBR——I/O接口

    3.3 让我们对显示器说点什么吧 3.3.1 CPU 如何与外设通信——IO 接口 IO 接口功能: 设置数据缓冲,解决 CPU 与外设的速度不匹配 设置信号电平转换电路 设置数据格式转换 设置时序控制电路来同步 CP...

    《操作系统真象还原》:第三章 完善MBR——I/O接口
  • 《操作系统真象还原》:第三章 完善MBR——CPU的实模式

    针对汇编 几个知识点: 第 1 行和第 4 行的 mov 操作,机器码第 1 个宇节都是B8,而另外第 2、3 行同样是 mov 指令,机器码却有天壤之别,似乎找不到共性。原因是机器码是由很多部分组成的,比如指令前缀、主操作码字...

    《操作系统真象还原》:第三章 完善MBR——CPU的实模式
  • 《操作系统真象还原》:第二章 编写 MBR

    先了解 CPU 的两种工作模式:实模式和保护模式 实模式(英语:Real mode)是 Intel 80286 和之后的 x86 兼容 CPU 的操作模式。实模式的特性是一个 20 比特的区段存储器地址空间(意思为只有 1MB 的存储...

    《操作系统真象还原》:第二章 编写 MBR