<Linux内核学习>内核中断体系结构

环境:Linux 0.11 / Linux 3.4.2

参考书籍:Linux内核完全剖析基于0.11内核-赵炯

一、Linux中断机制

硬件中断信号源

在早期80x86的PC机器上的,其采用了两片8259A中断控制芯片,可以发出IRQ0~IRQ15中断请求信号。

对于ARM上则是由中断控制器(NVIC/ GIC)发出中断请求信号。

1.1 Linux中断分类

Linux中断可以分为两类硬中断和软中断:

① 硬中断:对于早期PC主机上类似于8259A的中断控制芯片发出的中断 /

ARM中断控制器发出的中断 /

② 软中断:CPU自行保留的中断 /

系统调用异常 /

1.2 Linux中断号

Linux内核中每个中断都是由0~255之前的一个数字标识,称为中断号。早期的PC电脑中断号分配:

(1) int0~int31由Inter公司自行保留。

(2) Linux系统中int32~int47被分配为8259A对应的IRQ0~IRQ15。

(3) int 128被设置为系统调用(system_call)。

1.3 Linux中断工作流程

以80x86为例子:

① 首先将寄存器值入栈:原ss、原esp、eflags、cs、eip。

ss为堆栈的段地址

esp为堆栈的栈顶地址

eflags为标志寄存器

cs为代码段寄存器、ip为指令帧寄存器两个寄存器共同指示了要读取的指令的地址

② 如果有出错码则将出错码error_code压入栈,无出错码则不压栈。

③ 将当前函数的返回值入栈(保证中断执行后能正常返回)。

④ 将ebx、ecx等寄存器的值压入栈。

⑤ 在执行中断处理函数前最后把error_code和 函数的返回值入栈,并将sp指针加上8个字节返回正常位置。(是为了向中断处理函数进行传参)。

异常处理堆栈变化示意图

⑥ 出栈函数返回值。

⑦ 返回所有入栈的寄存器值。

二、中断的代码实现过程

1.Linux中断代码结构

中断前的处理过程,中断的回复过程

中断的执行过程

硬件中断的处理过程

asm.s

trap.c

软中断及系统调用的处理过程

system_call.s

fork.c signal.c sys.c

2.硬件中断处理过程分析

① asm.s文件

在Linux 0.11内核中,asm.s文件主要涉及对intel保留的中断int0~int16的说明(实现执行中断前的现场保护,和执行完中断的现场恢复的过程),int17~int31保留使用。具体的中中断定义如下图。

Linux 中断/异常分为有错误码无错误码

代码分析以无中断码的do_divide_error为例,内容已经注释在代码中。

/*
 *  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

//.global定义了一些全局函数的声明,函数的具体实现在traps.c文件中。
//在0.11内核中全局函数打印了当前正在执行中断/异常的类型。



//int0--divide_error是处理处理被零除错的情况
//无中断码
_divide_error:
    pushl $_do_divide_error//把do_divide_error函数地址入栈,此时esp的内容为函数的地址
no_error_code://无中断码中断
    xchgl %eax,(%esp)    //利用xchgl指令将eax寄存器的地址和esp内容交换,eax值入栈,此时eax保存函数的地址
    pushl %ebx    //寄存器ebx值入栈
    pushl %ecx    //寄存器ecx入栈
    pushl %edx    //寄存器edx入栈
    pushl %edi    //寄存器edi入栈    
    pushl %esi    //寄存器esi入栈
    pushl %ebp    //寄存器ebp入栈
    push %ds    //寄存器ds入栈,16位寄存器入栈高位也占4个字节.
    push %es    //寄存器es入栈
    push %fs    //寄存器fs入栈
    pushl $0        # "error code"    //无错误码,将0入栈
    lea 44(%esp),%edx    //将esp指针的地址+44字节处的内容存入edx寄存器中,从前面的异常处理堆栈变化示意图可以看出是将esp0地址处的内容,也就是将中断函数返回的地址存入edx
    pushl %edx    //将edx存入栈
    movl $0x10,%edx    //初始化段寄存器ds,es,fs,加载内核的数据段选择符
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
    call *%eax    //eax保存的是do_divide_error函数的地址,此代码是调用eax内容地址的函数
    addl $8,%esp    //弹出最后入栈的两个参数值为39行和41行(参数要传递给调用的C程序),让堆栈指针指向fs处。
    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

② traps.c文件

traps.c文件主要描述在处理硬件中断/异常的时候asm.s文件中调用的c程序函数的具体定义。

die()函数用于在中断显示详细错误信息。

trap_init()函数是中断/异常初始化函数:其中的关键为两个宏定义都是用来初始化中断向量表。

① set_trap_gate 初始化中断向量表,并设置特权级为0

② set_system_gate 初始化中断向量表,并设置特权级为3

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

//esp_ptr 段指针
//nr 出错的段号
//总的来说die函数就是用来打印错误信息
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);
}
//asm.s调用的第一个函数在这里
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);
}

//中断的初始化函数
//set_trap_gate 优先级较低 只能由用户程序来调用
//set_system_gate 优先级很高 能由系统和用户所有的程序调用
void trap_init(void)
{
    int i;

    set_trap_gate(0,&divide_error);//如果被除数是0就会产生这个中断
    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);
}

3.软中断处理过程分析

① system_call.s文件

在system_call.s文件中主要实现了

(1)系统调用中断(0x80)入口处理程序

(2)信号检测处理过程

(3)sys_execve和sys_fork系统调用

(4)int 16协处理器处理过程/ int 7 设备不存在处理过程 / int 32 时钟中断处理过程 / int 46 硬盘中断处理过程 / int 38 软盘中断处理过程

初学Linux内核,先了解系统调用处理过程:

在Linux 0.11内核中,在执行中断号为(0x80)的中断处理函数中会识别存放于eax寄存器中的功能号,处理函数会根据功能号在system_call_table中执行内核提供的不同系统调用功能。

所有实现系统调用的函数,内核按功能号的顺序定义在include/linux/sys.h中system_call_table如下:

//定义系统调用的sys_call_table
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 };

系统调用的代码如下所示:

.align 2    //2^2=4字节对齐
bad_sys_call:    //错误的系统调用
    movl $-1,%eax    //将-1存入eax寄存器
    iret    //返回
.align 2    //4字节对齐
reschedule:    //重新执行调度程序
    pushl $ret_from_sys_call    //将当前进程执行的地址存入栈
    jmp _schedule    //调度


/**************************************/

//int 0x80 Linux 0.11系统调用的入口处理函数
//eax寄存器中存放调用号
.align 2    //4字节对齐
_system_call:    
    cmpl $nr_system_calls-1,%eax    //比较调用号是否超出范围,如果超出范围,则在ea中写入-1
    ja bad_sys_call
    push %ds    //保存原段寄存器
    push %es
    push %fs

//系统调用最多可以传递三个参数,亦可以无参数。参数的传递方式是通过栈传递的,第一个参数ebx,第二个参数ecx,
//第三个参数edx,寄存器的入栈顺序是GCC GNU规定的
//在文件include/unistd.h中133~183行存放中三种系统调用的宏定义
    pushl %edx
    pushl %ecx        # push %ebx,%ecx,%edx as parameters
    pushl %ebx        # to the system call
    movl $0x10,%edx       
    mov %dx,%ds
    mov %dx,%es    //ds,es段寄存器指向内核数据段
    movl $0x17,%edx        # fs points to local data space
    mov %dx,%fs    //fs段寄存器指向用户数据段
    call _sys_call_table(,%eax,4)    //调用对应的系统调用,计算方法sys_call_table地址 + 4*ax(调用号
//)位置的系统调用
    pushl %eax    //将系统调用返回值存入栈

//40~44行是检查当前任务的运行状态,如果不在就绪状态(state不等于0)就执行任务调度程序
//如果时间片等于0(counter=0)也执行任务调度程序
    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

系统调用的执行流程

猜你喜欢

转载自blog.csdn.net/qq_42174306/article/details/128635378