ucore异常处理分析

Ucore异常处理详解

前言

本文由大量内容引用或者摘自以下来源,对原作者表示感谢

  1. ucore实验指导书
  2. ucore注释版

对于文章整体,我们将先对相关定义以及数据结构进行讲解,然后大致梳理异常处理的开启过程,最后讲解异常处理的过程.全程是以文件作为线索.

希望你对异常处理的相关概念比较熟悉,不然相关定义和结构体可能就看不太懂了.

一.相关定义与结构体

mmu.h定义了许多东西,除了许多宏和物理内存管理的一些变量函数,还包括中断描述符的格式,建立中断门的函数等等

1.mmu.h


/* *
 * Set up a normal interrupt/trap gate descriptor
 *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
 *   - sel: Code segment selector for interrupt/trap handler
 *   - off: Offset in code segment for interrupt/trap handler
 *   - dpl: Descriptor Privilege Level - the privilege level required
 *          for software to invoke this interrupt/trap gate explicitly
 *          using an int instruction.
 *
*gate其实是多个结构体,分别对应着callgate,interrupt gate和trap gate(具体是struct gatedesc)
* 在mmu.h里面定义,我们看下面的函数,是采用define而不是一个正常的函数.这样可以方便的解决这个问题(gate不是一个具体的类型),但说到底,无论是哪一个gate的结构体,其实都是某某门的描述符的一个结构体,结构体中的每一部分就对应着这个descripter的每一个字段的值.
 */
#define SETGATE(gate, istrap, sel, off, dpl) {               \
        (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;      \
        (gate).gd_ss = (sel);                                \
        (gate).gd_args = 0;                                 \
        (gate).gd_rsv1 = 0;                                 \
        (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
        (gate).gd_s = 0;                                    \
        (gate).gd_dpl = (dpl);                              \
        (gate).gd_p = 1;                                    \
        (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
    }
//中断,陷阱门描述符
struct gatedesc {
//冒号后面的是这个字段所占的位数
//从低位到高位,依次是:
    unsigned gd_off_15_0 : 16;      // low 16 bits of offset in segment,偏移地址
    unsigned gd_ss : 16;            // segment selector,段选择符
    unsigned gd_args : 5;           // # args, 0 for interrupt/trap gates
    unsigned gd_rsv1 : 3;           // reserved,为0
    unsigned gd_type : 4;           // 门的类型,不同的门此字段不同
    unsigned gd_s : 1;              // must be 0 (system)
    unsigned gd_dpl : 2;            // descriptor(meaning new) privilege level,特权级
    unsigned gd_p : 1;              // Present,p=1表示段存在,反之不存在
    unsigned gd_off_31_16 : 16;     // high bits of offset in segment,偏移地址
};

对照上面的中断描述符的结构体,我们来看下面的图,一一对照就可以对中断描述符有一个清晰的认识
中断描述符给出了一个段选择符和32位的偏移地址,段选择符指示异常处理程序或者中断服务程序所在段的段描述符在gdt或者ldt中的位置,偏移地址则给出对应处理程序的第一条指令所在的偏移量
中断描述符

2.vector.s

vector.S 文件通过 vectors.c 自动生成,其中定义了每个中断的入口程序和入口地址 (保存在 vectors 数组中)。其中,中断可以分成两类:一类是压入错误编码的 (error code),另一类不压入错误编码。对于第二类, vector.S 自动压入一个 0。此外,还会压入相应中断的中断号。在压入两个必要的参数之后,中断处理函数跳转到统一的入口 alltraps 处。

#这里是代码段
.text
.globl __alltraps

# 定义每个中断的入口和地址

# 对于中断处理,下面每一个都是
# 1 压入errcode 或 0
# 2 压入中断号

.globl vector0
vector0:
  pushl $0
  pushl $0
  jmp __alltraps
.globl vector1
vector1:
  pushl $0
  pushl $1
  jmp __alltraps
.globl vector2
vector2:
.......

# vector table
# 这里是数据段,每一项都是一个long,32,也就是每一个vector的入口地址.(在上面定义的)
.data
.globl __vectors
__vectors:
  .long vector0
  .long vector1
  .long vector2
  .long vector3
  .long vector4
  .long vector5
  .long vector6
  ............

3.中断描述符表

static struct gatedesc idt[256] = {{0}}//中断描述符表,是一个中断描述符类型的数组

4.trapframe中断/陷阱帧

在出现了中断(这里泛指)的时候,往往会涉及到进程切换,函数调用以及权限级的切换,而在这种切换中,我们使用了中断帧这样一个数据结构,将中断处理当作一个栈帧放在栈里,保存了中断处理前的各种信息,可以更方便的完成我们的各种调用,保存数据与切换.
中断帧的描述在trap.h里面

struct trapframe {
    struct pushregs tf_regs;    // pushal,寄存器组的值
    uint16_t tf_gs;//gs寄存器
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;  //中断号
    /* 下列字段由 x86 硬件定义 */
    uint32_t tf_err;    //错误号
    uintptr_t tf_eip;   // 产生中断后
    uint16_t tf_cs;     //出错的代码段
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* 下列字段近用于跨级别调用,如从 user 跨入 kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;    //出错的栈
    uint16_t tf_padding5;
} __attribute__((packed));//取消结构在编译过程中的优化对齐,按照实际结构对齐.

/* 执行pushal 指令时,这些寄存器值入栈 */
/* 参考 syscall,这些都是系统调用时传入的参数和返回值参数;eax 接收返回值 */
struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;          /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

二.异常处理的开启

先来看init.c,这个函数是内核初始化的总控函数,这一部分(异常处理的开启)会按照这个文件中的函数为线索进行分析

1.init.c

//用中文注释出来的是需要注意的
int kern_init(void) {
    extern char edata[], end[];
    memset(edata, 0, end - edata);  // clear bss 

    cons_init();                // 控制台初始化,初始化了外设,键盘等

    print_history();
    print_kerninfo();

    //grade_backtrace();

    pmm_init();                 // init physical memory management

    pic_init();                 // 中断控制初始化
    idt_init();                 // 中断描述符表初始化

    vmm_init();                 // init virtual memory management
    sched_init();               // init scheduler
    proc_init();                // init process table
    
    ide_init();                 // init ide devices
    swap_init();                // init swap
    fs_init();                  // init fs
    
    clock_init();               // 时钟中断初始化
    intr_enable();              // 中断请求初始化
    
    cpu_idle();                 // run idle process

2.console.c

外设基本初始化设置

Lab1实现了中断初始化和对键盘、串口、时钟外设进行中断处理。串口的初始化函数serial_init(位于/kern/driver/console.c)中涉及中断初始化工作的很简单:

......
// 使能串口1接收字符后产生中断
    outb(COM1 + COM_IER, COM_IER_RDI);
......
// 通过中断控制器使能串口1中断
pic_enable(IRQ_COM1);

键盘的初始化函数kbd_init(位于kern/driver/console.c中)完成了对键盘的中断初始化工作,具体操作更加简单:

......
// 通过中断控制器使能键盘输入中断
pic_enable(IRQ_KBD);

3.pic_init(driver/picirq.c)

这个函数主要是初始化了8259A的中断处理控制器,涉及到具体芯片的硬件以及很多数字电路的内容,对于8259A感兴趣的可以参考下面一篇博客
8259A

/* pic_init - initialize the 8259A interrupt controllers */
void
pic_init(void) {
    did_init = 1;

    // mask all interrupts
    outb(IO_PIC1 + 1, 0xFF);
    outb(IO_PIC2 + 1, 0xFF);

    // Set up master (8259A-1)

    // ICW1:  0001g0hi
    //    g:  0 = edge triggering, 1 = level triggering
    //    h:  0 = cascaded PICs, 1 = master only
    //    i:  0 = no ICW4, 1 = ICW4 required
    outb(IO_PIC1, 0x11);

    // ICW2:  Vector offset
    outb(IO_PIC1 + 1, IRQ_OFFSET);

    // ICW3:  (master PIC) bit mask of IR lines connected to slaves
    //        (slave PIC) 3-bit # of slave's connection to master
    outb(IO_PIC1 + 1, 1 << IRQ_SLAVE);

    // ICW4:  000nbmap
    //    n:  1 = special fully nested mode
    //    b:  1 = buffered mode
    //    m:  0 = slave PIC, 1 = master PIC
    //        (ignored when b is 0, as the master/slave role
    //         can be hardwired).
    //    a:  1 = Automatic EOI mode
    //    p:  0 = MCS-80/85 mode, 1 = intel x86 mode
    outb(IO_PIC1 + 1, 0x3);

    // Set up slave (8259A-2)
    outb(IO_PIC2, 0x11);    // ICW1
    outb(IO_PIC2 + 1, IRQ_OFFSET + 8);  // ICW2
    outb(IO_PIC2 + 1, IRQ_SLAVE);       // ICW3
    // NB Automatic EOI mode doesn't tend to work on the slave.
    // Linux source code says it's "to be investigated".
    outb(IO_PIC2 + 1, 0x3);             // ICW4

    // OCW3:  0ef01prs
    //   ef:  0x = NOP, 10 = clear specific mask, 11 = set specific mask
    //    p:  0 = no polling, 1 = polling mode
    //   rs:  0x = NOP, 10 = read IRR, 11 = read ISR
    outb(IO_PIC1, 0x68);    // clear specific mask
    outb(IO_PIC1, 0x0a);    // read IRR by default

    outb(IO_PIC2, 0x68);    // OCW3
    outb(IO_PIC2, 0x0a);    // OCW3

    if (irq_mask != 0xFFFF) {
        pic_setmask(irq_mask);
    }
    LOG_LINE("初始化完毕:中断控制器");
}

4.idt_init(trap.c)

操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责处理特定的中断事件。系统将所有的中断事件统一进行了编号(0~255),这个编号称为中断向量。以ucore为例,操作系统内核启动以后,会通过 idt_init 函数初始化 idt 表 (参见trap.c),而其中 vectors 中存储了中断处理程序的入口地址。vectors 定义在 vector.S 文件中,通过一个工具程序 vector.c 生成。在上文已做叙述.
其中仅有 System call 中断的权限为用户权限 (DPL_USER),即仅能够使用 int 0x80 指令。此外还有对 tickslock 的初始化,该锁用于处理时钟中断。

static struct gatedesc idt[256] = {{0}}//中断描述符表,是一个中断描述符类型的数组
/* 初始化kern/trap/vectors.S的中断向量表 */
void
idt_init(void) {
    /**
     * 中断处理函数的入口地址定义在__vectors,位于kern/trap/vector.S
     */ 
    LOG_LINE("初始化开始:中断向量表");
    LOG("idt_init:\n\n");

    extern uintptr_t __vectors[];
    int i;
    //遍历idt的每一项,并将每一项的详细信息填到idt的这一项里面
    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
        //SETGATE(gate, istrap, sel, off, dpl) 
        //初始化idt每一项,将vectors里面保存的中断向量处理入口地址写到idt(描述符)里面
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
    //下面这一行的权限是DPL_USER,和上面不一样
    SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
    /*
    lidt:asm volatile ("lidt (%0)" :: "r" (pd) : "memory");
    将idt_pd加载到 IDTR,idt_pd有两项,第一项是idt的大小,第二项是idt的入口地址
    */   
    lidt(&idt_pd);

	....

    LOG_LINE("初始化完毕:中断向量表");
}

首先理一下数据结构的关系,idt是一个数组,数组的每一项是一个中断描述符类型,这个类型里面保存了相应中断处理程序的位置(段描述符,偏移地址),而这个地址实在vectors里面保存的.对应的数据结构在上文也已经给出详解,如果还没有理清楚可以回去看看.

idt_init这个函数主要就是把idt的每一项写进去,其中中断处理函数的入口地址来自vectors.

5.clock.c

时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。这样当一个应用程序运行了一定时间后,操作系统会通过时钟中断获得CPU控制权,并可把CPU资源让给更需要CPU的其他应用程序。时钟的初始化函数clock_init(位于kern/driver/clock.c中)完成了对时钟控制器8253的初始化:

......
//设置时钟每秒中断100次
    outb(IO_TIMER1, TIMER_DIV(100) % 256);
    outb(IO_TIMER1, TIMER_DIV(100) / 256);
// 通过中断控制器使能时钟中断
    pic_enable(IRQ_TIMER);

最后,在intr.c里面,开启外设中断请求

/* intr_enable - enable irq interrupt */
void
intr_enable(void) {
    sti();
}

即在x86.h中的

//sti = Set Interupt, 允许中断
static inline void
sti(void) {
    asm volatile ("sti");
}

调用sti的汇编指令,允许中断

三.异常处理的流程

trap函数(定义在trap.c中)是对中断进行处理的过程,所有的中断在经过中断入口函数__alltraps预处理后 (定义在 trapentry.S中) ,都会跳转到这里。在处理过程中,根据不同的中断类型,进行相应的处理。在相应的处理过程结束以后,trap将会返回,被中断的程序会继续运行。

# vectors.S 把所有 trap 发送到此
.text
.globl __alltraps
__alltraps:
    # 中断陷入执行至此,由于 int 指令的结果,栈上已有
    # | trapno  |
    # |  errno  |
    # |   eip   |
    # |   cs    |
    # |  eflags |
    # |   ss    |
    # |   sp    |

    # 继续 push 寄存器值,以在栈空间构造struct trapframe中断帧.每个 pushl 都压入一个 uint32,对应 trapframe 中两个 uint16.
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    # pushal = pushregs
    pushal

    # 设置内核的 ds,es段地址为GD_KDATA,准备内核环境
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

    # 把刚刚构造的 trap frame 的地址作为参数传入trap(),并调用
    pushl %esp
    call trap

    # 清理调用 trap 前压栈的 esp
    popl %esp

trap.c

/**
 * 处理并分发一个中断或异常,包括所有异常
 * 如果返回,则trapentry.S 恢复 cpu 之前的状态(栈上保存,以 trapframe 的结构),然后执行 iret返回.
 */ 
void
trap(struct trapframe *tf) {
    //LOG("陷阱预处理,维护中断嵌套\n");
    // 基于陷阱的类型,分发 trapframe
    //current是当前进程
    if (current == NULL) {
    	//陷阱调度,参数为陷阱帧的地址
        trap_dispatch(tf);
    }
    else {
        // 在栈上维护一个 trapframe 链,用于处理中断嵌套
        //1 暂存当前进程的上一个 tf
        struct trapframe *otf = current->tf;//otf=old tf
        //2 更新当前进程的 tf
        current->tf = tf;
        //判断是不是内核代码的trap
        bool in_kernel = trap_in_kernel(tf);

        // 处理当前tf,这是重点
        trap_dispatch(tf);
        // 恢复上一个 tf
        current->tf = otf;

        // 只有用户态进程可以抢占.内核进程不可抢占
        if (!in_kernel) {
            // 检查进程标志,是否被标记为希望"被"退出
            if (current->flags & PF_EXITING) {
                do_exit(-E_KILLED);
            }
            // 每次系统调用,当前进程都可能被标记为应被调度.此时进行调度刷新工作
            if (current->need_resched) {
            //进行进程调度
                schedule();
            }
        }
    }
//上文的中断调度函数
static void trap_dispatch(struct trapframe *tf) {
    //LOG("trap_dispatch start:\n");
    //LOG("开始分发中断.中断号:%u\n",tf->tf_trapno);
    char c;

    int ret=0;
    //根据trap frame里面的trapno来进行不同的处理
    switch (tf->tf_trapno) {
    case T_PGFLT:  //page fault
        LOG("内核检测到缺页异常中断.\n");
        if ((ret = pgfault_handler(tf)) != 0) {//在这个pgfault_handler里面处理了缺页异常,下文会重点介绍
            print_trapframe(tf);
            if (current == NULL) {
                panic("handle pgfault failed. ret=%d\n", ret);
            }
            else {
                if (trap_in_kernel(tf)) {
                    panic("handle pgfault failed in kernel mode. ret=%d\n", ret);
                }
                LOG("killed by kernel.\n");
                panic("handle user mode pgfault failed. ret=%d\n", ret); 
                do_exit(-E_KILLED);
            }
        }
        break;
    case T_SYSCALL:
        syscall();
        break;
    case IRQ_OFFSET + IRQ_TIMER:
.................//这里是一些其他的中断处理的case,省略
    default:
        print_trapframe(tf);
        if (current != NULL) {
            LOG("unhandled trap.\n");
            do_exit(-E_KILLED);
        }
        // in kernel, it must be a mistake
        panic("unexpected trap in kernel.\n");

    }
    //LOG("trap_dispatch end\n");
}
# 通过__trapret从中断返回.
# 中断返回:
# 
.globl __trapret
__trapret:
    # 1. 恢复各种寄存器值
    popal

    popl %gs
    popl %fs
    popl %es
    popl %ds

    # 去掉 trap number 和 error code
    addl $0x8, %esp
    # int n 命令:
    #   1. 标志寄存器入栈
    #   2. CS,IP 入栈,(IP)=(n*4),  (CS)=(n*4+2)
    # iret  命令:
    #   1. 恢复 CS,IP
    #   2. 恢复标志寄存器
    #   3. 恢复 ESP,SS.(权限发生变化)
    iret

整个中断处理流程大致如下:
在这里插入图片描述
在这里插入图片描述
中断处理过程

最后

整个中断处理的过程还是比较好懂的,只要理解了几个关键的数据结构的关系(vectors,idt[],trapframe)再接下来看函数调用就会比较轻松.
但是如果想要搞懂每一个细节还是比较难的,就比如上文提到的8259A的中断处理控制器,键盘中断,时钟中断,不仅要求你熟悉软件,对于硬件和数字电路还需要一定量的知识,在这里我也不是很熟悉并且也不打算挖得那么深,就跳过了,有兴趣的可以深挖看看.

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值