[Linux-0.11] 中断初始化 traps_init()
对 Linux-0.11 系统中断初始化函数 trap_init()
的简要分析
中断初始化用到的几个宏
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);
...
set_system_gate(3,&int3); /* int3-5 can be called from all */
...
}
#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),\
"a" (0x00080000))
为看懂这段代码,需要的准备工作如下:
- GCC 内联汇编
- 中断描述符结构
GCC 内联汇编:
GCC 内联汇编格式为:
__asm__ (
"汇编指令1\n\t"
"汇编指令2\n\t"
:
: 输出操作数列表(可选)
: 输入操作数列表(可选)
: 破坏的寄存器列表(可选,这里未使用)
);
:
表示该内联汇编的约束部分,约束部分的代码会优先于汇编代码执行。
汇编指令中的 %0
、%1
为占位符,与约束条件相对应。
例如本例中 "movw %0,%%dx\n\t" \
的意思是:先计算立即数 "i" ((short) (0x8000+(dpl<<13)+(type<<8)))
后,插入到占位符 %0
的位置处。
中断描述符结构
根究 Intel 官方手册 Volume 3, Chapter 6.11 的 Figure 6.2
Intel 的中断描述符为 64 bits, 分为高四字节和低四字节。32 位模式下各个字段的功能为:
-
Offset (0-15, 16-31):中断处理程序的偏移地址。
- Offset 15-0(低 16 位):
- 位于中断描述符的 0-15 位(低 4 字节的第 0-15 位)。
- 存储中断处理程序地址的 低 16 位。
- Offset 31-16(高 16 位):
- 位于中断描述符的 48-63 位(高 4 字节的第 16-31 位)。
- 存储中断处理程序地址的 高 16 位。
- Offset 15-0(低 16 位):
-
Segment Selector:目标代码段的选择子。
通过门中的段选择子
-
Type (8-12):区分中断门(0xE)、陷阱门(0xF)、任务门(0x5)。
-
DPL (Descriptor Privilege Level, bits 13-14):描述符特权级。
-
P (Present, bit 15):当前中断描述符是否存在。
在 Linux-0.11 的 head.h
中定义了中断描述符的数据结构:
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];
extern desc_table idt,gdt;
因为 Linux-0.11 代码的硬件平台是 32 位 Intel CPU,unsigned long
为 4 字节大小,可知 desc_table
的每个元素都是 8 字节。idt
是全局中断符表。
所以到底实现了什么功能?
以语句 set_trap_gate(0,÷_error);
为例,假设中断函数 divide_error
的地址为:0x8458
-
set_trap_gate(0,÷_error);
传入中断描述符地址、中断函数入口地址之前阅读资料的时候看到过这么一句话:”CPU 以中断号为索引,从中断描述符中取出段选择子“。当时疑惑的点在于如何使用中断号为索引。但是呢,当我看到数组
desc_table
后,一切疑惑都豁然开朗:Linux-0.11 将中断描述符按照中断号存放到数组idt
中,因此,只要获取了中断号n
, 使用idt[n]
便可获得对应的中断描述符;回到代码,参数 0 为中断号,参数
divide_error
为中断函数地址。set_trap_gate() 是个宏,解析后为
_set_gate(&idt[n],15,0,addr);
你看,就是将 idt 中下标为 n 的元素地址作为第一个形参。 -
计算约束条件
-
"i" ((short) (0x8000+(dpl<<13)+(type<<8)))
:中断描述符高四字节的第 0-15 位,属性字段;其中,0x8000 的二进制为 0x1000 0000 0000 0000,即对高四字节第 15 位 P 置 1,表示该中断描述符存在。dpl 为特权等级,type 为中断门类型;
-
"o" (*((char *) (gate_addr)))
成员变量 a 的地址 -
"o" (*(4 + (char *) (gate_addr)))
成员变量 b 的地址(跳过 4 个字节) -
"d" ((char *) (addr))
将中断函数地址保存到寄存器EDX
中 -
"a" (0x00080000)
将段选择子存放到寄存器 EAX 的高 16 位中。为什么是
0x0008
? 段选择子的第 3-15 位为索引,0x0008 的二进制为 0x0000 0000 0000 1000,即索引是 1。在 setup.s 中对 GDT 进行了初始化,索引 1 指向代码区的段描述符。
-
-
执行汇编代码
-
movw %%dx,%%ax
将 DX 的第 16 位(w,word,2字节)复制到寄存器 AX 的低 16位。在前面的约束条件中已经将中断函数地址复制到 DX 寄存器中。因此,该指令会将中断函数地址的低 16 位复制到 AX 中。注意,在约束条件中已经将段选择子复制到 EAX 的高 16 位中。
-
movw %0,%%dx
将属性字段复制到 EDX 的低 16 位中。注意,EDX 寄存器原本是中断函数入口的地址,现在低 16 位已经是属性字段了。 -
movl %%eax,%1
将 EAX 的内容复制到成员变量 a 中(中断描述符低 4 字节); -
movl %%edx,%2
将 EDX 的内容复制到成员变量 b 中(中断描述符高 4 字节);
-
以上,便完成了对中断号为 0 的中断描述符初始化。