比如说,当你在家里吃晚餐,突然电话响了,因为可能是紧急事件,所以你去拿起电话,这就是中断。异常是这样的,它更像你在吃东西的时候,突然发现食物中有蟑螂,只剩一半了,而且还在蹬腿,呕~~~。在接完电话或者吐完后,我们继续回到正常生活中去。希望在这个异常发生后,你还能有胃口。
简而言之,中断和异常都停止的处理器的处理流程并强迫它去做另外的一些事,做完后处理器继续原来的处理。中断和异常的不同在于中断可以被调用,例如硬件或者详细一点像键盘输入、系统时钟之类的,但是异常只会在执行指令时被事先定义好的条件触发,例如除零错,但是这里有异常的异常:),例如
可以用户任务调用用于调试程序。INT
3
处理器把每一种中断或异常都关联到了一个独有的数值上去,这个数值实际上是中断向量表的索引。中断向量表在实模式下载地址0处,它们已经被我们的内核覆盖了,反正我们在保护模式下不能调用它们。
根据Intel手册,分别有两个原因可以造成中断也有两个原因可以触发异常,
中断
可屏蔽中断,通过INTR脚触发。
不可屏蔽中断,由NMI脚触发。
IRQ脚 | 中断向量 | 中断 |
---|---|---|
IRQ0 | 08 | 系统时钟 |
IRQ1 | 09 | 键盘 |
IRQ2 | 0A | 桥连到PIC2 |
IRQ3 | 0B | COM2 |
IRQ4 | 0C | COM1 |
IRQ5 | 0D | LPT2 |
IRQ6 | 0E | 软驱 |
IRQ7 | 0F | LPT1 |
IRQ8 | 70 | CMOS实时时钟 |
IRQ9 | 71 | |
IRQ10 | 72 | |
IRQ11 | 73 | |
IRQ12 | 74 | PS/2鼠标 |
IRQ13 | 75 | 数学协处理器 |
IRQ14 | 76 | 硬盘IDE0 |
IRQ15 | 77 | 硬盘IDE1 |
IRQ是连接PIC(一会儿解释)到硬件的物理针脚,在AT机上有16个针脚。
异常
处理器侦测的。分类为错误、陷阱和终止,后面的教程再解释它们。
可编程的。INTO
、
、INT
3
和INT
nBOUND
指令可触发异常。这些指令通常被称为“软件中断”,但是处理器把它们当做异常处理。
中断向量 | 异常 |
---|---|
00 | 除零错 |
01 | 调试异常 |
02 | 不可屏蔽中断(NMI) |
03 | 断电(INT 3指令) |
04 | 上溢出(INTO指令) |
05 | 边界检查(BOUND指令) |
06 | 无效操作码 |
07 | 无协处理器 |
08 | 双重错误 |
09 | 协处理器段超限 |
0A | 无效的TSS |
0B | 段不存在 |
0C | 栈异常 |
0D | 一般保护错,当年Windows 9X下臭名卓著的蓝屏 |
0E | 页错误 |
0F | Intel保留 |
10 | 协处理器错 |
11-19 | Intel保留 |
1A-FF | 未用 |
有趣的是,我们注意到中断和异常间是有冲突的,IRQ0-IRQ7的中断向量和异常08-10的向量是重叠的,我们必须处理这个问题。
可屏蔽中断的向量索引是由两块8259A可编程中断控制器(PIC)决定的,它们级联到一起来处理硬件中断,PIC1处理IRQ0-IRQ7,PIC2处理IRQ8-IRQ15,当中断发生时,它们获得信号并通知处理器,接着处理器停止正常的执行流程,并通过向量索引来找到ISR(中断服务程序),接着调用ISR。我们可以通过8259A来重新映射新的向量索引值。
为了重映射这些IRQ,我们必须对8259A编程(原来pic_install
中好几条注释放错了位置,沈峰指出了这些错误),这个不容易,你最好自己上网搜一下,但是重映射的代码基本大同小异,
04/init.c
static
void
pic_install(void
) {
outb(0x11, 0x20);
outb(0x11, 0xa0);
/*Remaps IRQ0-IRQ7 to 0x20-0x27 in interrupt vector table*/
outb(0x20, 0x21);
/*Remaps IRQ8-IRQ15 to 0x28-0x2F in interrupt vector table*/
outb(0x28, 0xa1);
/*PIC2 is connected to PIC1 via IRQ2*/
outb(0x04, 0x21);
outb(0x02, 0xa1);
/*Enables 8086/88 mode*/
outb(0x01, 0x21);
outb(0x01, 0xa1);
/*Disables all interrupts from IRQ0-IRQ7*/
outb(0xff, 0x21);
/*Disables all interrupts from IRQ8-IRQ15*/
outb(0xff, 0xa1);
}
顺便提一下,好消息是,我们现在可以使用C编程了。上面代码里的outb
是一个宏,工作起来像这样outb(byte, port)
,还有其它要用到的一些宏。
04/include/asm.h
#define cli() __asm__
("
\n\t")cli
#define sti() __asm__
("
\n\t")sti
#define halt() __asm__
("cli
;hlt
\n\t");
#define idle() __asm__
("jmp
.\n\t");
#define inb(port) (__extension__
({ \
unsigned
char
__res; \
__asm__
("inb
%%dx
, %%al
\n\t" \
:"=a"(__res) \
:"dx
"(port)); \
__res; \
}))
#define outb(value, port) __asm__
( \
"outb
%%
, %%al
\n\t"::"dx
"(value), "al
"(port))dx
#define insl(port, buf, nr) \
__asm__
("cld
;rep
;insl
\n\t" \
::"d"(port), "D"(buf), "c"(nr))
#define outsl(buf, nr, port) \
__asm__
("cld
;rep
;outsl
\n\t" \
::"d"(port), "S" (buf), "c" (nr))
现在我们知道怎样重映射中断,并且让处理器知道中断向量表中正确的索引。但是,这里有另一个问题,实模式下的中断表被我们的内核覆盖了,这该怎么办?坏消息是,我们必须从头写……
处理器对怎样处理中断和异常一视同仁,当任何一个发生时,处理器终止当前任务的执行并切换到特定的程序段中去以处理中断或异常,这些程序段叫做中断服务程序(ISR)。中断服务程序一旦执行完毕,处理器会回到原来的任务执行。
然而,怎样让处理器找到这些程序段呢?处理器管理了一个表叫做中断描述符表(IDT),它包含了一些描述了怎样访问那些ISR的描述符。就像GDT那样,IDT的物理地址保存在IDTR
。
在IDT中的每一个中断和异常都有一个独特的数值,就是被我们重新映射的向量。IDT包含了一组64字节长的描述符,最多有256个描述符在表中。LIDT
指令告诉处理器通过寄存器IDTR
去找IDT,就想LGDT
和GDT的关系那样。
看一下IDT项的格式,
我们熟悉大部分的域,我会在后面详细解释它们。实际上IDT中有好几类的描述符,Skelix使用中断门。
现在处理器知道了当中断或异常发生时到哪里去找处理程序,那么ISR应该是什么样的呢?因为中断或异常终止了正常的执行,并且最后还要回到正常流程中去,也就意味着ISR必须保存正常执行环境,它必须保存所有可能被影响到的寄存器并最终把它们的值重置为进入ISR之前的值。
如果ISR代码和当前任务有相同的权限级,那么ISR使用当前堆栈,否则任务切换代码会保存当前SS
、ESP
、EFLAGS
、CS
和EIP
,并从TSS
(后面的教程中讨论它)中载入新的CS
、EIP
和堆栈,并把保存的寄存器值压入新堆栈中。堆栈切换完成后(如果有的话),处理器把EFLAGS
、CS
和EIP
按这个顺序压入栈中,一些异常还提供了关于错误信息的错误码,这个错误码也必须入栈保存。之后,保存在相应IDT描述符中的CS
和EIP
被载入。因为我们使用中断门,EFLAGS
中的IF标志位将被清除。从ISR返回就要把这个顺序反过来,大体来说……
嗯……还没睡着吧?我猜你大概是糊涂了,所以还是让代码说明一切吧。在上节最后,我们让03/load.s
输出“Hello World!”。本节中,我们用一个对C代码的调用取代。
.text
.globl
pm_mode
.include
"kernel.inc"
.org
0
pm_mode:
movl
$DATA_SEL,%eax
movw
%ax
, %ds
movw
%ax
, %es
movw
%ax
, %fs
movw
%ax
, %gs
movw
%ax
, %ss
movl
$STACK_BOT,%esp
cld
movl
$0x10200,%esi
movl
$0x200, %edi
movl
$KERNEL_SECT<<7,%ecx
rep
movsl
call
init
init
初始化了一些硬件和系统表,此时它看起来像这样,
04/init.c
unsigned
long
long
*idt = ((unsigned
long
long
*)IDT_ADDR);
unsigned
long
long
*gdt = ((unsigned
long
long
*)GDT_ADDR);
方便起见,我们用一个long long
来表示描述符,为了禁掉GCC的警告信息,必须使用在Makefile中使用编译选项--Wno-long-long
。
static
void
isr_entry(int
index, unsigned
long
offset) {long
unsigned
long
idt_entry = 0x00008e0000000000ULL |long
((unsigned
long
)CODE_SEL<<16);long
idt_entry |= (offset<<32) & 0xffff000000000000ULL;
isr_entry
填充指定的IDT项,第一个参数是IDT中的索引,第二个参数是ISR地址。
现做一个模板,IDT项看起来像这样,
IDT模板的值是0x00008e0000080000,它表示开始于地址0,使用选择子0x8(内核代码选择子),存在于内存并且DPL为0。
idt_entry |= (offset) & 0xffff;
idt[index] = idt_entry;
填充IDT。
}
static
void
idt_install(void
) {
idt_install
装入了256个ISR。
unsigned
int
i = 0;
struct
DESCR {
unsigned
short
length;
unsigned
long
address;
} __attribute__
((packed)) idt_descr = {256*8-1, IDT_ADDR};
建立IDTR
所用的结构,和GDTR
的格式相同。我们需要GCC扩展
以阻止GCC填充结构。否则GCC会为了效率原因把这个结构扩展为64字节长,像2字节__attribute__
((packed))length
,2字节填充,4字节address
这样。
for
(i=0; i<VALID_ISR; ++i)
isr_entry(i, (unsigned
int
)(isr[(i<<1)+1]));
for
(++i; i<256; ++i)
isr_entry(i, (unsigned
int
)default_isr);
isr
数组存储了所有的ISR,一会儿解释它。深吸一口气,你将看到一个很长的栈帧。VALID_ISR
在04/include/isr.h
中定义,它指出了IDT中有多少个项是可用的,本节中用的值是32,它是上面介绍的所有中断和异常的总和。
剩余的IDT项被填充为默认ISR,就是在屏幕上改变一个字符来告诉我们有一个为处理的中断发生了。
__asm__
__volatile__
("lidt %0\n\t"::"m"(idt_descr));
LIDT
将IDT装入IDTR
。
}
static
void
pic_install(void
) {
outb
(0x11, 0x20);
outb
(0x11, 0xa0);
outb
(0x20, 0x21);
outb
(0x28, 0xa1);
outb
(0x04, 0x21);
outb
(0x02, 0xa1);
outb
(0x01, 0x21);
outb
(0x01, 0xa1);
outb
(0xff, 0x21);
outb
(0xff, 0xa1);
}
void
init(void
) {
int
a = 3, b = 0;
idt_install();
pic_install();
a /= b;
}
在init
的最后,我们做点坏事,让3/0好产生一个除零异常。
我们知道一旦有硬件发送信号到PIC,PIC会通知处理器中断当前执行的代码,然后处理器会找到指定的ISR去做我们想让它做的事。执行完ISR后,它会回到原来的执行流程中去。所以ISR才是我们关心的东西。
04/isr.s
.text
.include
"kernel.inc"
.globl
default_isr, isr
.macro
isrnoerror nr
isr\nr:
pushl
$0
pushl
$\nr
jmp
isr_comm
.endm
宏isrnoerror
处理没有错误码的异常,它实际上就是一个外壳,仅仅把当做错误码的值0和ISR索引压入栈。
.macro
isrerror nr
isr\nr:
pushl
$\nr
jmp
isr_comm
.endm
用这两个宏,不论有无错误码,所有的异常都可以被一视同仁。
isr: .long
divide_error, isr0x00, debug_exception, isr0x01
.long
breakpoint, isr0x02, nmi, isr0x03
.long
overflow, isr0x04, bounds_check, isr0x05
.long
invalid_opcode, isr0x06, cop_not_avalid, isr0x07
.long
double_fault, isr0x08, overrun, isr0x09
.long
invalid_tss, isr0x0a, seg_not_present, isr0x0b
.long
stack_exception, isr0x0c, general_protection, isr0x0d
.long
page_fault, isr0x0e, reversed, isr0x0f
.long
coprocessor_error, isr0x10, reversed, isr0x11
.long
reversed, isr0x12, reversed, isr0x13
.long
reversed, isr0x14, reversed, isr0x15
.long
reversed, isr0x16, reversed, isr0x17
.long
reversed, isr0x18, reversed, isr0x19
.long
reversed, isr0x1a, reversed, isr0x1b
.long
reversed, isr0x1c, reversed, isr0x1d
.long
reversed, isr0x1e, reversed, isr0x1f
这是在04/init.c
中使用的isr
数组,它的元素成对定义,例如,divide_error
和isr0x00
,divide_error
是干活的代码段的实际入口,isr0x20
是用上面两个宏其中之一产生的代码段,用来维持统一的栈帧。
/*
+-----------+
| old ss | 76
+-----------+
| old esp | 72
+-----------+
| eflags | 68
+-----------+
| cs | 64
+-----------+
| eip | 60
+-----------+
| 0/err | 56
+-----------+
| isr_nr | tmp = esp
+-----------+
| eax | 48
+-----------+
| ecx | 44
+-----------+
| edx | 40
+-----------+
| ebx | 36
+-----------+
| tmp | 32
+-----------+
| ebp | 28
+-----------+
| esi | 24
+-----------+
| edi | 20
+-----------+
| ds | 16
+-----------+
| es | 12
+-----------+
| fs | 8
+-----------+
| gs | 4
+-----------+
| ss | 0
+-----------+
*/
很漂亮的栈帧吧:),所有的异常和中断都使用这个栈帧。
isr_comm:
把所有信息压入栈后,两个宏isrnoerror
和isrerror
转跳到这里开始一些共有的处理代码。
pushal
pushl
%ds
pushl
%es
pushl
%fs
pushl
%gs
pushl
%ss
压栈、压栈、压栈……
movw
$DATA_SEL,%ax
movw
%ax
, %ds
movw
%ax
, %es
movw
%ax
, %fs
movw
%ax
, %gs
所有的数据段都载入权限0的选择子。
movl
52(%esp
),%ecx
call
*isr(, %ecx
, 8)
调用ISR干活,52是ISR在栈帧中的索引。
addl
$4, %esp
# for %ss
ISR结束后,栈回滚。最后一条语句跳过了栈中的SS
,因为我们不能像其它寄存器那样把它POP
出来。
popl
%gs
popl
%fs
popl
%es
popl
%ds
popal
addl
$8, %esp
# for isr_nr and err_code
跳过栈中的错误码和ISR索引,把栈恢复成ISR调用之前的状态。
iret
isrNoError 0x00
isrNoError 0x01
isrNoError 0x02
isrNoError 0x03
isrNoError 0x04
isrNoError 0x05
isrNoError 0x06
isrNoError 0x07
isrError 0x08
isrNoError 0x09
isrError 0x0a
isrError 0x0b
isrError 0x0c
isrError 0x0d
isrError 0x0e
isrNoError 0x0f
isrError 0x10
isrNoError 0x11
isrNoError 0x12
isrNoError 0x13
isrNoError 0x14
isrNoError 0x15
isrNoError 0x16
isrNoError 0x17
isrNoError 0x18
isrNoError 0x19
isrNoError 0x1a
isrNoError 0x1b
isrNoError 0x1c
isrNoError 0x1d
isrNoError 0x1e
isrNoError 0x1f
为异常生成所有的isr0x??
。
default_isr:
incb
0xb8000
movb
$2, 0xb8001
movb
$0x20, %al
outb
%al
, $0x20
outb
%al
, $0xa0
告诉PIC1和PIC2我们的ISR调用已经完成,它们可以接受新的中断。
iret
这是为未用的IDT项准备的默认ISR,我们在04/init.c
中用过它,它仅仅是在屏幕上用先有的颜色告诉我们什么刚刚发生,并告诉PIC我们的ISR调用已经完成。这两个outb
很重要,否则不会有新的中断进来。
在这里,所有的异常处理程序仅仅是打印寄存器信息,我展示一下是怎么做的。
04/exceptions.c
#include <kprintf.h>
#include <asm.h>
#include <scr.h>
void
divide_error(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
debug_exception(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
breakpoint(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
nmi(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
overflow(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
bounds_check(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
invalid_opcode(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
cop_not_avalid(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
double_fault(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
overrun(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
invalid_tss(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
seg_not_present(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
stack_exception(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
general_protection(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
page_fault(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
reversed(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
void
coprocessor_error(void
) {
__asm__
("pushl
%%eax
;call
info"::"a"(KPL_PANIC));
halt();
}
所有的ISR通过调用info
来在屏幕上显示信息。
void
info(enum
KP_LEVEL kl,
unsigned
int
ret_ip, unsigned
int
ss, unsigned
int
gs, unsigned
int
fs,
unsigned
int
es, unsigned
int
ds, unsigned
int
edi, unsigned
int
esi,
unsigned
int
ebp, unsigned
int
esp, unsigned
int
ebx, unsigned
int
edx,
unsigned
int
ecx, unsigned
int
eax, unsigned
int
isr_nr, unsigned
int
err,
unsigned
int
eip, unsigned
cs, int
unsigned
eflags,int
unsigned
int
old_esp, unsigned
old_ss) {int
static
const
char
*exception_msg[] = {
"DIVIDE ERROR",
"DEBUG EXCEPTION",
"BREAK POINT",
"NMI",
"OVERFLOW",
"BOUNDS CHECK",
"INVALID OPCODE",
"COPROCESSOR NOT VALID",
"DOUBLE FAULT",
"OVERRUN",
"INVALID TSS",
"SEGMENTATION NOT PRESENT",
"STACK EXCEPTION",
"GENERAL PROTECTION",
"PAGE FAULT",
"REVERSED",
"COPROCESSOR_ERROR",
};
unsigned
int
cr2, cr3;
(void
)ret_ip;
__asm__
("movl
%%cr2, %%eax
":"=a"(cr2));
__asm__
("movl
%%cr3, %%eax
":"=a"(cr3));
if
(isr_nr < sizeof
exception_msg)
kprintf(kl, "EXCEPTION %d: %s\n=======================\n",
isr_nr, exception_msg[isr_nr]);
else
kprintf(kl, "INTERRUPT %d\n=======================\n", isr_nr);
kprintf(kl, "cs:\t%x\teip:\t%x\teflags:\t%x\n", cs, eip, eflags);
kprintf(kl, "ss:\t%x\tesp:\t%x\n", ss, esp);
kprintf(kl, "old ss:\t%x\told esp:%x\n", old_ss, old_esp);
kprintf(kl, "errcode:%x\tcr2:\t%x\tcr3:\t%x\n", err, cr2, cr3);
kprintf(kl, "General Registers:\n=======================\n");
kprintf(kl, "eax:\t%x\tebx:\t%x\n", eax, ebx);
kprintf(kl, "ecx:\t%x\tedx:\t%x\n", ecx, edx);
kprintf(kl, "esi:\t%x\tedi:\t%x\tebp:\t%x\n", esi, edi, ebp);
kprintf(kl, "Segment Registers:\n=======================\n");
kprintf(kl, "ds:\t%x\tes:\t%x\n", ds, es);
kprintf(kl, "fs:\t%x\tgs:\t%x\n", fs, gs);
}
Makefile需要大改来使代码通过编译。
AS=as -Iinclude
LD=ld
CC=gcc
CPP=gcc -E -nostdinc -Iinclude
用GCC自动生成依赖关系。
CFLAGS=-Wall -pedantic -W -nostdlib -nostdinc -Wno-long-long -I include -fomit-frame-pointer
-Wall -pedantic -W
是警告选项,-nostdlib
告诉GCC不要用标准库,-nostdinc -Iinclude
告诉GCC应该在目录include
下寻找头文件而不搜索标准头文件目录,-Wno-long-long
禁止关于long long
不是C89标准的警告,-fomit-frame-pointer
很重要,否则info
不能正确的处理栈帧。
KERNEL_OBJS= load.o init.o isr.o libcc.o scr.o kprintf.o exceptions.o
#.c.s:
# ${CC} ${CFLAGS} -S -o $*.s $<
.s.o:
${AS} -a $< -o $*.o >$*.map
all: final.img
final.img: bootsect kernel
cat bootsect kernel > final.img
@wc -c final.img
bootsect: bootsect.o
${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<
kernel: ${KERNEL_OBJS}
${LD} --oformat binary -N -e pm_mode -Ttext 0x0000 -o $@ ${KERNEL_OBJS}
@wc -c kernel
clean:
rm -f *.img kernel bootsect *.o
dep:
sed '/\#\#\# Dependencies/q' < Makefile > tmp_make
(for i in *.c;do ${CPP} -M $$i;done) >> tmp_make
mv tmp_make Makefile
### Dependencies:
自动生成依赖关系,### Dependencies:
很重要,别删。
执行make clean && make dep && make
,make dep
必须在make
之前调用。
如果我们做的都对的话,我们就可以看到错误信息了:)
你可以自由使用我的代码,如有疑问请联系我。