IDT
阅读原文时间:2021年04月20日阅读:1

在刚开始学习  “中断与异常”, 就接触到了 IDT(Interrupt Descriptor Table), 所以有必要对 IDT 的初始化进行考量。

第一步: 中断描述符表寄存器 (IDTR) 的 init

         通过汇编指令 lidt 对中断向量寄存器 (IDTR) init , 在 arch/i386/boot/setup.S 中描述 

# set up gdt and idt
        lidt    idt_48                          # load idt with 0,0

...

idt_48:
        .word   0                               # idt limit = 0
        .word   0, 0                            # idt base = 0L
        .word   0                               # alignment byte

第二步: 把 IDT 的起始地址装入 IDTR

          通过汇编指令 lidt 装入IDT 的大小和它的地址在 /arch/i386/kernel/head.S 中描述为:

.globl SYMBOL_NAME(idt)

lidt idt_descr

...

idt_descr:
       .word IDT_ENTRIES*8-1          # idt contains 256 entries
SYMBOL_NAME(idt):
        .long SYMBOL_NAME(idt_table)

其中 idt 是一个全局变量,kernel 对这个变量的引用就可以获得 IDT 表的地址.    表长: 256*8 = 2048 Bytes. 在 /include/asm-i386/segment.h 定义IDT_ENTRIES : 

#define IDT_ENTRIES     256

第三步: 用 setup_idt() 函数对 idt_table表中的 256 个表项 init

       3.1   linux Kernel 中 IDT 是一个全局数据,在 arch/kernel/i386/traps.c 中描述为:

struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };

这就说明:

               a)  定义的 idt_table 其属性(_attrbute_), _section_是汇编中的 '节', 指定了 idt_table 的起始地址存放在数据节的 idt 变量中;

                b) 每一个表项是 desc_struct 结构类型, 在 /include/asm-i386/processor.h 中描述为:

struct desc_struct {
        unsigned long a,b;
};

      3.2   对 setup_idt 分析 (/arch/i386/kernel/head.S)  L301~L325

 301/*
 302 *  setup_idt
 303 *
 304 *  sets up a idt with 256 entries pointing to
 305 *  ignore_int, interrupt gates. It doesn't actually load
 306 *  idt - that can be done only after paging has been enabled
 307 *  and the kernel moved to PAGE_OFFSET. Interrupts
 308 *  are enabled elsewhere, when we can be relatively
 309 *  sure everything is ok.
 310 */

...

 325        ret



setup_idt:

将 ignore_int() 的地址放入 edx 寄存器中 

        lea ignore_int,%edx

将段选择符加载进 eax 寄存器的高 16 位

        movl $(__KERNEL_CS << 16),%eax

将 ignore_init()函数地址的低 16 位放入 eax 寄存器的低 16 位 (ax) 中, 这样就在 eax 寄存器中组成了 idt 的低4字节的值. 也就是赋给 desc_struct->a 的值.

        movw %dx,%ax            /* selector = 0x0010 = cs */

将 0X8E00 放入edx 寄存器的低 16 位中, 刚才 edx 中存放的 ignore_init() 函数地址, 这样就在 edx 寄存器中形成了 idt 的高 4 Bytes 的值, 也就是赋给 desc_struct->b 的值.

        movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */

将 idt_table 数组的首地址放入 edi 寄存器中

        lea idt_table,%edi

init  循环计数器 ecx 为 256

        mov $256,%ecx



rp_sidt:

将 eax 中的内容(32 Bytes)放入edi 所指向的内存中,也就是 idt_table[256-(%ecx)] -> a

        movl %eax,(%edi)

将 edx 中的内容放入 edi+4 所指向的内存中, 也就是 idt_struct[256-(%ecx)] -> b, 这样就对 idt_struct[256-(%ecx)] 进行了 init

        movl %edx,4(%edi)

给 edi 加 8, 使得其指向 idt_struct[256-(%ecx)+1]

        addl $8,%edi

自减计数器 ecx

        dec %ecx

判断如果, ecx 不为 0, 则跳至rp_sidt 处, 继续 init idt_struct 数组的下一个数据项,直到 ecx 为 0, 也就是 idt_struct 数组全部被 init

        jne rp_sidt



        ret

<<< 读了http://eelab.tsinghua.edu.cn/book/09-06/748091276059810.html 有关中断的一段

进一步说明:

如图结构, 对应 idt_bable 数组中的一个表项, 是一个 idt_desc 类型的数据类型. 程序中的 idt_struct->a 对应 0~31 共 32 位, idt_struct->b 对应 32~63 共享32 位. 其中:

                                                    0 ~15  bits  +  48~64 bits                  形成 32 bits 偏移量(offset, 中断处理程序所在段(由于6~31 bits 给出)的段内偏)    

                                                    16~31 bits                                           中断处理程序的段选择符

                                                    40~43 bits                                           4 bits 标识 描述符的类型

                                                    45~46 bits                                           2 bits 标识 描述符的权限等级(DPL, Descriptor Privilege Level)

                                                    47        bits                                           1 bits  标识 段是否在内存中(如果为1, 表示段,当前不在内存中)

                                                                32~47 bits  即 0X8E00                                                 43~40 bits 即 1110    表示将此, idt 表初始化为 中断门(Interrupt gate) 描述符  

                                                                47        bit     即  1                                                                                     0101    表示 任务门(Task gate) 描述符

                                                                                                                                                                                   1111     表示 陷阱门(Trap gate) 描述符

中断门和陷阱门含有一个长指针(即段选择符和偏移值),处理器使用这个长指针把程序执行权转移到代码段中异常或中断的处理过程中。这两个段的主要区别在于处理器操作EFLAGS寄存器IF标志上。IDT中任务门描述符的格式与GDT和LDT中任务门的格式相同。任务门描述符中含有一个任务TSS段的选择符,该任务用于处理异常或中断.

           要说的是: ignore_int() 中断处理程序实际上是一个 "空" 处理. 当再次对 IDT 初始化的时候, 会使其最终初始化为有效的处理程序,也就是在 start_kernel() 中. 当系统跳转到 start_kernel() 中的时候, 内核才进行真正的初始化. 其中就包括, 调用 trap_init() 对 IDT 进行最终初始化.

                                              调用 set_trap_gate() 将其 init 为 陷阱门;   

                                              调用 set_intr_gate()  将其 init 为 中断门;                

                                              调用 set_system_gate()         将其 init 为 访问权级为 3 的陷阱门;

                                              调用 set_task_gate() 将其 init 为任务门;

                                              调用 set_system_intr_gate()  将其 init 为 访问权级为 3 的中断门

除了 set_task_gate(), 其他 4 个函数的接口都一样, 形如:

void set_xxx_gate(unsigned int n,void *addr)
{
           _set_gate(n,DESCTYPE_XXX,addr,__KERNEL_CS);
}

其中, 参数 n 给出了要 init 的 IDT 表项(idt_table 数组的下标), addr 给出对应的物理程序的地址.

在这些函数的内部均是调用 _set_gate(). 以下为 _set_gate()结构( v2.6.20 )( include/asm-i386/desc.h ):

 115static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg)
 116{
 117        __u32 a, b;
 118        pack_gate(&a, &b, (unsigned long)addr, seg, type, 0);
 119        write_idt_entry(idt_table, gate, a, b);
 120}

...

  43static inline void pack_gate(__u32 *a, __u32 *b,
  44        unsigned long base, unsigned short seg, unsigned char type, unsigned char flags)
  45{
  46        *a = (seg << 16) | (base & 0xffff);
  47        *b = (base & 0xffff0000) | ((type & 0xff) << 8) | (flags & 0xff);
  48}

...

  86#define write_idt_entry(dt, entry, a, b) write_dt_entry(dt, entry, a, b)

...

  88static inline void write_dt_entry(void *dt, int entry, __u32 entry_a, __u32 entry_b)
  89{
  90        __u32 *lp = (__u32 *)((char *)dt + entry*8);
  91        *lp = entry_a;
  92        *(lp+1) = entry_b;
  93}

这些代码的功能和上面摘出的汇编功能是一样的.

分析:

         _set_gate() 中,参数gate 和 addr 当然是 set_xxx_gate() 传给的中断号和处理程序的地址, type 参数指明要将 idt_table[gate] 初始化为什么类型的描述符, 定义为:

  52#define DESCTYPE_TASK   0x85    /* present, system, DPL-0, task gate */
  53#define DESCTYPE_INT    0x8e    /* present, system, DPL-0, interrupt gate */
  54#define DESCTYPE_TRAP   0x8f    /* present, system, DPL-0, trap gate */
  55#define DESCTYPE_DPL3   0x60    /* DPL-3 */

这些最终对应每一个 IDT 表项的 32~47 bits. 

_set_gate() 最后一个参数 seg 给出了处理程序所在段的段选择符. 这 4 个函数seg 参数都为 _KERNEL_CS. 

          pack_gate() 是将_set_gate() 的参数组合为两个无符号的 32 bits a, b, 分别对 idt_table 结构的两个数据项 a, b, 这样在 write_dt_entry() 中就可以使用这两个数对 idt_table[gate] 初始化了.

          和以上 4 个 set_xxx_gate() 稍有区别的是 set_task_gate() 函数:

 836static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
 837{
 838        _set_gate(idt_table+n,5,0,0,(gdt_entry<<3));
 839}
 840

           在 trap_init() 函数中, 调用 set_task_gate() 函数:

1141        set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);

           表示内核有严重的非法操作时才会触发该中断, 然后执行 doublefault_fn() 异常处理函数.

  18static void doublefault_fn(void)

补充:

         trap_init() 函数也只是对 IDT 的前 20 项(0~19) 和 128 号(SYSCALL_VECTOR)进行了最终初始化, 而 20~31 共 12 项是系统保留位使用, 那么剩下的 223 项又是在什么地方 init ? 如何 init 的呢 ?

asmlinkage void __init start_kernel(void)   // init/main.c
--->...
--->trap_init();
--->rcu_init();
--->init_IRQ();  // arch/i386/kernel/paravirt.c
      |--->paravirt_ops.init_IRQ();  
      |--->native_init_IRQ  // arch/i386/kernel/i8259.c

可以看出通过逐级调用, 最后调用的是 native_init_IRQ() 函数, 在该函数中,有一部分是对 IDT 剩下的 223 项进行 init:

 387void __init native_init_IRQ(void)
 388{
 389        int i;
               ...
 399        for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
 400                int vector = FIRST_EXTERNAL_VECTOR + i;  /* NR_VECTOR=256, FIRST_EXTERNAL_VECTOR为0X20(即 32), 故, 循环共进行 224 次, 其中当要 init 的中断号等于 SYSCALL_VECTOR(系统调用中断号)时, 就跳过, 所以就与前面说的 init 剩下 223 项, 呼应 */
 401                if (i >= NR_IRQS)
 402                        break;
 403                if (vector != SYSCALL_VECTOR) 
 404                        set_intr_gate(vector, interrupt[i]);
 405        }
                ...
 426}

现在关键是, 给这些中断的处理程序是什么? 这又要说到 interrupt[ ] 数组了.

arch/x86_64/kernel/i8259.c

  70#define IRQ(x,y) \
  71        IRQ##x##y##_interrupt
  72
  73#define IRQLIST_16(x) \
  74        IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
  75        IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
  76        IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
  77        IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
  78
  79/* for the irq vectors */
  80static void (*interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = {
  81                                          IRQLIST_16(0x2), IRQLIST_16(0x3),
  82        IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
  83        IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
  84        IRQLIST_16(0xc), IRQLIST_16(0xd), IRQLIST_16(0xe), IRQLIST_16(0xf)
  85};

            结合 native_init_IRQ() 分析一下. 当进行 第一次循环也就是 i=0 的时候, vector=32, 所以 set_intr_gate() 的调用是:

 404                        set_intr_gate(32, interrupt[0]);

对应的是:

IRQLIST_16(0x2)
--->IRQ(0x2,0)
   |--->IRQ0x20_interrupt()

这样就将 IRQ0x20_interrupt()设置为 0x20 中断的处理程序了.

下来分析, 这些 IRQn_interrupt()(n=0x20~0xff) 是如何建立的.

===================================================================================================================================

include/asm_x86_64/hw_irq.h

 125#define BUILD_IRQ(nr) \
 126asmlinkage void IRQ_NAME(nr); \
 127__asm__( \
 128"\n.p2align\n" \
 129"IRQ" #nr "_interrupt:\n\t" \
 130        "push $" #nr "-256 ; " \
 131        "jmp common_interrupt");

其中, common_interrupt 是一个汇编标号, 在它里面会调用 do_IRQ() 函数去处理中断, 这里又提到 irq_dec[ ] 数组了.

        从注册中断的角度来分析, 编程人员在内核模块里调用 request_irq() 函数, 注册一个中断, 那么最终中断处理程序被注册在 irq_desc 数组里了.该数组大小是 256 个, 每一个都是 struct irq_desc 类型. 关于 struct irq_desc 类型具体参考 /include/linux/irq.h

        从中断处理过程来讲, 当一个中断 n 产生后, 代码执行跳转到 IDT(idt_table[n])处, 取出 ‘中断处理程序’(并非都是真正的中断处理程序)段基址和段内偏移, 组合称为 '中断处理程序' 入口地址并跳到此处执行. 如果 0x00<=n<0x20, 那么该地址就是真正的中断处理程序, 执行之后就从中断返回. 

                                                   如果 0x20<=n<=255, 则跳转到 IRQn_interrupt 处, 继而跳转到 common_interrupt 处, 调用 do_IRQ() 执行中断处理. 这大概就是说下一个人do_IRQ() 处理过程.

        中断进入 do_IRQ() 函数后, 如果给出的中断号是合法(irq<0xff)的, 则调用 generic_handle_irq(irq); 
v2.6.20 /include/linux/irq.h

 290static inline void generic_handle_irq(unsigned int irq)
 291{
 292        struct irq_desc *desc = irq_desc + irq;
 293
 294#ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ
 295        desc->handle_irq(irq, desc);
 296#else
 297        if (likely(desc->handle_irq))
 298                desc->handle_irq(irq, desc);
 299        else
 300                __do_IRQ(irq);
 301#endif
 302}

这里看到, 如果 irq_desc[n]->handle_irq 为空, 则调用 _do_IRQ() 函数处理. 要说明的是该数据项在静态 init 的时候不为空, 一旦注册了一个真正的中断, 则该项就为空了, 所以是调用 _do_IRQ() 执行中断处理. 在 _do_IRQ() 函数里, 才真正调用了注册在 irq_desc[n] 里的中断处理函数来处理中断.

===================================================================================================================================

至此, 以上大概描述了从中断的 init, 中断的注册, 中断的处理过程.