Loading

[Linux-0.11] 中断初始化 traps_init()

对 Linux-0.11 系统中断初始化函数 trap_init() 的简要分析

中断初始化用到的几个宏

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_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))

为看懂这段代码,需要的准备工作如下:

  1. GCC 内联汇编
  2. 中断描述符结构

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
img

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 位
  • 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,&divide_error); 为例,假设中断函数 divide_error 的地址为:0x8458

  1. set_trap_gate(0,&divide_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 的元素地址作为第一个形参。

  2. 计算约束条件

    • "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 指向代码区的段描述符。

  3. 执行汇编代码

    • 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 字节);
      img

以上,便完成了对中断号为 0 的中断描述符初始化。

posted @ 2025-04-23 12:34  三花娘娘  阅读(12)  评论(0)    收藏  举报