第4课:中断和异常1
声明:转载请保留:
译者:http://www.cppblog.com/jinglexy
原作者:xiaoming.mo at skelix dot org
MSN & Email: jinglexy at yahoo dot com dot cn
目标
下载源程序
在本课中,我们学习如何处理中断和异常。并且加入中断处理程序到skelix内核中,这样可以在中断或异常来临时打印一些可以看得到的东西,这些程序是后面课程的基础。
那么中断和异常到底是什么咚咚呢?
举例来说吧,小胖正在家里吃饭,吃的很歪歪的时候,MM来了一个电话,于是接电话告诉她晚上七点在村口YY树下不见不散,这就是一个突发事件,接完电话继续吃饭。突然小胖在米饭里面发现一个蟑螂,errr~~~~。只好结束晚餐了,这个就是异常了。中断是可以返回来的,比如上面的接电话,但异常就不能返回继续做刚才的事了。
简单的说,中断和异常都是停止CPU正在做的事情,强制它做另外一件事,然后回到原来的控制流上继续执行。中断和异常的区别在于它们的外部触发源,对于硬件设备来说,如键盘输入,系统时钟等就会触发中断。而保护模式下的指令执行才会引发异常,例如除零错误,双重错误:),INT 3指令就是主动触发异常(可以在用户态进行测试),表示这个进程进入调试状态。
处理器为每个中断或异常分配一个独立的编号,这个编号实际上就是中断向量表,在实模式下它是从物理地址0开始的,现在早已被我们的内核‘skelix’覆盖了,即便没有覆盖,我们仍然无法在保护模式中使用这些实模式的中断。在Intel手册上,有两种外部中断和两种异常。
中断分类
1)可屏蔽中断:通过INTR引脚告诉CPU来了一个中断,可以被寄存器设置屏蔽调
2)非可屏蔽中断:通过NMI引脚告诉CPU来了一个中断,不可屏蔽
下面是系统启动默认设置的中断向量:
IRQ 引脚
|
中断向量
|
中断
|
IRQ0
|
08
|
系统时钟
|
IRQ1
|
09
|
键盘
|
IRQ2
|
0A
|
PIC2桥接,即从8259A
|
IRQ3
|
0B
|
COM2
|
IRQ4
|
0C
|
COM1
|
IRQ5
|
0D
|
LPT2
|
IRQ6
|
0E
|
软盘
|
IRQ7
|
0F
|
LPT1
|
IRQ8
|
70
|
CMOS Real Time Clock
|
IRQ9
|
71
|
|
IRQ10
|
72
|
|
IRQ11
|
73
|
|
IRQ12
|
74
|
PS/2 Mouse
|
IRQ13
|
75
|
数学协处理器
|
IRQ14
|
76
|
硬盘设备 IDE0
|
IRQ15
|
77
|
硬盘设备 IDE1
|
上面表格中的IRQ是中断控制器的物理引脚,直接连到外部硬件设备上的。在AT兼容机器上有16个引脚。
异常分离
1)处理器检查:页故障,陷阱等
2)程序片:例如INTO,INT 3,INT n等系统调用
中断向量
|
异常
|
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中断向量和异常的0x08-0x10重叠了,所以我们得处理一下。中断的屏蔽可以通过8259A PIC(programmable interrupt controllers)设置。PIC1处理IRQ0到IRQ7,PIC2处理IRQ8到IRQ15,当中断到来时,PIC得到信号并通知CPU。CPU收到后会停止当前任务执行,并转向中断向量中指向的处理代码。我们称之为ISR(interrupt service routine)。8259A设置的中断向量编号可重新分配,为了做到这点,我们需要对8259A进行编程,这个是有难度的事情,你可以在网络上搜索相关的文章自己看下(否则本文可能很难看下去,如果你一点基础也没有的话,最好能很好的理解ICW和OCW的几个命令字),下面的程序就是干这事的:
04/init.c
static void
pic_install(void) {
outb(0x11,
0x20); //
ICW1命令字,使用边沿触发中断,多片8259A级联
outb(0x11, 0xa0);
outb(0x20,
0x21); //
ICW2命令字,重新分配中断向量编号IRQ0-IRQ7
outb(0x28, 0xa1); //
ICW2命令字,重新分配中断向量编号IRQ8-IRQ15
outb(0x04,
0x21); //
ICW3命令字,主从8259A链接设置
outb(0x02, 0xa1);
outb(0x01,
0x21); //
ICW4命令字,设置EIO/AEIO模式,缓冲方式等
outb(0x01, 0xa1);
outb(0xff,
0x21); //
屏蔽IRQ0-IRQ7所有中断
outb(0xff,
0xa1); //
屏蔽IRQ8-IRQ15所有中断
}
一个好消息:我们现在开始终于使用C语言了。类似outb这样的宏会替代一些汇编语言,这样看起来会好过一些。不熟悉AT&T汇编的可能要补一下了,否则可能认为进度太快了。
04/include/asm.h
#define cli() __asm__ ("cli\n\t")
#define sti() __asm__ ("sti\n\t")
#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,
%%dx\n\t"::"al"(value), "dx"(port))
#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))
现在我们知道了怎么重新分配中断向量,但是另外一个问题:实模式下的中断向量表已经被内核覆盖了,怎么办呢?我们不得不重写它们了。这一点也不有趣。
IDT and ISR
处理器执行中断和异常的方法都是一样的,当某个中断或异常发生时,处理器停止当前任务跳转到特定的例程中去,这个例程就是ISR。当ISR执行完后,返回控制到原来的任务中去。那么处理器又是怎么样路由这些中断的呢?原因是系统中存在一个叫IDTR(IDT register)的寄存器,它指向内存中的一个叫中断描述符表的缓冲,这个描述符表就定义了所有中断例程(即ISR)的逻辑地址等。它和GDT看起来很像,只有个别的位不同而已。IDT中为每个中断或异常都准备了独立的一项,我们常称之为向量(就是上面重新分配的中断向量编号)。IDT可以看作是一个64位长整型的数组,最多可有256项。LIDT指令可以加载IDT的地址到IDTR中,就像LGDT加载GDT地址到GDTR一样。现在我们来看一下IDT描述符:
图-0
63_______________56__55__54__53__52__51_____________48_
| 偏移地址(31到16位) |
|_______________________________________________________|
_47__46__45________________________________36________32_
| P | DPL
| 01110000 |
未使用 |
|_______________________________________________________|
31____________________________________________________16
|
描述符选择子
|
|_______________________________________________________|
16_____________________________________________________
| 偏移地址(15到0位)
|
|_______________________________________________________|
大多数的域我们已经很熟悉了,后面的代码中我会详细讲解到。实际上,有多种描述符,但这里我们只用到中断门。现在我们设置CPU使其路由到正确的中断例程ISR中去,那么ISR到底是什么呢?因为中断和异常会停止当前执行的任务并且需要返回回来继续执行,所以ISR需要保存当前任务的运行环境,就是一大堆寄存器。需要在进入ISR的时候保存这些寄存器,并在离开的时候回复它们。
如果ISR代码和当前任务特权级相同,那么ISR将会使用当前任务堆栈(这个任务一般是内核线程,当然也有例外),或者就是切换到内核栈中, 即从TSS(后面介绍这个咚咚)中加载新的CS,EIP和栈,并按顺序保存所有的寄存器到新的堆栈中。当堆栈切换后(如果有的话),CPU会自动保存SS, ESP, EEFLAGS, CS 和 EIP等寄存器到栈中。有些异常会带一个错误号(表示错误的一些信息),这个错误号也会自动压入栈中。之后IDT中的CS和EIP将会被加载从而执行ISR例程。因为我们使用的是中断门,所以IF标识(EFLAGS寄存器)将会被清掉。从ISR例程返回逆序做上面的步骤即可,不值得一提。
图1-特权级不变
_________________ _________________
|
|
|
| |
|_________________|
|_________________| |
|
|
|
| |
|_________________|
|_________________| | 栈增长方向
| Old EFLAGS
| | Old EFLAGS
| |
|_________________|
|_________________| |
| Old
CS |
| Old
CS | |
|_________________|
|_________________| |
| Old
EIP |
| Old
EIP | \|/
|_________________|
|_________________|
|
| | Error Code
|
|_________________|
|_________________|
图2-特权级变化
_________________ _________________
| Old
SS |
| Old SS
| |
|_________________|
|_________________| |
| Old
ESP |
| Old ESP
| |
|_________________|
|_________________| | 栈增长方向
| Old EFLAGS
| | Old EFLAGS
| |
|_________________| |_________________|
|
| Old
CS |
| Old
CS | |
|_________________|
|_________________| |
| Old
EIP |
| Old
EIP | \|/
|_________________|
|_________________|
|
| | Error Code
|
|_________________|
|_________________|
我假定读者看到这里还没有昏菜,如果是的话找些保护模式的书补一补哦。也许看到下面的程序可能更清晰一些。在前面课程中我们在load.s程序的最后面打印"Hello World!",本课中我们先跳转到C代码中去执行,稍后就是中断和异常相关程序了。
04/load.s
.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
# 进入到C语言编写的程序中
init函数将会初始化硬件和系统表(idt和gdt):
04/init.c
unsigned long long *idt = ((unsigned long long
*)IDT_ADDR);
unsigned long long *gdt = ((unsigned long long *)GDT_ADDR);
为了方便存取,我们使用一个long long(ia32上是8个字节)表示一个描述符,GCC也许会给出警告(对于这种类型),使用--Wno-long-long 就可以了.
这个函数用来填充IDT项,index参数是索引,第二个参数offset是ISR例程的地址。
static void
isr_entry(int index, unsigned long long offset)
{ //
IDT描述符格式请参考图-0
unsigned long long idt_entry = 0x00008e0000000000ULL |
((unsigned long
long)CODE_SEL<<16);
// 通过上面设置后,idt项值变为0x00008e0000080000,表示偏移地址为0,使用0x8作为描述符选择子(即内核代码段描述符),该ISR是存在的且特权级DPL为0
idt_entry |= (offset<<32) &
0xffff000000000000ULL;
idt_entry |= (offset) & 0xffff;
// 上面两行填充ISR地址
idt[index] = idt_entry;
// 填充值到idt中
}
// 这个函数安装所有的256 ISR例程
static void
idt_install(void) {
unsigned int i = 0;
struct DESCR {
unsigned short length;
unsigned long address;
} __attribute__((packed)) idt_descr = {256*8-1,
IDT_ADDR}; // 防止默认4字节对齐,该变量占6字节
for (i=0; i<VALID_ISR;
++i) // isr数组存放着所有的ISR例程地址,后面会讲到
isr_entry(i, (unsigned
int)(isr[(i<<1)+1]));
// 安装异常,就是 i * 2 + 1
for (++i; i<256; ++i)
isr_entry(i, (unsigned
int)default_isr); //
安装中断
__asm__ __volatile__ ("lidt
%0\n\t"::"m"(idt_descr));
}
static void
pic_install(void) {
This function has been explained earlier 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();
sti();
a /=
b; //
测试除零异常,看看发生了什么
}
好了,现在进入核心程序。我们不得不又和一些汇编程序打交道。我们知道,当硬件向PIC发送一个信号后,PIC通知处理器停止当前任务执行,然后处理器查找ISR例程,然后执行ISR,再返回控制流。所以ISR就是我们关注的地方:
04/isr.s
.text
.include "kernel.inc"
.globl default_isr, isr
.macro
isrnoerror
nr // 用于无错误码异常的宏
isr\nr:
pushl
$0 //
push一个额外的0
pushl $\nr
jmp
isr_comm
.endm
.macro
isrerror nr
isr\nr:
pushl $\nr
jmp
isr_comm
.endm
关于宏的说明,可以参考linux中as的帮助页。使用pinfo可以很好的浏览,
这个指令在我的博客上有介绍(http://www.cppblog.com/jinglexy),
查找2007.4月份文档即可。
使用上面两个宏,我们可以很方便的定义中断和异常函数及编号:
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
上面就是init.c中使用的isr例程数组,注意类似isr0x00的咚咚就是代码地址。
图-3
+-----------+
| 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:
pushal //
依次把寄存器AX、CX、DX、BX、SP、BP、SI和DI压栈
pushl
%ds //
入栈,入栈,入栈......
pushl %es
pushl %fs
pushl %gs
pushl %ss
movw
$DATA_SEL,%ax // 所有数据段特权级都是0
movw
%ax, %ds
movw
%ax, %es
movw
%ax, %fs
movw
%ax, %gs
movl
52(%esp),%ecx // 看上面的堆栈图,52就是ISR例程的编号
call *isr(, %ecx,
8) // 不带参数执行isr例程
addl
$4, %esp // 我们当然不能popl %ss,所以就这样跳过去了
popl %gs
popl %fs
popl %es
popl %ds
popal
addl
$8, %esp // 跳过 isr_nr 和 err_code
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
default_isr: #
硬件中断处理例程
incb 0xb8000
movb
$2, 0xb8001
movb $0x20, %al
outb
%al, $0x20 # 发送OCW2,告诉 PIC1 ISR执行完毕
outb %al,
$0xa0 # 发送OCW2,告诉 PIC2 ISR执行完毕
iret
可以在这里找到OCW2的资料:
http://docs.huihoo.com/gnu_linux/own_os/interrupt-8259_5.htm,
我一直都想找一份端口大全的资料,如果哪位有可以发一份给我:
jinglexy at yahoo dot com dot cn
在现在阶段中,所有异常暂时打印一些寄存器,只是演示一下而已:
04/exceptions.c
void
divide_error(void) {
__asm__ ("pushl
%%eax;call info"::"a"(KPL_PANIC));
halt();
}
info 是这个文件最后定义的一个函数,它的参数就是上面图-3中的堆栈
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();
}
// 很好理解的函数,不费口舌了这里。
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 int cs, unsigned int
eflags,
unsigned int old_esp, unsigned int old_ss) {
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。
04/Makefile
AS=as -Iinclude
LD=ld
CC=gcc #
不用说,我们开始使用gcc了
CPP=gcc -E -nostdinc -Iinclude
CFLAGS=-Wall -pedantic -W -nostdlib -nostdinc
-Wno-long-long -I include -fomit-frame-pointer
-Wall -pedantic -W 打开所有的编译警告,-nostdlib 告诉 GCC 不使用标准库, -nostdinc -I include 告诉 GCC 只在本目录的include文件夹下找寻头文件。-Wno-long-long 上面已经说了。-fomit-frame-pointer 告诉编译器可能优化而不使用栈寄存器,个人觉得不要使用这个选项为好,用-fnoomit-frame-pointer就一定可以正确的回溯堆栈。
KERNEL_OBJS= load.o init.o isr.o libcc.o scr.o kprintf.o
exceptions.o
Adds new modules into kernel.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
上面的这个自动产生依赖是从linux-0.11里面来的。赵博的书上讲的很详细了。