• linux0.11下的中断机制分析


        异常就是控制流中的突变,用来响应处理器状态中的某些变化。当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序,这张表即中断描述符表IDT。本文将针对Linux0.11代码进行分析和调试,来了解中断机制,主要分析以下三个问题:

    1.  中断描述符表的建立

    2.  一般中断的处理过程,以0x3号中断为例。

    3.  系统调用的处理过程,以fork系统调用为例。

    中断描述符表的建立

           中断描述符表(IDT)的创建代码在boot/head.s中,与全局描述符表的创建类似,内核执行lidt idt_descr指令完成创建工作,全局变量idt_descr的定义如下:

    idt_descr:

           .word 256*8-1              # idt contains 256 entries

           .long _idt

    _idt: .fill 256,8,0            # idt is uninitialized

           lidt指令为6字节操作数,它将_idt的地址加载进idtr寄存器,IDT被设置为包含256个8字节表项的描述符表。

     

           中断描述符表的初始化工作主要通过宏_set_get来完成,它定义于include/asm/system.h中,如下:

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

    /*设置中断门函数,特权级0,类型386中断门*/

    #define set_intr_gate(n,addr) /

           _set_gate(&idt[n],14,0,addr)

    /*设置陷阱门函数,特权级0,类型386陷阱门*/

    #define set_trap_gate(n,addr) /

           _set_gate(&idt[n],15,0,addr)

    /*设置系统调用函数,特权级3,类型386陷阱门*/

    #define set_system_gate(n,addr) /

           _set_gate(&idt[n],15,3,addr)

     

           内核将用这些宏初始化IDT表,代码如下:

    /*摘自kernel/traps.c,trap_init函数*/

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

           set_trap_gate(39,&parallel_interrupt);

    /*摘自kernel/chr_drv/serial.c,rs_init函数*/

           set_intr_gate(0x24,rs1_interrupt);

           set_intr_gate(0x23,rs2_interrupt);

    /*摘自kernel/chr_drv/console.c,con_init函数*/

           set_trap_gate(0x21,&keyboard_interrupt);

    /*摘自kernel/sched.c,sched_init函数*/

           set_intr_gate(0x20,&timer_interrupt);

           set_system_gate(0x80,&system_call);

    /*摘自kernel/blk_drv/hd.c,hd_init函数*/

           set_intr_gate(0x2E,&hd_interrupt);

    /*摘自kernel/blk_drv/floppy.c,floppy_init函数*/

           set_trap_gate(0x26,&floppy_interrupt);

     

           每个中断向量号具体意义这里不做说明,有兴趣的同志可以参考清华大学出版社出版的《保护方式下的80386及其编程》和赵炯博士的《Linux内核完全注释》;中断调用的具体过程将在后面的例子中详细分析。现在我们关心的是初始化完毕的IDT,调试查看这张表的内容,选取0x0号、0x20号、0x80号中断作为例子。通过查看System.map文件可知:0x0号中断调用的divide_error函数地址为0x8dec,0x20号中断调用的timer_interrupt函数地址为0x74f4,0x80号中断调用的system_call函数地址为0x7418。当内核第一次调用fork函数创建进程0的子进程时,IDT表已经初始化完毕,因此我们在fork函数地址0x753c处设置断点,启动bochsdgb进行调试,命令行如下:

    <bochs:1> break 0x753c

    <bochs:2> c

    (0) Breakpoint 1, 0x753c in ?? ()

    Next at t=16879006

    (0) [0x0000753c] 0008:0000753c (unk. ctxt): call .+0x93d4             ; e8931e00

    00

    <bochs:3> dump_cpu

    ……

    idtr:base=0x54b8, limit=0x7ff

    ……

     

           IDT基址为0x54b8,0号中断描述符的地址为0x54b8+0*8=0x54b8,20号中断描述符的地址为0x54b8+0x20*8= 0x55b8,80号中断描述符的地址为0x54b8+0x80*8=0x58b8,分别查看内存这三个地址的8字节内容,命令行如下:

    <bochs:4> x /2 0x54b8

    [bochs]:

    0x000054b8 <bogus+       0>:    0x00088dec      0x00008f00

    <bochs:5> x /2 0x55b8

    [bochs]:

    0x000055b8 <bogus+       0>:    0x000874f4      0x00008e00

    <bochs:6> x /2 0x58b8

    [bochs]:

    0x000058b8 <bogus+       0>:    0x00087418      0x0000ef00

     

           门描述符具有如下形式:

    m+7

    m+6

    m+5

    m+4

    m+3

    m+2

    m+1

    m+0

    Offset(31...16)

    Attributes

    Selector

    Offset(15...0)

     

    Byte m+5

    Byte m+4

    BIT7

    BIT6

    BIT5

    BIT4

    BIT3

    BIT2

    BIT1

    BIT0

    BIT7

    BIT6

    BIT5

    BIT4

    BIT3

    BIT2

    BIT1

    BIT0

    P

    DPL

    DT0

    TYPE

    000

    Dword Count

                                   

     

           因此调试信息显示,0x0号中断描述符中断调用地址为0x0008:0x00008dec,是一个特权级为0的386陷阱门,0x20号中断描述符中断调用函数地址为0x0008:0x000074f4,是一个特权级为0的386中断门,0x80号中断描述符中断调用函数地址为0x0008:0x00007418,是一个特权级为3的386陷阱门。这和预先分析的情况一致。

    任务的内核态堆栈

           在分析中断响应过程之前,先介绍一下任务的内核态堆栈。

           当中断事件发生时,中断源向cpu发出申请,若cpu受理,则保存当前的寄存器状态、中断返回地址等许多信息,然后cpu转去执行相应的事件处理程序。中断处理完毕后,cpu将恢复之前保存的信息,并继续原来的工作。因为中断处理需要在内核态下进行,因此每个任务都有一个内核态堆栈,用来完成中断处理中保护现场和恢复现场的工作。这个内核态堆栈与每个任务的任务数据结构放在同一页面内,在创建新任务时,fork函数在任务tss内核级字段中设置,代码位于kernel/fork.c的copy_process函数中,如下:

    /*p即需创建的新任务*/

           p->tss.esp0 = PAGE_SIZE + (long) p;

           p->tss.ss0 = 0x10;

           tss.esp0和tss.ss0的值在任务内核态工作时不会被改变,因此任务每次进入内核态工作时,这个堆栈总是空的。

    一般中断的处理过程

           0x3号中断用于暂停程序的执行,通过查看Linux代码,可以知道对这个中断的处理仅仅是打印一些寄存器状态信息。选取这个中断作为例子的意义在于:它有一个完整的保护现场和恢复现场的过程(比如0x0号中断的处理将直接终止进程而不需要恢复现场);中断信号可以由用户态的程序产生。

           0x3号中断处理程序int3在kernel/asm.s中定义,如下:

    #源代码书写顺序并非如此,这样排列是为了阅读的方便

    _int3:

           pushl $_do_int3

           jmp no_error_code

    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

           这里有个问题:如果发生特权级改变,用户态的堆栈指针在什么时候保存和恢复?答案是cpu响应中断时自动将这些数据入栈,执行iret指令时自动将这些数据出栈。下面的实验可以验证这一点。

     

           接下来的试验比较繁琐,按照以下步骤进行:

    1.  编写产生0x3号中断的程序。

    2.  int3函数地址处设置断点,查看此时内核态堆栈的内容,即验证保护现场的动作。

    3.  执行直到中断返回,验证iret指令的作用,即验证恢复现场的动作。

          

           编写产生0x3号中断的程序非常简单,启动bochs+linux-0.11-devel-040329(这个img由赵炯博士加入了gcc)。用vi创建编辑一个c文件int3.c,代码如下:

    #include <stdio.h>

     

    int main()

    {

      __asm__(“int3”);

      return 0;

    }

           编译这个文件产生执行程序int3。

     

           通过查看System.map文件可知0x3号中断处理函数_int3的地址为0x8e2f。启动bochsdgb进行调试,命令行如下:

    <bochs:1> b 0x8e2f

    <bochs:2> c   #同时在启动的Linux下运行int3程序,将获得下面这些信息

    (0) Breakpoint 1, 0x8e2f in ?? ()

    Next at t=143245141

    (0) [0x00008e2f] 0008:00008e2f (unk. ctxt): push 0x7af4               ; 68f47a00

    00

     

           首先关注一下内核堆栈中的内容,当前任务(0x60-0x20)/8=8号任务的tss结构中的ss0和esp0字段包含了内核态堆栈的段描述符和堆栈指针,tss结构的地址由GDT表的TSS描述符提供。继续调试,命令行如下:

    <bochs:3> dump_cpu

    ……

    esp:0xfa3fec  #这个值在在后面的分析将用到

    ……

    tr:s=0x60, dl=0x32e80068, dh=0x89fa, valid=1

    gdtr:base=0x5cb8, limit=0x7ff

    ……

    <bochs:4> x /2 0x5d18  #0x5cb8+0x60=0x5d18

    [bochs]:

    0x00005d18 <bogus+       0>:    0x32e80068      0x00008bfa

    <bochs:5> x /26 0x00fa32e8

    [bochs]:

    0x00fa32e8 <bogus+       0>:    0x00000000      0x00fa4000      0x00000010

    0x00000000

    0x00fa32f8 <bogus+      16>:    0x00000000      0x00000000      0x00000000

    0x00000000

    0x00fa3308 <bogus+      32>:    0x000398af      0x00000246      0x00000000

    0x00000005

    0x00fa3318 <bogus+      48>:    0x000574c0      0x00000014      0x03fffdd8

    0x03fffde4

    0x00fa3328 <bogus+      64>:    0x00000001      0x00000000      0x00000017

    0x0000000f

    0x00fa3338 <bogus+      80>:    0x00000017      0x00000017      0x00000017

    0x00000017

    0x00fa3348 <bogus+      96>:    0x00000068      0x80000000

     

           对这些调试信息按照tss字段的顺序排列得出下表:

    BIT31—BIT16

    BIT15—BIT1

    BIT0

    Offset

    Data

    0000000000000000

    链接字段

    0

    0x00000000

    ESP0

    4

    0x00fa4000

    0000000000000000

    SS0

    8

    0x00000010

    ESP1

    0CH

    0x00000000

    0000000000000000

    SS1

    10H

    0x00000000

    ESP2

    14H

    0x00000000

    0000000000000000

    SS2

    18H

    0x00000000

    CR3

    1CH

    0x00000000

    EIP

    20H

    0x000398af

    EFLAGS

    24H

    0x00000246

    EAX

    28H

    0x00000000

    ECX

    2CH

    0x00000005

    EDX

    30H

    0x000574c0

    EBX

    34H

    0x00000014

    ESP

    38H

    0x03fffdd8

    EBP

    3CH

    0x03fffde4

    ESI

    40H

    0x00000001

    EDI

    44H

    0x00000000

    0000000000000000

    ES

    48H

    0x00000017

    0000000000000000

    CS

    4CH

    0x0000000f

    0000000000000000

    SS

    50H

    0x00000017

    0000000000000000

    DS

    54H

    0x00000017

    0000000000000000

    FS

    58H

    0x00000017

    0000000000000000

    GS

    5CH

    0x00000017

    0000000000000000

    LDTR

    60H

    0x00000068

    I/O许可位图偏移

    000000000000000

    T

    64H

    0x80000000

                   表1:任务8的tss结构

     

           由表1可知:任务8内核态堆栈的起始堆栈指针为0x00fa4000。查看寄存器状态可知当前堆栈指针指向0x00fa3fec,与栈顶相差20/4 = 5个字,调试查看这5个字的内容,命令行如下:

    <bochs:6> x /5 0xfa3fec

    [bochs]:

    0x00fa3fec <bogus+       0>:    0x0000001c      0x0000000f      0x00010202

    0x03fffefc

    0x00fa3ffc <bogus+      16>:    0x00000017

     

           这些信息就是cpu在进入int3中断处理之前自动保存的信息,参考赵炯博士的《Linux内核完全注释》可知:在用户程序(进程)将控制权交给中断处理程序之前,cpu会首先将至少12字节的信息压入中断处理程序的堆栈中。这种情况与一个长调用(段间子程序调用)比较相像。Cpu会将代码段选择符合返回地址的偏移值压入堆栈。另一个与段间调用比较相像的地方是80386将信息压入到了目的代码的堆栈上。当发生中断时,这个目的堆栈就是内核态堆栈。另外cpu还总是将标志寄存器EFLAGS的内容压入堆栈。如果优先级别发生变化,比如从用户级改变到内核系统级,cpu还会将原代码的堆栈段值和堆栈指针压入中断程序的堆栈中。

           按照堆栈向下增长方向整理调试信息,如下表所示:

    0x0000

    SS

    0x00000017

    ESP

    0x03fffefc

    EFLAGS

    0x00010202

    0x0000

    CS

    0x0000000f

    EIP

    0x0000001c

             表2:发生中断时堆栈的内容

     

           执行iret指令返回时也类似从一个段间子程序调用的返回,堆栈中的这些内容将自动弹出到响应寄存器中,完成中断返回恢复现场的动作。调试来验证这一过程,命令行如下:

    <bochs:7> n   #7,8,9指令都是为了找到iret的位置

    Next at t=172477604

    (0) [0x00008e34] 0008:00008e34 (unk. ctxt): jmp .+0x8df1              ; ebbb

    <bochs:8> n

    Next at t=172477605

    (0) [0x00008df1] 0008:00008df1 (unk. ctxt): xchg dword ptr ss:[esp], eax ; 87042

    4

    <bochs:9> u /30

    ……

    00008e20: (                    ): iretd                     ; cf

    <bochs:10> b 0x8e20

    <bochs:11> c

    (0) Breakpoint 2, 0x8e20 in ?? ()

    Next at t=172498467

    (0) [0x00008e20] 0008:00008e20 (unk. ctxt): iretd                     ; cf

    <bochs:12> n   #中断返回

    Next at t=172498468

    (0) [0x00fac01c] 000f:0000001c (unk. ctxt): xor eax, eax              ; 31c0

    <bochs:13> dump_cpu

    ……

    esp:0x3fffefc

    eflags:0x10202

    eip:0x1c

    cs:s=0xf, dl=0x0, dh=0x10c0fa00, valid=1

    ss:s=0x17, dl=0x3fff, dh=0x10c0f300, valid=1

    ……

           无需解释,表2和上面寄存器状态信息即可说明问题。

    系统调用的处理过程

           以系统调用fork函数为例,它的定义如下:

    /*摘自init/main.c*/

    static inline _syscall0(int,fork)

    /*摘自include/unistd.h*/

    #define __NR_fork 2

    /*摘自include/unistd.h*/

    #define _syscall0(type,name) /

    type name(void) /

    { /

    long __res; /

    __asm__ volatile ("int $0x80" /

           : "=a" (__res) /

           : "0" (__NR_##name)); /

    if (__res >= 0) /

           return (type) __res; /

    errno = -__res; /

    return -1; /

    }

     

           __NR_fork值2是系统调用中断处理的跳转表的索引,这张系统调用函数指针表定义如下:

    /*摘自include/linux/sched.h*/

    typedef int (*fn_ptr)();

    /*摘自include/linux/sys.h*/

    fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,

    sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,

    sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,

    sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,

    sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,

    sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,

    sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,

    sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,

    sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,

    sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,

    sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,

    sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,

    sys_setreuid,sys_setregid };

           sys_call_table[2]的值是sys_fork函数指针,这个函数的功能不是我们研究的重点,有兴趣的同志可以参考其它资料。

     

           将宏_syscall0和__NR_fork展开:

    staic inline

    int fork(void)

    {

    long __res;

    __asm__ volatile ("int $0x80"

           : "=a" (__res)

           : "0" (2));  /* eax的值置为2*/

    if (__res >= 0)

           return (int) __res;

    errno = -__res;

    return -1;

    }

     

           现在fork函数的功能就很清楚了:将eax的值置为2,产生0x80中断,0x80中断的中断处理函数是system_call(还记得吗?set_system_gate(0x80,&system_call))。system_call定义如下:

    _system_call:

           cmpl $nr_system_calls-1,%eax  #eax保存系统调用跳转函数表的索引值

           ja bad_sys_call

           push %ds   #保护现场

           push %es

           push %fs

           pushl %edx

           pushl %ecx            # push %ebx,%ecx,%edx as parameters

           pushl %ebx            # to the system call

           movl $0x10,%edx         # set up ds,es to kernel space

           mov %dx,%ds

           mov %dx,%es

           movl $0x17,%edx         # fs points to local data space

           mov %dx,%fs

           call _sys_call_table(,%eax,4)  #通过系统调用跳转函数表调用相关处理程序

           pushl %eax

           movl _current,%eax

           cmpl $0,state(%eax)             # state 当前进程未就绪则进行进程调度

           jne reschedule

           cmpl $0,counter(%eax)         # counter  时间片用完进行则进程调度

           je reschedule

    ret_from_sys_call:

           movl _current,%eax             # task[0] cannot have signals

           cmpl _task,%eax

           je 3f

           cmpw $0x0f,CS(%esp)        # was old code segment supervisor ?

           jne 3f

           cmpw $0x17,OLDSS(%esp)        # was stack segment = 0x17 ?

           jne 3f

           movl signal(%eax),%ebx

           movl blocked(%eax),%ecx

           notl %ecx

           andl %ebx,%ecx

           bsfl %ecx,%ecx

           je 3f

           btrl %ecx,%ebx   #有信号则调用信号处理程序

           movl %ebx,signal(%eax)

           incl %ecx

           pushl %ecx

           call _do_signal

           popl %eax       #恢复现场

    3:     popl %eax

           popl %ebx

           popl %ecx

           popl %edx

           pop %fs

           pop %es

           pop %ds

           iret      #中断返回

     

           cpu 处理0x80中断与一般中断处理过程是一样的:压入cs,eip,eflags到目标堆栈,中断返回则从堆栈中弹出这些值到相应寄存器。其中断处理函数将通过系统调用函数指针表来处理相应系统调用。这个过程就不做验证了,有兴趣的同志可以参考一般中断处理的调试过程。

    eip的值

           在cpu响应中断源时,压入的eip的值,中断返回将这个值弹出加载到eip,用这样的方式继续应用程序控制流。这个eip的值将根据不同的异常来确定:

    类别

    原因

    异步/同步

    返回行为

    中断

    来自I/O设备的信号

    异步

    总是返回到下一条指令

    陷阱

    有意的异常

    同步

    总是返回到下一条指令

    故障

    潜在可恢复的错误

    同步

    根据故障是否可修复决定要么重新执行当前指令,要么终止

    终止

    不可修复的错误

    同步

    不会返回

           表3:异常的类别(摘自《深入理解计算机系统》)

     

           之前分析到的0x3号中断和0x80号中断即属于“陷阱”,因此它们中断处理完毕后总是由内核态转换到用户态(通过分段机制,段寄存器加载不同的段描述符),并返回到应用程序的下一条指令。

    后记

           中断处理的行为和长调用(段间子程序调用)的行为颇为相似,理解长调用的处理过程即可理解中断处理过程。计算机理论中很多概念都是相通的,因此,扎实的基本功完全可以触类旁通的指导我们开发应用程序。

  • 相关阅读:
    MYSQL增量备份与恢复
    Centos7上MariaDB数据库启动问题解决
    mysql数据库的常用命令
    mysql数据库用户权限设置
    使mysql数据库支持简体中文
    如何在mysql数据库中开启使用tab键补全功能
    忘记mysql超户密码的解决方法
    Excel教程(复习)
    MySQL教程(复习)
    Linux教程(复习)
  • 原文地址:https://www.cnblogs.com/wanghj-dz/p/3985354.html
Copyright © 2020-2023  润新知