《操作系统真象还原》第七章——启用中断

中断分类

外部中断

如图所示,外部硬件的中断是通过两根信号线通知CPU的,这两根信号线就是INTR(INTeRrupt)和NMI(Non Maskable Interrupt)

可屏蔽中断

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

另外,这些设备都是接在某个中断代理设备的,通过该中断代理也可以单独屏蔽某个设备的中断。对于这类可屏蔽中断,CPU可以选择不用理会

不可屏蔽中断

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

因为该中断表示出的问题太大了,要宕机了,屏蔽不了,因为根本不能假装没看见,比如断电了,机器都没法运行了,所以,这里的不可屏蔽不是说“不可以、不建议屏蔽”,而是真地处理不了啦,所以eflags 寄存器中的 IF 位对其无效也是没办法的事,即使IF位对它有效又能怎么样呢?问题已经出了,中断只是通知 CPU而已,不是说通知收不到问题就没有发生,宕机是不可避免的。

中断向量号

CPU 收到中断后,得知道发生了什么事情才能执行相应的处理办法。

这是通过中断向量表或中断描述符表来实现的,首先为每一种中断分配一个中断向量号,中断向量号就是一个整数,它就是中断向量表或中断描述符表中的索引下标,用来索引中断项。

中断发起时,相应的中断向量号通过NMI或INTR引脚被传入CPU,中断向量号是中断向量表或中断描述符表里中断项的下标,CPU 根据此中断向量号在中断向量表或中断描述符表中检索对应的中断处理程序并去执行。

可屏蔽中断并不会导致致命问题,它的数量是有限的,所以每一种中断源都可以获得一个中断向量号。

不可屏蔽中断引起的致命错误原因有很多,每一种都是硬伤,出现了基本上可以认为用软件解决不了,多数属于物理上的问题,只能找硬件工程师解决了。所以,既然用软件解决不了,而且每种原因对于软件工程师来说都意义不大,就没必要再细分原因,统统为导致宕机的各种原因分配一个中断向量号就足够了,所以不可屏蔽中断的中断向量号为2

内部中断

软中断

软中断,就是由软件主动发起的中断,因为它来自于软件,所以称之为软中断。由于该中断是软件运行中主动发起的,所以它是主观上的,并不是客观上的某种内部错误

可以发起软中断的指令

  • int 8位立即数,我们要通过它进行系统调用,8位立即数可表示 256种中断
  • int3
  • into
  • bound
  • ud2

异常

异常是另一种内部中断,是指令执行期间CPU内部产生的错误引起的,由于是运行时错误,所以它不受标志寄存器eags 中的 位影响,无法向用户隐瞒

对于中断是否无视 eflags 中的IF 位,可以这么理解:

(1)首先,只要是导致运行错误的中断类型都会无视IF 位,不受正位的管束,如 NMI、异常

(2)其次,由于int n型的软中断用于实现系统调用功能,不能因为正位为0就不顾用户请求,所以为了用户功能正常,软中断必须也无视IF位。

总结:只要中断关系到“正常”运行,就不受IF位影响。
另外,这里所说的运行错误,是说指令语法方面的错误。

举个例子,比如说在执行 DIV 和 DDIV 除法指令时,处理器发现分母为0(除 0,通常是程序忘记为分母赋值或传给分母的参数有误导致的),除法中分母是不能为0的,这不符合除法要求,将引发0号异
常(叫中断也行)。

表中第一列的 Vector No.,即中断向量号。它就是个整数,范围是0~255。 

异常和不可屏蔽中断的中断向量号是由CPU自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的(咱们这里的中断代理是8259A),软中断是由软件提供的。

中断机制的本质是来了一个中断信号后,调用相应的中断处理程序。所以,CPU 不管有多少种类型的中断,为了统一中断管理,把来自外部设备、内部指令的各种中断类型统统归结为一种管理方式,即为每个中断信号分配一个整数,用此整数作为中断的D,而这个整数就是所谓的中断向量,然后用此DD作为中断描述符表中的索引,这样就能找到对应的表项,进而从中找到对应的中断处理程序。

 中断描述符

以前说过的低端1MB内存布局,位于地址0~0x3f的是中断向量表IVT,它是实模式下用于存储中断处理程序入口的表。

由于实模式下功能有限,运行机制比较“死板”,所以它的位置是固定的,必须位于最低端。已知0~0x3f共 1024个字节,又知 IVT 可容纳 256个中断向量,所以每个中断向量用4字节描述。

对比中断向量表,中断描述符表有两个区别。
(1)中断描述符表地址不限制,在哪里都可以。
(2)中断描述符表中的每个描述符用8字节描述

中断描述符表寄存器IDTR

在 CPU内部有个中断描述符表寄存器(Intenupt Descriptor TableRegister,IDTR),该寄存器分为两部分:第 0~15 位是表界限,即 IDT 大小减 1,第16~47位是 IDT的基地址

中断描述符表地址要加载到这个寄存器中,只有寄存器 DTR指向了 IDT,当 CPU接收到中断向量号时才能找到中断向量处理程序,这样中断系统才能正常运作。IDTR结构如图所示。

16位的表界限,表示最大范围是0x,即64KB。可容纳的描述符个数是64KB/8=8K=8192个。

特别注意的是GDT中的第0个段描述符是不可用的,但IDT却无此限制,第0个门描述符也是可用的,中断向量号为0的中断是除法错。但处理器只支持256个中断,即0~254,中断描述符中其余的描述符不可用。在门描述符中有个P位,所以,将来在构建IDT时,要把P位置0,这样就表示门描述符中的中断处理程序不在内存中。

同加载 GDTR 一样,加载 IDTR 也有个专门的指令--1idt,其用法是:lidt 48 位内存数据
在这 48位内存数据中,前16位是IDT表界限,后32位是IDT线性基地址。

中断处理流程

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

  • CPU 外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到CPU
  • CPU内:CPU执行该中断向量号对应的中断处理程序

cpu外部的中断处理

cpu外部的中断处理,是由中断代理完成的,其功能主要是接收外部设备的中断信号,并进行处理后输出中断向量号,再将此中断向量号输送给cpu,交由cpu继续向下执行

中断代理有很多种,我们采用的是较流行的中断代理:Imntel8259A芯片,也就是可编程中断控制器(PIC)8259A。

8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。而它称为可编程的原因,就是可以通过编程的方式来设置以上的功能

  • INT:8259A选出优先级最高的中断请求后,发信号通知CPU。
  • INTA:INT Acknowledge,中断响应信号。位于8259A中的INTA接收来自CPU的INTA 接口的中断响应信号。
  • IMR:Interrupt Mask Register,中断屏蔽寄存器,宽度是8位,用来屏蔽某个外设的中断。
  • IRR:Interrupt Request Register,中断请求寄存器:,宽度是8位。它的作用是接受经过 IMR寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断,“相当于”5259A维护的未处理中断信号队列。
  • PR:Priority Resolver,优先级仲裁器。当有多个中断同时发生,或当有新的中断请求进来时,将它与当前正在处理的中断进行比较,找出优先级更高的中断。
  • ISR:In-Service Register,中断服务寄存器,宽度是8位。当某个中断正在被处理时,保存在此寄存器中

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

cpu内部的中断处理

  • 处理器根据中断向量号定位中断门描述符

中断向量号是中断描述符的索引,当处理器收到一个外部中断向量号后,它用此向量号在中断描述符表中查询对应的中断描述符,然后再去执行该中断描述符中的中断处理程序。

由于中断描述符是8个字节,所以处理器用中断向量号乘以8后,再与IDTR中的中断描述符表地址相加,所求的地址之和便是该中断向量号对应的中断描述符。

  • 处理器进行特权级检查
  • 执行中断处理程序

特权级检查通过后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到 EIP,开始执行中断处理程序

以上过程如下所示

代码处理思路

  • 初始化中断代理,即8259A芯片
  • 按照中断描述符的格式构造中断描述符
  • 构造IDT(中断描述符表)
  • 加载IDT,开中断(设置中断屏蔽寄存器IMR,只放行主片上IRO的时钟中断,屏蔽其他外部设备的中断。)

开中断是用 sti 指令,它将标志寄存器efags中的F位置1,这样来自中断代理 8259A的中断信号便被处理器受理了。

外部设备都是接在8259A的引脚上,由于我们在8259A中已经通过IMR寄存器将除时钟之外的所有外部设备中断都屏蔽了,这样开启中断后,处理器只会收到源源不断地时钟中断。

代码文件布局

代码调用结构及思路说明

  • pic_init:初始化可编程中断控制器8259A
  • idt_desc_init:构建中断描述符表
  • idt_init
    •  利用idt_desc_init构建中断描述符表
    • 利用pic_init初始化可编程中断控制器8259A
    • 加载idt
  • init_all:完成上述所有初始化操作后开中断

本章代码

kernel/kernel.S

[bits 32]
%define ERROR_CODE nop
%define ZERO push 0
extern put_str                          ;使用外部函数(/kernel/print.S中的字符串打印函数)

section .data                           ;定义数据段
intr_str db "interrupt occur!",0xa,0    ;声明intr_str字符串数组(指针类型),0xa表示换行符,0表示字符串的末尾标记符

global intr_entry_table                 ;定义中断处理程序数据,数组的元素值是中断处理程序,共33个
intr_entry_table:

;宏定义的语法格式为:
;       %macro 宏名 参数个数
;           宏定义
;       %endmacro
%macro VECTOR 2                         ;定义VECTOR宏,该宏用于定义中断处理程序
section .text                           ;中断处理程序的代码段
intr%1entry:                            ;这是一个标号,用于表示中断程序的入口地址,即中断程序的函数名
    %2                                  ;压入中断错误码(如果有)
    push intr_str                       ;压入put_str的调用参数
    call put_str
    add esp,4                           ;回收put_str函数压入的字符串参数的栈帧空间(cdecl调用约定)
                                        ;由于intr_str是一个指针类型,而该系统为32bit,因此该参数占用4个字节空间
    
    mov al,0x20                         ;中断结束命令EOI
    out 0xa0,al                         ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
    out 0x20,al                         ;向从片发送OCW2,其中EOI位为1,告知结束中断

    add esp,4                           ;抛弃有错误码的中断(如果有)
    iret                                ;中断返回
    
section .data
    dd intr%1entry                      ;存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacro

VECTOR 0x00,ZERO                         ;调用之前写好的宏来批量生成中断处理函数,传入参数是中断号码与上面中断宏的%2步骤,这个步骤是什么都不做,还是压入0看p303
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

kernel/interrupt.h

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;		//将intr_handler定义为void*同类型
void idt_init(void);
#endif

kernel/interrupt.c

#include "interrupt.h"      //里面定义了intr_handler类型
#include "stdint.h"         //各种uint_t类型
#include "global.h"         //里面定义了选择子
#include "io.h"             
#include "print.h"

#define PIC_M_CTRL 0x20	       // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21	       // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0	       // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1	       // 从片的数据端口是0xa1

#define IDT_DESC_CNT 0x21	   //支持的中断描述符个数33

//定义中断描述符结构体
struct gate_desc {
   uint16_t    func_offset_low_word;        //函数地址低字
   uint16_t    selector;                    //选择子字段
   uint8_t     dcount;                      //此项为双字计数字段,是门描述符中的第4字节。这个字段无用
   uint8_t     attribute;                   //属性字段
   uint16_t    func_offset_high_word;       //函数地址高字
};

//定义中断门描述符结构体数组,形成中断描述符表idt,该数组中的元素是中断描述符
static struct gate_desc idt[IDT_DESC_CNT];
//静态函数声明,该函数用于构建中断描述符,将上述定义的中断描述符结构体按照字段说明进行填充即可
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);

//intr_handler为void*类型,intr_entry_table为中断处理程序数组,其内的元素是中断处理程序入口地址
extern intr_handler intr_entry_table[IDT_DESC_CNT];

/*初始化可编程中断控制器8259A*/
static void pic_init()
{
    /* 初始化主片 */
   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 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;
   p_gdesc->dcount = 0;
   p_gdesc->attribute = attr;
   p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}


/*
函数功能:中断描述符表idt
函数实现:循环调用make_idt_desc构建中断描述符,形成中断描述符表idt
参数:中断描述符表中的某个中断描述符地址,属性字段,中断处理函数地址
*/
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");
}


/*完成有关中断的所有初始化工作*/
void idt_init() {
   put_str("idt_init start\n");
   idt_desc_init();	   //完成中段描述符表的构建
   pic_init();		  //设定化中断控制器,只接受来自时钟中断的信号

   /* 加载idt */
   uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));    //定义要加载到IDTR寄存器中的值
   asm volatile("lidt %0" : : "m" (idt_operand));
   put_str("idt_init done\n");
}


kernel/global.h

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

//选择子的RPL字段
#define	 RPL0  0
#define	 RPL1  1
#define	 RPL2  2
#define	 RPL3  3

//选择子的TI字段
#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)

定义模块化的中断门描述符attr字段,attr字段指的是中断门描述符高字第8到16bit
#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)  //DPL为0的中断门描述符attr字段
#define	 IDT_DESC_ATTR_DPL3  ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)  //DPL为3的中断门描述符attr字段

#endif

kernel/init.h

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H

void init_all(void);

#endif

kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"

/*初始化所有模块*/
void init_all()
{
    put_str("init_all\n");
    idt_init();//初始化中断
}

lib/kernel/io.h

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

 //一次送一字节的数据到指定端口,static指定只在本.h内有效,inline是让处理器将函数编译成内嵌的方式,就是在该函数调用处原封不动地展开
 //此函数有两个参数,一个端口号,一个要送往端口的数据
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));    
}

//利用outsw(端口输出串,一次一字)指令,将ds:esi指向的addr处起始的word_cnt(存在ecx中)个字写入端口port,ecx与esi会自动变化
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));
}                                       //S表示寄存器esi/si

/* 将从端口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");
}                                   //D表示寄存器edi/di                       //通知编译器,内存已经被改变了

#endif

lib/kernel/print.h

#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"                 //我们的stdint.h中定义了数据类型,包含进来
void put_char(uint8_t char_asci);   //在stdint.h中uint8_t得到了定义,就是unsigned char
void put_str(char* message);
#endif

lib/kernel/print.S

TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0;定义显存段选择子

[bits 32]
section .text
global put_str

put_str:
    push ebx
    push ecx
    xor ecx,ecx
    mov ebx,[esp+12]                ;ebx存放字符串首地址
.goon:
    mov cl,[ebx]                    ;取出字符串的第一个字符,存放到cx寄存器中
    cmp cl,0                        ;判断是否到了字符串结尾,字符串结尾标志符'\0'即0
    jz .str_over
    push ecx                        ;压入put_char参数,调用put_char函数
    call put_char
    add esp,4                       ;回收栈空间
    inc ebx                         ;指向字符串的下一个字符
    jmp .goon
.str_over:
    pop ecx                         ;回收栈空间
    pop ebx                         ;回收栈空间
    ret


[bits 32]
section .text
global put_char                     ;通过global关键字将put_char函数定义为全局符号
                                    ;使其对外部文件可见
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
put_char:
    pushad                          ;push all double,压入所有双字长的寄存器
                                    ;入栈顺序为eax->ecx->edx->ebx->esp->ebp->esi->edi

    mov ax,SELECTOR_VIDEO
    mov gs,ax                       ;为gs寄存器赋予显存段的选择子

;以下代码用于获取光标的坐标位置,光标的坐标位置存放在光标坐标寄存器中
;其中索引为0eh的寄存器和索引为0fh的寄存器分别存放光标高8位和低8位
;访问CRT controller寄存器组的寄存器,需要先往端口地址为0x03d4的寄存器写入索引
;从端口地址为0x03d5的数据寄存器中读写数据
    mov dx,0x03d4                   ;将0x03d4的端口写入dx寄存器中
    mov al,0x0e                     ;将需要的索引值写入al寄存器中
    out dx,al                       ;向0x03d4端口写入0x0e索引
    mov dx,0x03d5                   
    in al,dx                        ;从0x03d5端口处获取光标高8位
    mov ah,al                       ;ax寄存器用于存放光标坐标,
                                    ;因此将光标坐标的高8位数据存放到ah中

;同上,以下代码获取光标坐标的低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx                        ;此时ax中就存放着读取到的光标坐标值
    mov bx,ax                       ;bx寄存器不仅是光标坐标值,同时也是下一个可打印字符的位置
                                    ;而我们习惯于bx作为基址寄存器,以后的处理都要基于bx寄存器
                                    ;因此才将获取到的光标坐标值赋值为bx
                                    

    mov ecx,[esp+36]                ;前边已经压入了8个双字(2个字节)的寄存器,
                                    ;加上put_char函数的4字节返回地址
                                    ;所以待打印的字符在栈顶偏移36字节的位置

    cmp cl,0xd                      ;回车符处理
    jz .is_carriage_return

    cmp cl,0xa                      ;换行符处理
    jz .is_line_feed

    cmp cl,0x8                      ;退格键处理
    jz .is_backspace                
    jmp .put_other                  ;正常字符处理

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;退格键处理
    ;处理思路:
        ;1.将光标位置减一
        ;2.将待删除的字符使用空格字符(ASCII:0x20)代替
.is_backspace:
    dec bx                          ;bx中存储光标的坐标位置,将光标坐标位置减去一,即模拟退格
    shl bx,1                        ;由于文本模式下一个字符占用两个字节(第一个字节表示字符的ASCII码,第二个字节表示字符的属性),
                                    ;故光标位置乘以2就是光标处字符的第一个字节的偏移量
    mov byte[gs:bx],0x20            ;将空格键存入待删除字符处
    inc bx                          ;此时bx中存储的是字待删除字符的第一个字节位置,
                                    ;使用inc指令将bx加1后就是该字符的第二个字节的位置
    mov byte[gs:bx],0x07            ;将黑底白字(0x07)属性加入到该字符处
    shr bx,1                        ;bx除以2,恢复光标坐标位置
    jmp .set_cursor                 ;去设置光标位置, 这样光标位置才能真正在视觉上更新

;将cx指向的字符放入到光标处
.put_other:
    shl bx,1                        ;将光标坐标转换为内存偏移量
    mov byte[gs:bx],cl              ;将cx指向的字符放入到光标处
    inc bx                          ;bx指向当前字符的下一个字节处,存放当前字符属性
    mov byte[gs:bx],0x07            ;存放字符属性
    shr bx,1                        ;将内存偏移量恢复为光标坐标值
    inc bx                          ;bx指向下一个待写入字符位置
    cmp bx,2000                     ;80*25=2000,判断是否字符已经写满屏了
    jl .set_cursor                  ;更新光标坐标值


;换行处理
    ;思路:首先将光标移动到本行行首,之后再将光标移动到下一行行首
.is_line_feed:
.is_carriage_return:
    xor dx,dx
    ;将光标移动到本行行首
    mov ax,bx
    mov si,80
    div si                          ;除法操作,ax/si,结果ax存储商,dx存储余数
    sub bx,dx

    .is_carriage_return_end:
        add bx,80                   ;将光标移动到下一行行首
        cmp bx,2000
    .is_line_feed_end:
        jl .set_cursor

;滚屏处理
    ;思路:屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen:
    cld                             ;清除方向标志位,字符串的内存移动地址从低地址向高地址移动
                                    ;若方向标志位被设置,则字符串的内存移动地址从高地址向低地址移动
    mov ecx,960                     ;共移动2000-80=192个字符,每个字符占2个字节,故共需移动1920*2=3840个字节
                                    ;movsd指令每次移动4个字节,故共需执行该指令3840/4=960次数
    mov esi,0xb80a0                 ;第一行行首地址,要复制的起始地址
    mov edi,0xb8000                 ;第0行行首地址,要复制的目的地址
    rep movsd                       ;rep(repeat)指令,重复执行movsd指令,执行的次数在ecx寄存器中

    ;将最后一行填充为空白
    mov ebx,3840
    mov ecx,80
    .cls:
        mov word[gs:ebx],0x0720
        add ebx,2
        loop .cls
        mov bx,1920                 ;将光标值重置为1920,最后一行的首字符.


.set_cursor:
    					                                    ;将光标设为bx值
    ;;;;;;; 1 先设置高8位 ;;;;;;;;
    mov dx, 0x03d4			                                ;索引寄存器
    mov al, 0x0e				                            ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5			                                ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bh
    out dx, al

    ;;;;;;; 2 再设置低8位 ;;;;;;;;;
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    mov al, bl
    out dx, al

.put_char_done:
    popad
    ret

lib/stdint.h

#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif

编译命令

#编译mbr
nasm ./boot/mbr.S -o ./build/mbr -I ./boot/include/ 
dd if=./build/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm ./boot/loader.S -o ./build/loader -I ./boot/include/
dd if=./build/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

#编译print函数
nasm ./lib/kernel/print.S -f elf -o ./build/print.o 

# 编译kernel
nasm ./kernel/kernel.S -f elf -o ./build/kernel.o 

#sudo apt-get install libc6-dev-i386

#编译main函数
gcc-4.4 ./kernel/main.c -o build/main.o -c  -fno-builtin -m32 -I ./lib/kernel/ -I ./lib/ -I ./kernel/

# 编译interrupt
gcc-4.4 ./kernel/interrupt.c -o ./build/interrupt.o -c -fno-builtin -m32 -I ./lib/kernel/ -I ./lib/ -I ./kernel/ 

# 编译init
gcc-4.4 ./kernel/init.c -o ./build/init.o -c -fno-builtin -m32 -I ./lib/kernel/ -I ./lib/ -I ./kernel/ 

#链接成内核
ld build/main.o  build/init.o build/interrupt.o build/kernel.o build/print.o -o build/kernel.bin  -m elf_i386 -Ttext 0xc0001500 -e main 

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=./build/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf build/*

运行

./bin/bochs -f boot.disk

查看中断描述符

info idt
Interrupt Descriptor Table (base=0xc0002c40, limit=263):
IDT[0x00]=32-Bit Interrupt Gate target=0x0008:0xc0001720, DPL=0
IDT[0x01]=32-Bit Interrupt Gate target=0x0008:0xc0001739, DPL=0
IDT[0x02]=32-Bit Interrupt Gate target=0x0008:0xc0001752, DPL=0
IDT[0x03]=32-Bit Interrupt Gate target=0x0008:0xc000176b, DPL=0
IDT[0x04]=32-Bit Interrupt Gate target=0x0008:0xc0001784, DPL=0
IDT[0x05]=32-Bit Interrupt Gate target=0x0008:0xc000179d, DPL=0
IDT[0x06]=32-Bit Interrupt Gate target=0x0008:0xc00017b6, DPL=0
IDT[0x07]=32-Bit Interrupt Gate target=0x0008:0xc00017cf, DPL=0
IDT[0x08]=32-Bit Interrupt Gate target=0x0008:0xc00017e8, DPL=0
IDT[0x09]=32-Bit Interrupt Gate target=0x0008:0xc0001800, DPL=0
IDT[0x0a]=32-Bit Interrupt Gate target=0x0008:0xc0001819, DPL=0
IDT[0x0b]=32-Bit Interrupt Gate target=0x0008:0xc0001831, DPL=0
IDT[0x0c]=32-Bit Interrupt Gate target=0x0008:0xc0001849, DPL=0
IDT[0x0d]=32-Bit Interrupt Gate target=0x0008:0xc0001862, DPL=0
IDT[0x0e]=32-Bit Interrupt Gate target=0x0008:0xc000187a, DPL=0
IDT[0x0f]=32-Bit Interrupt Gate target=0x0008:0xc0001892, DPL=0
IDT[0x10]=32-Bit Interrupt Gate target=0x0008:0xc00018ab, DPL=0
IDT[0x11]=32-Bit Interrupt Gate target=0x0008:0xc00018c4, DPL=0
IDT[0x12]=32-Bit Interrupt Gate target=0x0008:0xc00018dc, DPL=0
IDT[0x13]=32-Bit Interrupt Gate target=0x0008:0xc00018f5, DPL=0
IDT[0x14]=32-Bit Interrupt Gate target=0x0008:0xc000190e, DPL=0
IDT[0x15]=32-Bit Interrupt Gate target=0x0008:0xc0001927, DPL=0
IDT[0x16]=32-Bit Interrupt Gate target=0x0008:0xc0001940, DPL=0
IDT[0x17]=32-Bit Interrupt Gate target=0x0008:0xc0001959, DPL=0
IDT[0x18]=32-Bit Interrupt Gate target=0x0008:0xc0001972, DPL=0
IDT[0x19]=32-Bit Interrupt Gate target=0x0008:0xc000198a, DPL=0
IDT[0x1a]=32-Bit Interrupt Gate target=0x0008:0xc00019a3, DPL=0
IDT[0x1b]=32-Bit Interrupt Gate target=0x0008:0xc00019bb, DPL=0
IDT[0x1c]=32-Bit Interrupt Gate target=0x0008:0xc00019d3, DPL=0
IDT[0x1d]=32-Bit Interrupt Gate target=0x0008:0xc00019ec, DPL=0
IDT[0x1e]=32-Bit Interrupt Gate target=0x0008:0xc0001a04, DPL=0
IDT[0x1f]=32-Bit Interrupt Gate target=0x0008:0xc0001a1c, DPL=0
IDT[0x20]=32-Bit Interrupt Gate target=0x0008:0xc0001a35, DPL=0
You can list individual entries with 'info idt [NUM]' or groups with 'info idt [NUM] [NUM]'

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值