kernel源码(十)asm.s和trap.c

0 回顾

在main.c中我们对各个设备进行了初始化动作,并创建了进程0,通过进程0创建了其他进程,现在操作系统内核已经初步初始化完成。但是在main.c中,我们讲解时对于如何初始化没有展开细讲,因为这些初始化动作分布在内核代码的各个文件中,从这一篇文章开始,我们讲解内核初始化的各个细节,可能会持续多篇文章。

现在开始讲解硬件中断(注意这里是硬件中断)的设置过程

1 硬件中断的主要流程

 当外设发生中断请求,中断请求会发送到8259A中断处理芯片,8259A芯片会暂存这个中断信号到自己的寄存器中,同时8259A芯片的输出引脚连接着CPU的INTR引脚,CPU在处理完当前指令后,会检查INTR引脚是否有中断信号到达,如果有中断信号到达,则CPU会发送信号给8259A的INTA引脚,8259A收到信号后,会把缓存的中断信号通过8259A的D0-d7引脚将中断信号发送到数据总线,CPU在数据总线上识别到中断信号,保护现场,中断描述符表idtr寄存器获取中断描述附表idt地址,在中断描述符表中根据中断类型获取中断处理程序地址(比如上面的&divide_error),处理中断,处理完中断恢复现场。

在CPU的INTR引脚检测到中断信号的同时,在同一时钟周期中会把下图黄色的寄存器(原ss,原sp,原eflags,cs,ip)压入当前进程的内核栈(非用户栈)。然后,根据中断控制芯片8259A发来的中断向量号查找中断描述符表idt,得到中断处理程序地址并压入内核栈(esp1处)。然后 jmp no_error_code 跳转到这里执行,在no_error_code中保护现场(esp1-esp2处),然后执行 call *%eax 开始执行中断处理程序,执行完毕,恢复现场(恢复当前进程在用户空间的执行状态)。最后执行iret命令完成硬件中断,弹出下图黄色部分标志的寄存器,返回用户态当前进程中断前的状态。

 2 Makefile

我们现在讲解kernel文件夹,该文件夹下有一个Makefile文件,这个文件描述了生成kernel.o的过程(kernel文件夹下的所有文件最终会被编译成一个kernel.o文件,这也是为什么asm.s能够调用trap.c中代码的原因)

AR    =gar
AS    =gas
LD    =gld
LDFLAGS    =-s -x
CC    =gcc
CFLAGS    =-Wall -O -fstrength-reduce -fomit-frame-pointer -fcombine-regs \
    -finline-functions -mstring-insns -nostdinc -I../include
CPP    =gcc -E -nostdinc -I../include

.c.s:
    $(CC) $(CFLAGS) \
    -S -o $*.s $<
.s.o:
    $(AS) -c -o $*.o $<
.c.o:
    $(CC) $(CFLAGS) \
    -c -o $*.o $<

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
    panic.o printk.o vsprintf.o sys.o exit.o \
    signal.o mktime.o

kernel.o: $(OBJS)
    $(LD) -r -o kernel.o $(OBJS)
    sync
View Code

3 asm.s

在阅读接下来内容之前,我们先复习一下进程的内核栈。通过https://www.cnblogs.com/zhenjingcool/p/15996841.html 3.3.1 进程中的内核栈,我们可以了解到:一个用户进程有两个栈,分别是用户栈和内核栈,分别存在于用户空间和内核空间。当进程在用户空间运行时,CPU的ss和sp寄存器指向的是用户栈;当进程在内核空间运行时,ss和sp指向的是进程的内核栈。

当发生中断或系统调用而陷入内核态执行时,进程会将原ss(进程用户栈基址)、原sp(进程用户栈sp)、原eflags(进程用户栈eflags)压入内核栈,然后将ss、sp为内核栈地址,这就完成了用户态向内核态的转变。

当进程从内核态恢复到用户态执行时,在内核态执行的最后将保存在内核栈里面的原ss、原sp恢复,这样就实现了内核态向用户态的转变。

下面进行asm.s的讲解

asm.s这个程序包括了大部分cpu能够探测到的异常的处理代码

下图是发生中断时堆栈的变化,可帮助我们理解代码

 注:上图黄色部分是硬件中断发生时CPU自动的行为。然后,中断发生时,CPU从中断控制器8092A中获取到中断号,然后内核查找中断向量表,找到中断处理函数压入栈中,也就是上图中的esp1处,然后esp1到esp2是保存现场。然后将error_code压入栈,接下来一步esp3处为什么这样做暂不清楚,好像不做这一步照样没问题。接下来执行call eax调用中断处理函数(call指令会将当前程序计数器PC的内容入栈,并将要调用的中断处理函数的地址送入PC,于是CPU的下一条指令就会转去执行中断处理函数)

 call执行完毕后,返回到esp3处,然后通过+8到达esp2处,然后就是一系列pop指令,恢复现场。pop完毕后iret返回,至此,本次硬件中断结束。这里的中断处理函数都比较简单,只是打印一些信息,后面会对这些中断处理函数进行重写(比如向特定进程发送signal,然后通过软中断处理比较耗时的操作)。

 源码

/*
 *  linux/kernel/asm.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * asm.s contains the low-level code for most hardware faults.
 * page_exception is handled by the mm, so that isn't here. This
 * file also handles (hopefully) fpu-exceptions due to TS-bit, as
 * the fpu must be properly saved/resored. This hasn't been tested.
 */

.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved

_divide_error:
    pushl $_do_divide_error
no_error_code:
    xchgl %eax,(%esp)
    pushl %ebx
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds
    push %es
    push %fs
    pushl $0        # "error code"
    lea 44(%esp),%edx
    pushl %edx
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
    call *%eax
    addl $8,%esp
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
    iret

_debug:
    pushl $_do_int3        # _do_debug
    jmp no_error_code

_nmi:
    pushl $_do_nmi
    jmp no_error_code

_int3:
    pushl $_do_int3
    jmp no_error_code

_overflow:
    pushl $_do_overflow
    jmp no_error_code

_bounds:
    pushl $_do_bounds
    jmp no_error_code

_invalid_op:
    pushl $_do_invalid_op
    jmp no_error_code

_coprocessor_segment_overrun:
    pushl $_do_coprocessor_segment_overrun
    jmp no_error_code

_reserved:
    pushl $_do_reserved
    jmp no_error_code

_irq13:
    pushl %eax
    xorb %al,%al
    outb %al,$0xF0
    movb $0x20,%al
    outb %al,$0x20
    jmp 1f
1:    jmp 1f
1:    outb %al,$0xA0
    popl %eax
    jmp _coprocessor_error

_double_fault:
    pushl $_do_double_fault
error_code:
    xchgl %eax,4(%esp)        # error code <-> %eax
    xchgl %ebx,(%esp)        # &function <-> %ebx
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds
    push %es
    push %fs
    pushl %eax            # error code
    lea 44(%esp),%eax        # offset
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    call *%ebx
    addl $8,%esp
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
    iret

_invalid_TSS:
    pushl $_do_invalid_TSS
    jmp error_code

_segment_not_present:
    pushl $_do_segment_not_present
    jmp error_code

_stack_segment:
    pushl $_do_stack_segment
    jmp error_code

_general_protection:
    pushl $_do_general_protection
    jmp error_code
View Code

首先,定义了全局的标号

.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved

0号中断_int0,把do_divide_error函数地址入栈(在trap.c中定义)(注:汇编中带下划线,c中不带下划线)

_divide_error:
    pushl $_do_divide_error

接着往下看,对照上图查看

no_error_code:
    xchgl %eax,(%esp) //交换eax和esp,交换后见上图显示。因为do_devide_error已经入栈,所以这里执行后esp位于图中esp1处,且堆栈里存放的是eax而且也是do_devide_error的地址
    pushl %ebx //ebx入栈
    pushl %ecx //ecx入栈
    pushl %edx //edx入栈
    pushl %edi //edi入栈
    pushl %esi //esi入栈
    pushl %ebp //ebp入栈
    push %ds //ds入栈
    push %es //es入栈
    push %fs //fs入栈
    pushl $0        # 错误码入栈,这里没有错误码,写0
    lea 44(%esp),%edx //lea为取有效地址,esp+44赋给edx
    pushl %edx //edx入栈,edx指向esp0处,也就是指向发生中断前的位置。
    movl $0x10,%edx
    mov %dx,%ds //代码段寄存器设置为0x10,在保护模式下段寄存器的唯一作用是存放段选择符,根据段选择符的结构,index=2,TI=0,RPL=0,我们知道这个数据段指向GDT的,1个段描述符(从0开始)
    mov %dx,%es //附加段寄存器设置为0x10
    mov %dx,%fs //标志段寄存器设置为0x10
    call *%eax //eax中存储的是do_divide_error的地址,因此,这里是调用do_divide_error函数
    addl $8,%esp //do_divide_error函数返回后,esp+8(注意堆栈是向下生长的),esp+8后堆栈指针到达esp2处
    pop %fs //弹出fs
    pop %es //弹出es
    pop %ds //弹出ds
    popl %ebp //弹出ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
    iret //返回

1号中断,调试中断的入口点

_debug:
    pushl $_do_int3        # _do_debug
    jmp no_error_code

和0号中断类似,区别是执行的中断处理函数不再是do_divide_error,而是do_int3

2号中断

_nmi:
    pushl $_do_nmi
    jmp no_error_code

3号中断

_int3:
    pushl $_do_int3
    jmp no_error_code

其他中断

_overflow: //4号中断
    pushl $_do_overflow
    jmp no_error_code

_bounds: //5号中断
    pushl $_do_bounds
    jmp no_error_code

_invalid_op: //6号中断
    pushl $_do_invalid_op
    jmp no_error_code

_coprocessor_segment_overrun: //9号中断
    pushl $_do_coprocessor_segment_overrun
    jmp no_error_code

_reserved: //其他保留的中断
    pushl $_do_reserved
    jmp no_error_code

45号中断

_irq13:
    pushl %eax
    xorb %al,%al
    outb %al,$0xF0
    movb $0x20,%al
    outb %al,$0x20
    jmp 1f
1:    jmp 1f
1:    outb %al,$0xA0
    popl %eax
    jmp _coprocessor_error

以上讲的中断都是无错误码的中断

下面开始是有错误码的中断

_double_fault: //8号中断_int8
    pushl $_do_double_fault
error_code:
    xchgl %eax,4(%esp)        # 错误码放到eax中
    xchgl %ebx,(%esp)        # 函数地址放到ebx中
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds
    push %es
    push %fs
    pushl %eax            # error code
    lea 44(%esp),%eax        # offset
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    call *%ebx
    addl $8,%esp
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
    iret

_invalid_TSS: //无效的任务状态段,10号中断
    pushl $_do_invalid_TSS
    jmp error_code

_segment_not_present: //段不存在,11号中断
    pushl $_do_segment_not_present
    jmp error_code

_stack_segment: //堆栈错误,12号中断
    pushl $_do_stack_segment
    jmp error_code

_general_protection: //13号中断,一般保护错误
    pushl $_do_general_protection
    jmp error_code

 

总结:asm.s定义了几个中断的处理方式。

4 traps.c

该文件主要作用是实现陷阱门的初始化。

具体为:在head.s中我们对中断描述符表idt进行了初始化,其默认中断处理函数为ignore_int,即所有硬件中断都会使用这个中断处理程序处理,打印“Unknown interrupt”。

在本文件中,对一些陷阱门进一步初始化,更新中断描述符表idt,替代默认的中断处理函数为真正的中断处理函数。注意此处处理的是陷阱门和系统门,中断门在sched.c中初始化的(比如时钟中断:set_intr_gate(0x20,&timer_interrupt);)

/*
 *  linux/kernel/traps.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * 'Traps.c' handles hardware traps and faults after we have saved some
 * state in 'asm.s'. Currently mostly a debugging-aid, will be extended
 * to mainly kill the offending process (probably by giving it a signal,
 * but possibly by killing it outright if necessary).
 */
#include <string.h>

#include <linux/head.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/segment.h>
#include <asm/io.h>

#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
    :"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \
    :"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

#define _fs() ({ \
register unsigned short __res; \
__asm__("mov %%fs,%%ax":"=a" (__res):); \
__res;})

int do_exit(long code);

void page_exception(void);

void divide_error(void);
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
void page_fault(void);
void coprocessor_error(void);
void reserved(void);
void parallel_interrupt(void);
void irq13(void);

static void die(char * str,long esp_ptr,long nr)
{
    long * esp = (long *) esp_ptr;
    int i;

    printk("%s: %04x\n\r",str,nr&0xffff);
    printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
        esp[1],esp[0],esp[2],esp[4],esp[3]);
    printk("fs: %04x\n",_fs());
    printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
    if (esp[4] == 0x17) {
        printk("Stack: ");
        for (i=0;i<4;i++)
            printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
        printk("\n");
    }
    str(i);
    printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
    for(i=0;i<10;i++)
        printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
    printk("\n\r");
    do_exit(11);        /* play segment exception */
}

void do_double_fault(long esp, long error_code)
{
    die("double fault",esp,error_code);
}

void do_general_protection(long esp, long error_code)
{
    die("general protection",esp,error_code);
}

void do_divide_error(long esp, long error_code)
{
    die("divide error",esp,error_code);
}

void do_int3(long * esp, long error_code,
        long fs,long es,long ds,
        long ebp,long esi,long edi,
        long edx,long ecx,long ebx,long eax)
{
    int tr;

    __asm__("str %%ax":"=a" (tr):"0" (0));
    printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",
        eax,ebx,ecx,edx);
    printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",
        esi,edi,ebp,(long) esp);
    printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",
        ds,es,fs,tr);
    printk("EIP: %8x   CS: %4x  EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}

void do_nmi(long esp, long error_code)
{
    die("nmi",esp,error_code);
}

void do_debug(long esp, long error_code)
{
    die("debug",esp,error_code);
}

void do_overflow(long esp, long error_code)
{
    die("overflow",esp,error_code);
}

void do_bounds(long esp, long error_code)
{
    die("bounds",esp,error_code);
}

void do_invalid_op(long esp, long error_code)
{
    die("invalid operand",esp,error_code);
}

void do_device_not_available(long esp, long error_code)
{
    die("device not available",esp,error_code);
}

void do_coprocessor_segment_overrun(long esp, long error_code)
{
    die("coprocessor segment overrun",esp,error_code);
}

void do_invalid_TSS(long esp,long error_code)
{
    die("invalid TSS",esp,error_code);
}

void do_segment_not_present(long esp,long error_code)
{
    die("segment not present",esp,error_code);
}

void do_stack_segment(long esp,long error_code)
{
    die("stack segment",esp,error_code);
}

void do_coprocessor_error(long esp, long error_code)
{
    if (last_task_used_math != current)
        return;
    die("coprocessor error",esp,error_code);
}

void do_reserved(long esp, long error_code)
{
    die("reserved (15,17-47) error",esp,error_code);
}

void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_error);
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3);    /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);
    outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xdf,0xA1);
    set_trap_gate(39,&parallel_interrupt);
}
View Code

取段的一个字节

#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
    :"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

程序退出的函数,这里是个函数原型,函数定义在exit.c中定义

 int do_exit(long code); 

页异常函数,在page相关的代码中定义

 void page_exception(void); 

下面这些函数原型,其定义都在本文件中

void divide_error(void);
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
void page_fault(void);
void coprocessor_error(void);
void reserved(void);
void parallel_interrupt(void);
void irq13(void);

下面这个函数是用来打印出错或中断信息的

static void die(char * str,long esp_ptr,long nr)
{
    long * esp = (long *) esp_ptr;
    int i;

    printk("%s: %04x\n\r",str,nr&0xffff);
    printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
        esp[1],esp[0],esp[2],esp[4],esp[3]);
    printk("fs: %04x\n",_fs());
    printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
    if (esp[4] == 0x17) {
        printk("Stack: ");
        for (i=0;i<4;i++)
            printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
        printk("\n");
    }
    str(i);
    printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
    for(i=0;i<10;i++)
        printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
    printk("\n\r");
    do_exit(11);        /* play segment exception */
}

下面的函数都是通过调用die函数对中断做出处理的

void do_double_fault(long esp, long error_code)
{
    die("double fault",esp,error_code);
}

void do_general_protection(long esp, long error_code)
{
    die("general protection",esp,error_code);
}

void do_divide_error(long esp, long error_code)
{
    die("divide error",esp,error_code);
}

在main.c中我们讲过,在main函数中调用了trap_init函数,这个函数就是在这里定义的。

在这个函数中,我们设置了各个中断的处理程序,比如 set_trap_gate(0,&divide_error); 表示_int0的中断处理程序是 divide_error , divide_error 和其他中断处理程序都是在asm.s中定义的。 set_trap_gate 函数在其他文件中定义的,在以后讲解。

 set_trap_gate 和 set_system_gate 的区别是,前者特权优先级为0,后者特权优先级为3.

void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_error);
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3);    /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);
    outb_p(inb_p(0x21)&0xfb,0x21); //允许中断处理芯片8259A芯片的IRQ2中断请求
    outb(inb_p(0xA1)&0xdf,0xA1); //允许8259A芯片的IRQ13
    set_trap_gate(39,&parallel_interrupt);
}

set_trap_gate在system.h中定义,如下。在这里设置idt表项。

#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)

我们把int17到48都设置为保留的

    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);

 

至此,中断门初始化完成,当外设发生中断请求,中断请求会发送到8259A中断处理芯片,8259A芯片会暂存这个中断信号到自己的寄存器中,同时8259A芯片的输出引脚连接着CPU的INTR引脚,CPU在处理完当前指令后,会检查INTR引脚是否有中断信号到达,如果有中断信号到达,则CPU会发送信号给8259A的INTA引脚,8259A收到信号后,会把缓存的中断信号通过8259A的D0-d7引脚将中断信号发送到数据总线,CPU在数据总线上识别到中断信号,保护现场,中断描述符表idtr寄存器获取中断描述附表idt地址,在中断描述符表中根据中断类型获取中断处理程序地址(比如上面的&divide_error),处理中断,处理完中断恢复现场。

 

posted @ 2022-03-13 02:34  zhenjingcool  阅读(304)  评论(0编辑  收藏  举报