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

作者 : admin 本文共12182个字,预计阅读时间需要31分钟 发布时间: 2024-06-17 共1人阅读

代码文件布局

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

代码调用结构及思路说明

《操作系统真象还原》第七章——启用中断插图(1)

  • 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
");
}

/*
函数功能:构建中断描述符
函数实现:按照中断描述符结构体定义填充字段
参数:中断门描述符地址,属性,中断处理函数
*/
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
");
}


/*完成有关中断的所有初始化工作*/
void idt_init() {
   put_str("idt_init start
");
   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
");
}


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
");
    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<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

《操作系统真象还原》第七章——启用中断插图(2)

查看中断描述符

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]'

本站无任何商业行为
个人在线分享 » 《操作系统真象还原》第七章——启用中断
E-->