《操作系统真象还原》第七章——启用中断
代码文件布局
代码调用结构及思路说明
即
- 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
查看中断描述符
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]'